Skip to main content

usage/
parse.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use std::sync::Arc;
9use strum::EnumTryAs;
10
11#[cfg(feature = "docs")]
12use crate::docs;
13use crate::error::UsageErr;
14use crate::spec::arg::SpecDoubleDashChoices;
15use crate::spec::unknown_flags::UnknownFlags;
16use crate::warn::Warning;
17use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
18
19/// Merge a subcommand's flags into the currently available flags when descending
20/// into that subcommand.
21///
22/// On descent we drop the parent's non-global flags (they are scoped to the parent)
23/// but keep its global flags so they remain recognized further down. A subcommand may
24/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
25/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
26/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
27/// entirely and later parsing would treat the already-consumed global token as an
28/// unexpected positional/flag value.
29///
30/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
31/// command describes another program, which does not accept the mounting CLI's globals.
32/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
33/// re-parses them), but the mounted command's own flags take precedence over them, so its
34/// choices/completions are not replaced by a global's. Which flags a completion may offer
35/// there is a separate question, answered by [`ParseOutput::completion_flags`].
36fn merge_subcommand_flags(
37    available: &mut BTreeMap<String, Arc<SpecFlag>>,
38    new_flags: BTreeMap<String, Arc<SpecFlag>>,
39    crossing_mount: bool,
40) {
41    // Keep only inherited global flags from the parent.
42    available.retain(|_, f| f.global);
43
44    if crossing_mount {
45        // A mounted command owns its flags outright, including names an inherited global also
46        // uses: a word after the mounted command belongs to the mounted program. Words before
47        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
48        // mounted command does not declare (e.g. a global's short) stay inherited.
49        for (key, flag) in new_flags {
50            available.insert(key, flag);
51        }
52        return;
53    }
54
55    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
56    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
57    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
58    // Maps each merged flag produced below back to the inherited global it was merged from, so
59    // the collision check can compare *origins*: a flag this loop already merged is not a
60    // different global, even though it is a different `Arc`.
61    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
62    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
63    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
64        let ptr = Arc::as_ptr(flag) as usize;
65        *merged_origin.get(&ptr).unwrap_or(&ptr)
66    }
67
68    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
69    // map's existing intra-subcommand collision resolution: when two flags in the same command
70    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
71    // to its last-declared owner, and we must not change which flag owns it.
72    for (key, flag) in new_flags {
73        if flag.global {
74            // A child that re-declares (or adds) a global flag stays recognized everywhere.
75            available.insert(key, flag);
76            continue;
77        }
78
79        // A non-global re-declaration that shares a LONG name with an inherited global flag is
80        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
81        // global). Keep the global flag (global precedence, so it survives the next descent's
82        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
83        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
84        // deliberate: a re-declaration sharing only a short letter with an unrelated global
85        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
86        // handled by the `contains_key` skip below instead.
87        let inherited_global = flag.long.iter().find_map(|l| {
88            available
89                .get(&format!("--{l}"))
90                .filter(|f| f.global)
91                .cloned()
92        });
93        if let Some(global_flag) = inherited_global {
94            // Never clobber a *different* inherited global's alias. If this re-declaration's
95            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
96            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
97            // precedence dictates, instead of stealing the alias for the merged flag.
98            //
99            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
100            // earlier key of this same child already replaced some of them with the merged flag,
101            // which the lookups above may now resolve to. That is the same logical flag, so it
102            // must not read as a collision and leave this key on the pre-merge global.
103            let global_origin = origin_of(&merged_origin, &global_flag);
104            if available.get(&key).is_some_and(|existing| {
105                existing.global && origin_of(&merged_origin, existing) != global_origin
106            }) {
107                continue;
108            }
109            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
110                Some(merged) => merged.clone(),
111                None => {
112                    let mut merged = (*global_flag).clone();
113                    // `exclusive` is deliberately *not* reconciled here, in either direction.
114                    // One object now answers to two alias sets that may disagree: the child
115                    // owns the spellings it declared, the ancestor keeps the ones only it
116                    // declared. A single bool cannot hold both, so the merged flag carries the
117                    // ancestor's and validation resolves the occurrence by the spelling that
118                    // was typed — the ledger it already consults to decide whether selecting
119                    // the child is company.
120                    for s in &flag.short {
121                        if !merged.short.contains(s) {
122                            merged.short.push(*s);
123                        }
124                    }
125                    for l in &flag.long {
126                        if !merged.long.contains(l) {
127                            merged.long.push(l.clone());
128                        }
129                    }
130                    // A child may deliberately promote one of the ancestor's hidden aliases.
131                    // Hidden lists are subsets of the accepted spellings, so a spelling present
132                    // on the child but absent from its hidden subset is visible at this level.
133                    merged.hidden_short_aliases.retain(|alias| {
134                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
135                    });
136                    merged.hidden_aliases.retain(|alias| {
137                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
138                    });
139                    for s in &flag.hidden_short_aliases {
140                        if !merged.hidden_short_aliases.contains(s) {
141                            merged.hidden_short_aliases.push(*s);
142                        }
143                    }
144                    for l in &flag.hidden_aliases {
145                        if !merged.hidden_aliases.contains(l) {
146                            merged.hidden_aliases.push(l.clone());
147                        }
148                    }
149                    let merged = Arc::new(merged);
150                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
151                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
152                    // Rebind the global's *other* aliases onto the merged flag. The loop only
153                    // visits keys the child declared, so an alias the child left out (the `-y` of
154                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
155                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
156                    // logical flag must be one object under every key it answers to.
157                    for existing in available.values_mut() {
158                        if origin_of(&merged_origin, existing) == global_origin {
159                            *existing = Arc::clone(&merged);
160                        }
161                    }
162                    merged
163                }
164            };
165            available.insert(key, merged);
166            continue;
167        }
168
169        // Purely-local flag (shares nothing with an inherited global), or one that collides only
170        // on a short with an unrelated global. Insert this alias but never shadow an inherited
171        // global flag. Such non-global flags are dropped by the next descent's `retain`.
172        if available.contains_key(&key) {
173            continue;
174        }
175        available.insert(key, flag);
176    }
177}
178
179/// Build the lookup keys a flag is registered under in `available_flags`:
180/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
181fn flag_keys(flag: &SpecFlag) -> Vec<String> {
182    let mut keys: Vec<String> = flag
183        .long
184        .iter()
185        .map(|l| format!("--{l}"))
186        .chain(flag.short.iter().map(|s| format!("-{s}")))
187        .collect();
188    if let Some(negate) = &flag.negate {
189        keys.push(negate.clone());
190    }
191    keys
192}
193
194/// The flags a command declares, keyed by each of their aliases.
195fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
196    cmd.flags
197        .iter()
198        .flat_map(|f| {
199            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
200            flag_keys(&f)
201                .into_iter()
202                .map(|key| (key, Arc::clone(&f)))
203                .collect::<Vec<_>>()
204        })
205        .collect()
206}
207
208fn unique_flags<'a>(
209    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
210) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
211    let mut seen = HashSet::new();
212    flags
213        .into_iter()
214        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
215}
216
217/// Every flag a command accepts, resolved the way parsing an invocation of it
218/// resolves them.
219///
220/// `chain` runs from the root command (`spec.cmd`) down to the command in
221/// question; an empty chain yields no flags.
222///
223/// This is not "the command's flags plus its ancestors' globals". A subcommand
224/// that re-declares a global's long name is describing the *same* flag rather
225/// than a new one, so the global's help, argument and effect survive and only
226/// the re-declaration's extra aliases are added — see
227/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
228/// going through this will disagree with what the parser actually accepts.
229pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
230    let Some((root, rest)) = chain.split_first() else {
231        return vec![];
232    };
233    let mut available = gather_flags(root);
234    for cmd in rest {
235        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
236    }
237
238    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
239    // global that has both a short and a long, the merged flag is written under
240    // the long key while the short key keeps pointing at the pre-merge `Arc` —
241    // two objects for one logical flag. That is harmless for parsing, which
242    // looks flags up by key, but a caller listing flags would see it twice.
243    //
244    // Names break the tie because a long key always sorts before a short one
245    // (`--x` < `-y` at the second byte), so the merged declaration is the one
246    // reached first. Two genuinely distinct flags sharing a name is a spec bug
247    // that `usage lint` reports as a duplicate flag.
248    let mut seen_names = HashSet::new();
249    unique_flags(available.values())
250        .filter(|f| seen_names.insert(f.name.clone()))
251        .cloned()
252        .collect()
253}
254
255/// Extract the flag key from a flag word for lookup in available_flags map
256/// Handles both long flags (--flag, --flag=value) and short flags (-f)
257fn get_flag_key(word: &str) -> &str {
258    if word.starts_with("--") {
259        // Long flag: strip =value if present
260        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
261    } else if let Some((end, _)) = word.char_indices().nth(2) {
262        // Short flag: the dash and one letter, which is one character and not
263        // necessarily one byte.
264        &word[0..end]
265    } else {
266        word
267    }
268}
269
270/// Where a value came from, when it did not come from the command line.
271///
272/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
273/// token for the flag and none for the value, and that distinction is the whole question
274/// a spec author is asking when they ask why `--color` came out `always`. Values that were
275/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
276#[derive(Debug, Clone, PartialEq, Eq)]
277#[non_exhaustive]
278pub enum ValueOrigin {
279    /// A flag that takes a value was given without one, so the declaration supplied it:
280    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
281    /// One variant for both, because from argv's side the same thing happened — the flag
282    /// was typed and the value was not.
283    DefaultMissing,
284    /// An environment variable, named.
285    ///
286    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
287    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
288    /// not say which declaration fired or which one to delete.
289    Env(String),
290    /// A declared `default`, on the flag or on the flag's argument.
291    ///
292    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
293    /// a fact about the value, and `usage lint` is the place to complain about declaring
294    /// both.
295    Default,
296    /// A `default_if` whose condition matched, with the condition that decided it. The
297    /// selector alone is ambiguous — several conditions may name it with different `when`
298    /// values.
299    DefaultIf {
300        selector: String,
301        when: Option<String>,
302    },
303}
304
305/// What one word of the command line became.
306///
307/// Several because a single token can do more than one thing: `-abc` sets three flags,
308/// `-j8` is a flag and its value.
309#[derive(Debug, Clone)]
310#[non_exhaustive]
311pub enum TokenRole {
312    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
313    Program,
314    /// Selected a subcommand.
315    Command { name: String },
316    /// Named a flag, in this spelling. `negated` for the `negate` form.
317    Flag {
318        flag: Arc<SpecFlag>,
319        spelling: String,
320        negated: bool,
321    },
322    /// Supplied a flag's value. Several values when a `delimiter` split the word.
323    Value {
324        flag: Arc<SpecFlag>,
325        values: Vec<String>,
326        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
327        /// rather than following it as its own word.
328        attached: bool,
329    },
330    /// Filled a positional argument. Several values when a `delimiter` split the word.
331    Arg {
332        arg: Arc<SpecArg>,
333        values: Vec<String>,
334    },
335    /// An explicit `--`, consumed as a separator.
336    Separator,
337    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
338    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
339    /// without a role the word reads as having done nothing while a whole help page arrives
340    /// in the error list.
341    Builtin { spelling: String },
342    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
343    /// declaration whose run it closed — the word is not one of that run's values, which is
344    /// the whole reason it was declared.
345    ValueTerminator { ends: String },
346    /// A declared `restart_token`: the positional cursor and the values it had filled start
347    /// over here. Recorded because the words before it are still in the report, and without
348    /// this row they look like they filled arguments that then came back empty.
349    Restart,
350    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
351    /// under `unknown_flags="value"`, and `None` when the word was refused.
352    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
353    /// The word reached a declaration that would not take it, and was dropped. Without this
354    /// the token reads as having done nothing, which is the one thing it did not do.
355    Refused { reason: String },
356    /// Forwarded to an external subcommand.
357    External,
358    /// The parser stopped before this word — a help request, a refused value.
359    Unread,
360}
361
362/// One word of the command line, and what it became.
363#[derive(Debug, Clone)]
364#[non_exhaustive]
365pub struct TokenBinding {
366    /// Position in the argv slice the parse was given, argv[0] included.
367    pub index: usize,
368    pub word: String,
369    /// Roles a word the parser made up contributed, folded onto the token it was derived
370    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
371    /// argv[0]. `word` is what the caller wrote, not what the parser read.
372    pub synthesized: bool,
373    pub roles: Vec<TokenRole>,
374}
375
376#[non_exhaustive]
377pub struct ParseOutput {
378    pub cmd: SpecCommand,
379    pub cmds: Vec<SpecCommand>,
380    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
381    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
382    /// What each word of the command line became, in argv order, one entry per word.
383    ///
384    /// The token half of provenance; [`ParseOutput::flag_origins`] and
385    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
386    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
387    /// a token that bound to nothing, so both exist.
388    pub tokens: Vec<TokenBinding>,
389    /// Where a flag's value came from when it did not come from argv, in the order the
390    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
391    ///
392    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
393    /// `default_missing` value, so one flag can have several.
394    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
395    /// Where an argument's value came from when it did not come from argv. Keyed as
396    /// [`ParseOutput::args`] is.
397    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
398    /// Flags a later occurrence removed, and the flag that removed them.
399    ///
400    /// The overriding name is the half a caller needs: the fallback phase silently declines
401    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
402    /// otherwise" has no answer without it.
403    pub overridden_flags: BTreeMap<String, String>,
404    /// Every flag the parser recognizes at this point, keyed by each of its aliases
405    /// (`--long`, `-s`, negations).
406    ///
407    /// This includes flags that only remain recognized because they may appear *before* a
408    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
409    /// should offer.
410    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
411    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
412    pub errors: Vec<UsageErr>,
413    /// Deprecated declarations this command line used, for the caller to render when its
414    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
415    /// not used anything yet.
416    pub warnings: Vec<Warning>,
417    /// The positional argument the next word would have filled, i.e. where the parser's
418    /// cursor stopped. `None` once every argument is satisfied.
419    ///
420    /// Completions need exactly this: the parser already accounts for `var_max`, for
421    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
422    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
423    pub next_arg: Option<Arc<SpecArg>>,
424    /// Whether an explicit `--` was consumed *as a separator*.
425    ///
426    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
427    /// of the variadic argument collecting it, not a separator, so it does not unlock a
428    /// `double_dash="required"` argument.
429    pub double_dash_seen: bool,
430    /// Remaining argv captured when an unmatched word was forwarded as an external
431    /// subcommand: the command name first, then every token after it.
432    ///
433    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
434    pub external: Option<Vec<String>>,
435}
436
437impl ParseOutput {
438    /// The flags a completion should offer for the parsed command.
439    ///
440    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
441    /// command has been reached, though, the commands above it belong to the mounting CLI and
442    /// their flags are not accepted there — mise, for example, forwards everything after a task
443    /// name to the task itself — so only the flags declared from the mount boundary down are
444    /// offered. Those globals stay in `available_flags` because they may legitimately appear
445    /// *before* the mounted command.
446    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
447        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
448            return self.available_flags.clone();
449        };
450        // A mount can also merge flags from its spec's root into the command it is mounted on
451        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
452        // replay starts one level up to inherit its globals.
453        let start = match boundary.checked_sub(1) {
454            Some(prev) if self.cmds[prev].flags_from_mount => prev,
455            _ => boundary,
456        };
457        // Re-run the descent from there, which starts with no inherited flags. Below the
458        // boundary the mounted program's commands are ordinary commands, so the descents use
459        // the same merge as the real parse.
460        let mut offered = gather_flags(&self.cmds[start]);
461        for cmd in &self.cmds[start + 1..] {
462            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
463        }
464        offered
465    }
466}
467
468#[derive(Debug, EnumTryAs, Clone)]
469pub enum ParseValue {
470    Bool(bool),
471    String(String),
472    MultiBool(Vec<bool>),
473    MultiString(Vec<String>),
474}
475
476/// The deprecated declarations argv itself named: the commands it descended through, and the
477/// flags it bound.
478///
479/// Called before the environment and defaults have filled anything, because afterwards nothing
480/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
481/// differently, at the point where each is applied.
482///
483/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
484/// including `--help`, and the compiled parser reports selected commands rather than the one the
485/// process already is.
486fn collect_deprecations(out: &mut ParseOutput) {
487    for cmd in out.cmds.iter().skip(1) {
488        if cmd.deprecated.is_none()
489            && cmd.deprecated_warn_at.is_none()
490            && cmd.deprecated_remove_at.is_none()
491        {
492            continue;
493        }
494        out.warnings.push(Warning::command(
495            cmd.name.clone(),
496            cmd.deprecated.clone(),
497            cmd.deprecated_warn_at.clone(),
498            cmd.deprecated_remove_at.clone(),
499        ));
500    }
501    for flag in out.flags.keys() {
502        if let Some(warning) = flag_deprecation(flag) {
503            out.warnings.push(warning);
504        }
505    }
506}
507
508/// A warning for a flag that was used, if its declaration is deprecated at all.
509fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
510    if flag.deprecated.is_none()
511        && flag.deprecated_warn_at.is_none()
512        && flag.deprecated_remove_at.is_none()
513    {
514        return None;
515    }
516    Some(Warning::flag(
517        flag_spelling(flag),
518        flag.deprecated.clone(),
519        flag.deprecated_warn_at.clone(),
520        flag.deprecated_remove_at.clone(),
521    ))
522}
523
524/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
525/// about `old-flag` would be about a word nobody typed.
526fn flag_spelling(flag: &SpecFlag) -> String {
527    flag.long
528        .first()
529        .map(|long| format!("--{long}"))
530        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
531        .unwrap_or_else(|| flag.name.clone())
532}
533
534/// The name this flag reads first, which is what to use instead of a deprecated alias.
535fn flag_current_env(flag: &SpecFlag) -> Option<String> {
536    flag.env
537        .clone()
538        .or_else(|| flag.env_fallback.first().cloned())
539}
540
541fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
542    flag.deprecated_env.iter().any(|declared| declared == name)
543}
544
545/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
546fn arg_current_env(arg: &SpecArg) -> Option<String> {
547    arg.env
548        .clone()
549        .or_else(|| arg.env_fallback.first().cloned())
550}
551
552fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
553    arg.deprecated_env.iter().any(|declared| declared == name)
554}
555
556/// The first of `names` that is set, and which one it was.
557///
558/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
559/// aliases, so the winner's identity is what says whether a value arrived through an alias.
560/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
561/// disagree with it.
562fn first_set_env<'a>(
563    mut names: impl Iterator<Item = &'a str>,
564    get_env: &impl Fn(&str) -> Option<String>,
565) -> Option<(&'a str, String)> {
566    names.find_map(|name| get_env(name).map(|value| (name, value)))
567}
568
569/// Builder for parsing command-line arguments with custom options.
570///
571/// Use this when you need to customize parsing behavior, such as providing
572/// a custom environment variable map instead of using the process environment.
573///
574/// # Example
575/// ```
576/// use std::collections::HashMap;
577/// use usage::Spec;
578/// use usage::parse::Parser;
579///
580/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
581/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
582///
583/// let result = Parser::new(&spec)
584///     .with_env(env)
585///     .parse(&["cmd".into()])
586///     .unwrap();
587/// ```
588#[non_exhaustive]
589pub struct Parser<'a> {
590    spec: &'a Spec,
591    env: Option<HashMap<String, String>>,
592    mount_outputs: Option<HashMap<String, String>>,
593}
594
595impl<'a> Parser<'a> {
596    /// Create a new parser for the given spec.
597    pub fn new(spec: &'a Spec) -> Self {
598        Self {
599            spec,
600            env: None,
601            mount_outputs: None,
602        }
603    }
604
605    /// Use a custom environment variable map instead of the process environment.
606    ///
607    /// This is useful when parsing for tasks in a monorepo where the env vars
608    /// come from a child config file rather than the current process environment.
609    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
610        self.env = Some(env);
611        self
612    }
613
614    /// Inject deterministic outputs for mount commands instead of executing them.
615    ///
616    /// Keys are the exact `run` strings declared by mount nodes and values are the
617    /// usage specs those commands would print. When this is set, every encountered
618    /// mount must have an entry. Production parsing remains process-backed unless a
619    /// caller explicitly opts into injection.
620    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
621        self.mount_outputs = Some(outputs);
622        self
623    }
624
625    /// Parse the input arguments.
626    ///
627    /// Returns the parsed arguments and flags, with defaults and env vars applied.
628    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
629        let out = self.parse_collecting(input)?;
630        if let Some(err) = out
631            .errors
632            .iter()
633            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
634        {
635            bail!("{err}");
636        }
637        if !out.errors.is_empty() {
638            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
639        }
640        Ok(out)
641    }
642
643    /// Everything the parse learned, whether or not it succeeded.
644    ///
645    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
646    /// caller about to act on a command line. A caller that wants to *explain* one wants
647    /// the opposite: the bindings that worked and every complaint about the rest, since a
648    /// report saying only "missing required <src>" is the report you already had.
649    ///
650    /// Failures that stop the parse dead — a mount that will not run, a word no
651    /// declaration can take — still come back as `Err`. There is no output to describe in
652    /// those cases; see [`Parser::explain`] for what to do about it.
653    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
654        self.parse_collecting(input)
655    }
656
657    /// The binding phase's own answer for a line [`Parser::explain`] refused.
658    ///
659    /// `Ok` when the binding phase finished and the failure came after it — a flag left
660    /// waiting for a value, say. Everything argv supplied is there and only the
661    /// environment-and-defaults phase is missing.
662    ///
663    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
664    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
665    /// is more useful next to the three tokens that did bind than on its own. The word that
666    /// caused the failure carries a role saying so, and everything still queued behind it is
667    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
668    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
669    /// than in the line, such as a mount that will not run, stops the trace where it stopped
670    /// and the words past it carry no role.
671    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
672        let mut trace = Trace::new(input);
673        match parse_partial_traced(
674            self.spec,
675            input,
676            self.env.as_ref(),
677            self.mount_outputs.as_ref(),
678            MountTiming::WhenAWordIsUnknown,
679            &mut trace,
680        ) {
681            // A parse that got as far as stopping normally already moved its tokens onto the
682            // output, which is where a caller should read them from.
683            Ok((out, _)) => Ok(out),
684            Err(_) => Err(trace.tokens),
685        }
686    }
687
688    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
689        let custom_env = self.env.as_ref();
690        let (mut out, overridden_flags) = parse_partial_with_env(
691            self.spec,
692            input,
693            custom_env,
694            self.mount_outputs.as_ref(),
695            MountTiming::WhenAWordIsUnknown,
696        )?;
697        trace!("{out:?}");
698
699        // A flag still waiting for a value never got one, so the command line ended
700        // mid-flag. `parse_partial` leaves this for completions to look at — a
701        // half-typed `--jobs ` is exactly what a completion is asked about — but a
702        // full parse has nothing left to wait for, and dropping the flag silently
703        // made a forgotten value look like a working command.
704        while try_bind_default_missing(
705            &mut out.flags,
706            &mut out.flag_awaiting_value,
707            custom_env,
708            &mut out.flag_origins,
709        )? {}
710        if let Some(flag) = out.flag_awaiting_value.first() {
711            let token = flag
712                .long
713                .first()
714                .map(|l| format!("--{l}"))
715                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
716                .unwrap_or_else(|| flag.name.clone());
717            let rendered = input.join(" ");
718            let span = rendered
719                .rfind(&token)
720                .map(|at| (at, token.len()))
721                .unwrap_or((0, 0));
722            return Err(UsageErr::InvalidFlag {
723                token,
724                reason: "requires an argument".to_string(),
725                span: span.into(),
726                input: rendered,
727            }
728            .into());
729        }
730
731        // Before the environment and defaults have their turn, because both mark a field as
732        // filled and only argv can be reported as something the user typed. Env is reported
733        // where it is applied, below; a default is nobody's request and reports nothing.
734        collect_deprecations(&mut out);
735
736        let get_env = |key: &str| -> Option<String> {
737            if let Some(env_map) = custom_env {
738                env_map.get(key).cloned()
739            } else {
740                std::env::var(key).ok()
741            }
742        };
743
744        // Apply env vars and defaults for args
745        //
746        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
747        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
748        for arg in out.cmd.args.iter() {
749            if out.args.contains_key(arg) {
750                continue;
751            }
752            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
753                if arg_env_is_deprecated(arg, env_name) {
754                    out.warnings
755                        .push(Warning::env(env_name, arg_current_env(arg)));
756                }
757                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
758                validate_choice_values(
759                    ChoiceTarget::arg(arg),
760                    &values,
761                    arg.choices.as_ref(),
762                    custom_env,
763                )?;
764                let parsed = if arg.var {
765                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
766                    ParseValue::MultiString(values)
767                } else {
768                    ParseValue::String(values.into_iter().next().unwrap_or_default())
769                };
770                out.args.insert(Arc::new(arg.clone()), parsed);
771                out.arg_origins
772                    .entry(Arc::new(arg.clone()))
773                    .or_default()
774                    .push(ValueOrigin::Env(env_name.to_string()));
775                continue;
776            }
777            if !arg.default.is_empty() {
778                // Consider var when deciding the type of default return value
779                if arg.var {
780                    let values = split_fallback_values(&arg.default, arg.delimiter);
781                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
782                    validate_choice_values(
783                        ChoiceTarget::arg(arg),
784                        &values,
785                        arg.choices.as_ref(),
786                        custom_env,
787                    )?;
788                    // For var=true, always return a vec (MultiString)
789                    out.args
790                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
791                    out.arg_origins
792                        .entry(Arc::new(arg.clone()))
793                        .or_default()
794                        .push(ValueOrigin::Default);
795                } else {
796                    validate_choice_value(
797                        ChoiceTarget::arg(arg),
798                        &arg.default[0],
799                        arg.choices.as_ref(),
800                        custom_env,
801                    )?;
802                    // For var=false, return the first default value as String
803                    out.args.insert(
804                        Arc::new(arg.clone()),
805                        ParseValue::String(arg.default[0].clone()),
806                    );
807                    out.arg_origins
808                        .entry(Arc::new(arg.clone()))
809                        .or_default()
810                        .push(ValueOrigin::Default);
811                }
812            }
813        }
814
815        // Environment first, for every flag, so a `default_if` can see a sibling
816        // that was filled from env. Applying both in one pass would make the
817        // answer depend on declaration order: `--bin-names` before `--json`
818        // would miss `EX_JSON=1`.
819        let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
820        for flag in &flags {
821            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
822                continue;
823            }
824            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
825                // The flag's own deprecation before the alias's, which is the order the
826                // compiled parser reports them in: it walks a command's flags and then its
827                // aliases. Using a deprecated flag through a variable is still using it.
828                if let Some(warning) = flag_deprecation(flag) {
829                    out.warnings.push(warning);
830                }
831                if flag_env_is_deprecated(flag, env_name) {
832                    out.warnings
833                        .push(Warning::env(env_name, flag_current_env(flag)));
834                }
835                if let Some(arg) = flag.arg.as_ref() {
836                    let values =
837                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
838                    validate_choice_values(
839                        ChoiceTarget::option(flag),
840                        &values,
841                        arg.choices.as_ref(),
842                        custom_env,
843                    )?;
844                    let parsed = if flag.var || arg.var {
845                        if flag.var {
846                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
847                        }
848                        if arg.var {
849                            validate_flag_arg_fallback_count(
850                                flag,
851                                arg,
852                                values.len(),
853                                &mut out.errors,
854                            );
855                        }
856                        ParseValue::MultiString(values)
857                    } else {
858                        ParseValue::String(values.into_iter().next().unwrap_or_default())
859                    };
860                    out.flags.insert(Arc::clone(flag), parsed);
861                } else {
862                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
863                    out.flags
864                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
865                }
866                out.flag_origins
867                    .entry(Arc::clone(flag))
868                    .or_default()
869                    .push(ValueOrigin::Env(env_name.to_string()));
870            }
871        }
872        // Decide every `default_if` against argv+env only. Binding as we go would put
873        // a default into `out.flags` and make the next flag's condition treat it as
874        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
875        // here, so an unconditional `default` on `--json` must not fire
876        // `default_if "--json"`.
877        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
878        for flag in &flags {
879            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
880                continue;
881            }
882            if let Some(condition) = flag.default_if.iter().find(|condition| {
883                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
884            }) {
885                from_default_if.push((Arc::clone(flag), condition.clone()));
886            }
887        }
888        for (flag, condition) in &from_default_if {
889            // The whole condition, not just the value: several conditions may name the same
890            // selector with different `when` values, so the selector alone does not say
891            // which one fired.
892            bind_flag_fallback(
893                flag,
894                std::slice::from_ref(&condition.value),
895                &mut out,
896                custom_env,
897                ValueOrigin::DefaultIf {
898                    selector: condition.selector.clone(),
899                    when: condition.when.clone(),
900                },
901            )?;
902        }
903        for flag in &flags {
904            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
905                continue;
906            }
907            if !flag.default.is_empty() {
908                bind_flag_fallback(
909                    flag,
910                    &flag.default,
911                    &mut out,
912                    custom_env,
913                    ValueOrigin::Default,
914                )?;
915                continue;
916            }
917            if let Some(arg) = flag.arg.as_ref() {
918                if !arg.default.is_empty() {
919                    bind_flag_fallback(
920                        flag,
921                        &arg.default,
922                        &mut out,
923                        custom_env,
924                        ValueOrigin::Default,
925                    )?;
926                }
927            }
928        }
929        // Declarative value validation is deliberately post-binding. Defaults and
930        // environment fallbacks have landed by here, and delimiters were already split
931        // while binding. Like clap's value parsers, a declaration judges each resulting
932        // raw value independently.
933        for (arg, parsed) in &out.args {
934            validate_expression(
935                &arg.name,
936                arg.validate.as_deref(),
937                arg.validate_error.as_deref(),
938                parsed,
939                &mut out.errors,
940            );
941        }
942        for (flag, parsed) in &out.flags {
943            if let Some(arg) = &flag.arg {
944                validate_expression(
945                    &flag.name,
946                    arg.validate.as_deref(),
947                    arg.validate_error.as_deref(),
948                    parsed,
949                    &mut out.errors,
950                );
951            }
952        }
953        // Applied once, here, because this is where the CLI's own version is known: a
954        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
955        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
956        Ok(out)
957    }
958}
959
960/// Parse command-line arguments according to a spec.
961///
962/// Returns the parsed arguments and flags, with defaults and env vars applied.
963/// Uses `std::env::var` for environment variable lookups.
964///
965/// For custom environment variable handling, use [`Parser`] instead.
966#[must_use = "parsing result should be used"]
967pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
968    Parser::new(spec).parse(input)
969}
970
971/// Parse command-line arguments without applying defaults.
972///
973/// Use this for help text generation or when you need the raw parsed values.
974#[must_use = "parsing result should be used"]
975pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
976    parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
977}
978
979/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
980/// `.exe` stripped so Windows and Unix agree.
981pub fn multicall_basename(argv0: &str) -> &str {
982    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
983    match name.get(name.len().saturating_sub(4)..) {
984        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
985        _ => name,
986    }
987}
988
989/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
990///
991/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
992/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
993pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
994    let base = multicall_basename(argv0);
995    if !name.is_empty() && base == multicall_basename(name) {
996        return None;
997    }
998    if let Some(bin) = bin {
999        if !bin.is_empty() && base == multicall_basename(bin) {
1000            return None;
1001        }
1002    }
1003    Some(base)
1004}
1005
1006/// Internal version of parse_partial that accepts an optional custom env map.
1007/// When a command's own `mount` runs, for the root — which nothing descends into.
1008///
1009/// A completion has to know every command before it can offer one, even with
1010/// nothing typed yet, so it resolves up front. An execution knows the word it was
1011/// given, so it only pays for discovery when that word matches nothing declared —
1012/// and a CLI that declares its commands and mounts a few more does not spawn a
1013/// process on every invocation.
1014#[derive(Clone, Copy, PartialEq, Eq)]
1015enum MountTiming {
1016    Eager,
1017    WhenAWordIsUnknown,
1018}
1019
1020/// One word on its way through the parser, with what the parser has learned about it.
1021///
1022/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1023/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1024/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1025/// the word itself there is nothing to keep aligned. The argv position is here for the
1026/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1027/// removed from the middle, so position in the queue stops meaning position in argv on the
1028/// first descent.
1029struct Token {
1030    word: String,
1031    /// Where in the caller's argv this word came from.
1032    ///
1033    /// A word the parser made up points at the token it was derived from — the tail of a
1034    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1035    /// token a reader would point at, and there is nothing else to point at.
1036    argv: usize,
1037    /// The flag Phase 1 read this word as, and the command level it read it at.
1038    ///
1039    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1040    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1041    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1042    /// then the recognized flags have changed, because each descent drops the parent's
1043    /// non-global flags and a mounted command may declare the same name as a global seen
1044    /// here. Recording the owner keeps a word bound to the flag it was read as.
1045    ///
1046    /// The level matters to strict parsing: clap permits an inherited global once on each
1047    /// side of a subcommand boundary.
1048    binding: Option<(Arc<SpecFlag>, usize)>,
1049}
1050
1051impl Token {
1052    fn new(word: String, argv: usize) -> Self {
1053        Self {
1054            word,
1055            argv,
1056            binding: None,
1057        }
1058    }
1059}
1060
1061/// The token trace, while it is being built.
1062///
1063/// One row per word of the caller's argv, so a role can be recorded against a position
1064/// without the recorder having to know how many words came before it. Words the parser
1065/// made up have no row of their own and fold onto the row they were derived from.
1066struct Trace {
1067    tokens: Vec<TokenBinding>,
1068}
1069
1070impl Trace {
1071    fn new(input: &[String]) -> Self {
1072        Self {
1073            tokens: input
1074                .iter()
1075                .enumerate()
1076                .map(|(index, word)| TokenBinding {
1077                    index,
1078                    word: word.clone(),
1079                    synthesized: false,
1080                    roles: vec![],
1081                })
1082                .collect(),
1083        }
1084    }
1085
1086    fn record(&mut self, argv: usize, role: TokenRole) {
1087        if let Some(token) = self.tokens.get_mut(argv) {
1088            token.roles.push(role);
1089        }
1090    }
1091
1092    /// Note that what was read at this position is not what the caller wrote there.
1093    fn note_synthesized(&mut self, argv: usize) {
1094        if let Some(token) = self.tokens.get_mut(argv) {
1095            token.synthesized = true;
1096        }
1097    }
1098
1099    /// Every word the parse never reached, once it has stopped.
1100    fn close(&mut self, unread: &VecDeque<Token>) {
1101        for token in unread {
1102            self.record(token.argv, TokenRole::Unread);
1103        }
1104    }
1105}
1106
1107fn parse_partial_with_env(
1108    spec: &Spec,
1109    input: &[String],
1110    custom_env: Option<&HashMap<String, String>>,
1111    mount_outputs: Option<&HashMap<String, String>>,
1112    mount_timing: MountTiming,
1113) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1114    let mut trace = Trace::new(input);
1115    parse_partial_traced(
1116        spec,
1117        input,
1118        custom_env,
1119        mount_outputs,
1120        mount_timing,
1121        &mut trace,
1122    )
1123}
1124
1125/// The binding phase, with the trace left somewhere the caller can still read it.
1126///
1127/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1128/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1129/// words read before the failure are most of what a report wants, so the caller owns the
1130/// trace instead and keeps them. See [`Parser::explain_refused`].
1131fn parse_partial_traced(
1132    spec: &Spec,
1133    input: &[String],
1134    custom_env: Option<&HashMap<String, String>>,
1135    mount_outputs: Option<&HashMap<String, String>>,
1136    mount_timing: MountTiming,
1137    trace: &mut Trace,
1138) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1139    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1140        let viewed = spec.for_view(view)?;
1141        return parse_partial_traced(
1142            &viewed,
1143            input,
1144            custom_env,
1145            mount_outputs,
1146            mount_timing,
1147            trace,
1148        );
1149    }
1150    trace!("parse_partial: {input:?}");
1151    let mut input = input
1152        .iter()
1153        .enumerate()
1154        .map(|(argv, word)| Token::new(word.clone(), argv))
1155        .collect::<VecDeque<_>>();
1156    let argv0 = input.pop_front();
1157    if let Some(argv0) = argv0.as_ref() {
1158        trace.record(argv0.argv, TokenRole::Program);
1159    }
1160    if spec.multicall {
1161        if let Some(raw) = argv0 {
1162            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1163                // A symlink invocation reads a word the caller never typed — the basename of
1164                // the program itself — so argv[0] is both the program and, below, whatever
1165                // that word selects.
1166                trace.note_synthesized(raw.argv);
1167                input.push_front(Token::new(applet.to_string(), raw.argv));
1168            }
1169        }
1170    }
1171    // The policy observes the selected command's own argv, not values eventually filled from
1172    // env/default. Start at the root, then reset on every explicit descent. A default
1173    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1174    let mut command_has_argv = !input.is_empty();
1175
1176    let mut out = ParseOutput {
1177        cmd: spec.cmd.clone(),
1178        cmds: vec![spec.cmd.clone()],
1179        args: IndexMap::new(),
1180        flags: IndexMap::new(),
1181        tokens: vec![],
1182        flag_origins: IndexMap::new(),
1183        arg_origins: IndexMap::new(),
1184        overridden_flags: BTreeMap::new(),
1185        available_flags: gather_flags(&spec.cmd),
1186        flag_awaiting_value: vec![],
1187        errors: vec![],
1188        warnings: vec![],
1189        next_arg: None,
1190        double_dash_seen: false,
1191        external: None,
1192    };
1193    // Keep this internal so adding relationship support remains semver-compatible. The full
1194    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1195    let mut overridden_flags = HashSet::new();
1196    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1197    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1198    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1199    // or an inherited `-c` belonged to the ancestor.
1200    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1201
1202    // Phase 1: Scan for subcommands and collect global flags
1203    //
1204    // This phase identifies subcommands early because they may have mount points
1205    // that need to be executed with the global flags that appeared before them.
1206    //
1207    // Example: "usage --verbose run task"
1208    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1209    //   -> then finds "task" as a subcommand of "run" (if it exists)
1210    //
1211    // We only collect global flags for mounts because:
1212    // - Non-global flags are specific to the current command, not subcommands
1213    // - Global flags affect all commands and should be passed to mount points
1214    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1215    // Which flag each word skipped here belongs to is recorded on the word — see
1216    // `Token::binding`.
1217    let mut command_arg_found = false;
1218    let mut variadic_flag_active = false;
1219    let mut idx = 0;
1220    // Track whether we've already applied the default_subcommand to prevent
1221    // multiple switches (e.g., if default is "run" and there's a task named "run")
1222    let mut used_default_subcommand = false;
1223    // Whether the command in scope has had its own mounts run. A mount on the root
1224    // is the case that needs this: a subcommand's mounts are run when the parser
1225    // descends into it, but nothing descends into the root.
1226    let mut mounts_resolved = false;
1227    // A completion needs the whole command list before it can offer anything, and
1228    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1229    // mean a root mount never contributed to the very thing it exists for.
1230    //
1231    // The default-subcommand gate applies here too, and has to: offering a discovered
1232    // command that a real parse would hand to the default instead would be worse than
1233    // not offering it. A root mount under a `default_subcommand` that does not say
1234    // `overrides_default` therefore contributes nothing anywhere, which is what
1235    // "the default outranks discovery" means.
1236    let default_outranks_mounts =
1237        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1238    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1239    {
1240        mounts_resolved = true;
1241        let mut mounted = out.cmd.clone();
1242        mounted.mount(&[], mount_outputs)?;
1243        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1244        if let Some(last) = out.cmds.last_mut() {
1245            *last = mounted.clone();
1246        }
1247        out.cmd = mounted;
1248    }
1249
1250    while idx < input.len() {
1251        // Only for a word that could name a command, and only when it matches
1252        // nothing already declared. A CLI that declares its commands and mounts more
1253        // does not spawn a process for every invocation, and a flag — `--help`, or
1254        // anything unrecognized — never triggers discovery at all, which it would
1255        // otherwise do simply by not being a subcommand.
1256        // A declared `default_subcommand` already says what an unmatched word means,
1257        // and it costs nothing — so discovery waits behind it unless a mount asks to
1258        // outrank it. Without this, a task runner would spawn its discovery process
1259        // once per task invocation.
1260        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1261            default_accepts_word(&out.cmd, name, &input[idx].word)
1262                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1263        });
1264        if !mounts_resolved
1265            && !out.cmd.mounts.is_empty()
1266            && !default_catches_it
1267            && is_command_word(&input[idx].word)
1268            && !is_negative_number(&input[idx].word)
1269            && out.cmd.find_subcommand(&input[idx].word).is_none()
1270        {
1271            mounts_resolved = true;
1272            let mut mounted = out.cmd.clone();
1273            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1274            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1275            if let Some(last) = out.cmds.last_mut() {
1276                *last = mounted.clone();
1277            }
1278            out.cmd = mounted;
1279        }
1280        if variadic_flag_active
1281            && out.cmd.find_subcommand(&input[idx].word).is_some()
1282            && !out.cmd.subcommand_precedence_over_arg
1283        {
1284            break;
1285        }
1286        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1287            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1288                bail!(
1289                    "subcommand '{}' cannot be used with arguments on its parent command",
1290                    input[idx].word
1291                );
1292            }
1293            let mut subcommand = subcommand.clone();
1294            // Pass prefix words (global flags before this subcommand) to mount
1295            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1296            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1297            // commands are ordinary commands relative to each other.
1298            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1299            merge_subcommand_flags(
1300                &mut out.available_flags,
1301                gather_flags(&subcommand),
1302                crossing_mount,
1303            );
1304            // Remove subcommand from input
1305            let selected = input.remove(idx);
1306            if let Some(selected) = selected {
1307                trace.record(
1308                    selected.argv,
1309                    TokenRole::Command {
1310                        name: subcommand.name.clone(),
1311                    },
1312                );
1313            }
1314            command_has_argv = idx < input.len();
1315            out.cmds.push(subcommand.clone());
1316            out.cmd = subcommand.clone();
1317            // A descent already ran the new command's mounts, above.
1318            mounts_resolved = true;
1319            prefix_flags.clear();
1320            command_arg_found = false;
1321            variadic_flag_active = false;
1322            // Continue from current position (don't reset to 0)
1323            // After remove(), idx now points to the next element
1324        } else if !is_command_word(&input[idx].word)
1325            || declared_numeric_short(&out.available_flags, &input[idx].word)
1326        {
1327            // Check if this is a known flag
1328            let word = input[idx].word.clone();
1329            let flag_key = get_flag_key(&word);
1330
1331            // A short token keys on its first letter, so `-az` would be recorded as
1332            // `-a` and its tail left over. Check the whole token here, where it is
1333            // first read: a token containing an unrecognized letter is not a bundle,
1334            // and recording it as one is what let `-a` be applied from a token that
1335            // never named it.
1336            let is_bundle = word.starts_with("--")
1337                || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1338            if let Some(f) = out
1339                .available_flags
1340                .get(flag_key)
1341                .cloned()
1342                .filter(|_| is_bundle)
1343            {
1344                command_arg_found = true;
1345                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1346                // Skip the flag and keep scanning. Both global and non-global flags may precede
1347                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1348                // stopping at one would hide the subcommand — and any mount on it — from the
1349                // parse, leaving the subcommand name to be mis-read as a positional argument.
1350                //
1351                // Only globals are forwarded to mounts: a non-global flag belongs to the
1352                // command that declared it, not to what is mounted below it.
1353                input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1354                let mut forwarded = f.global.then(|| vec![word.clone()]);
1355                idx += 1;
1356
1357                // Only consume next word if flag takes an argument AND value isn't embedded
1358                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1359                if f.arg.is_some()
1360                    && !word.contains('=')
1361                    && idx < input.len()
1362                    && (!is_flag_like(&input[idx].word)
1363                        || (f.arg.as_ref().is_some_and(|arg| arg.allow_negative_numbers)
1364                            && is_negative_number(&input[idx].word)))
1365                {
1366                    if let Some(words) = forwarded.as_mut() {
1367                        words.push(input[idx].word.clone());
1368                    }
1369                    idx += 1;
1370                }
1371                if let Some(words) = forwarded {
1372                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1373                    prefix_flags.push((f, words));
1374                }
1375            } else {
1376                // Unknown flag - stop looking for subcommands
1377                // Let the main parsing phase handle the error
1378                break;
1379            }
1380        } else {
1381            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1382                idx += 1;
1383                continue;
1384            }
1385            // Found a word that's not a flag or subcommand
1386            // Check if we should use the default_subcommand (only once, and only at the
1387            // root, which is the only place a spec can declare one — `out.cmds` holds just
1388            // the root until something descends). Without that second condition the one
1389            // declared name is looked up wherever the parser happens to be standing, so an
1390            // unrelated command acquires a default because a name matched one level down:
1391            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1392            // `config ls`.
1393            if !used_default_subcommand && out.cmds.len() == 1 {
1394                if let Some(default_name) = &spec.default_subcommand {
1395                    if let Some(subcommand) = out
1396                        .cmd
1397                        .find_subcommand(default_name)
1398                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1399                    {
1400                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1401                            bail!(
1402                                "subcommand '{}' cannot be used with arguments on its parent command",
1403                                subcommand.name
1404                            );
1405                        }
1406                        let mut subcommand = subcommand.clone();
1407                        // Pass prefix words (global flags before this) to mount
1408                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1409                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1410                        merge_subcommand_flags(
1411                            &mut out.available_flags,
1412                            gather_flags(&subcommand),
1413                            crossing_mount,
1414                        );
1415                        out.cmds.push(subcommand.clone());
1416                        out.cmd = subcommand.clone();
1417                        command_has_argv = true;
1418                        prefix_flags.clear();
1419                        command_arg_found = false;
1420                        variadic_flag_active = false;
1421                        // This descent ran the new command's mounts, so lazy
1422                        // discovery must not run them a second time.
1423                        mounts_resolved = true;
1424                        used_default_subcommand = true;
1425                        // Continue the loop to check if this word is a subcommand of the
1426                        // default subcommand (e.g., a task name added via mount).
1427                        // If it's not a subcommand, the next iteration will break and
1428                        // Phase 2 will handle it as a positional arg.
1429                        continue;
1430                    }
1431                }
1432            }
1433            // An unmatched word that names no subcommand is forwarded as an external
1434            // command: this word, then every token after it, including flags. Known
1435            // subcommands already won above, and a default_subcommand already caught.
1436            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1437            if out.cmd.external_subcommand {
1438                let rest: Vec<Token> = input.drain(idx..).collect();
1439                for token in &rest {
1440                    trace.record(token.argv, TokenRole::External);
1441                }
1442                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1443                break;
1444            }
1445            // This could be a positional argument, so stop subcommand search
1446            break;
1447        }
1448    }
1449
1450    // Phase 2: Main argument and flag parsing
1451    //
1452    // Now that we've identified all subcommands and executed their mounts,
1453    // we can parse the remaining arguments, flags, and their values.
1454
1455    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
1456    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
1457    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1458    // argument filled?" has to consult `out.args` by key instead of counting.
1459    let mut next_arg_idx: usize = 0;
1460    let mut enable_flags = true;
1461    let mut grouped_flag = false;
1462    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1463    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1464    // words that come after it — see `report_double_dash_violation`.
1465    let mut seen_double_dash = false;
1466    // Args already reported as having been offered a word before the `--` they require, so a
1467    // variadic one does not report the same violation for every word it is offered.
1468    let mut double_dash_violations: HashSet<String> = HashSet::new();
1469    // Scalar occurrences are scoped to the command level where they were written. Inherited
1470    // globals may therefore appear once before and once after a subcommand under clap's strict
1471    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1472    // opposite forms may override one another, while repeating either spelling is an error.
1473    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1474
1475    while !input.is_empty() {
1476        let token = input.pop_front().unwrap();
1477        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1478        let binding = token.binding;
1479        let argv = token.argv;
1480        let mut w = token.word;
1481        // A short's attached value is re-queued with `grouped_flag` set, and that
1482        // continuation is not a following word. `require_equals` refuses only the
1483        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1484        // only when the value is actually missing, so `-cnever` is still `never`.
1485        let attached_continuation = grouped_flag;
1486
1487        // Check for restart_token - resets argument parsing for multiple command invocations
1488        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1489        if let Some(ref restart_token) = out.cmd.restart_token {
1490            if w == *restart_token {
1491                // Reset argument parsing state for a fresh command invocation, keeping the
1492                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1493                // is not cleared here either, so clearing it would let one arg report the same
1494                // violation once per invocation.
1495                out.args.clear();
1496                // With the values gone, so is where they came from — otherwise the second
1497                // invocation of `run lint ::: test` reports the first one's provenance. The
1498                // token trace is *not* cleared: those words were read, and a report that
1499                // dropped them would show a command line with a hole in it.
1500                out.arg_origins.clear();
1501                trace.record(argv, TokenRole::Restart);
1502                next_arg_idx = 0;
1503                out.flag_awaiting_value.clear(); // Clear any pending flag values
1504                enable_flags = true; // Reset -- separator effect
1505                seen_double_dash = false; // The next invocation needs its own `--`
1506                continue;
1507            }
1508        }
1509
1510        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1511        // like, and that has to be asked before the separator arm below rather than
1512        // after it. Asked after, a `--` was consumed as a separator while the flag
1513        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1514        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1515        // itself, which is what clap does with the same declaration — and no flag can
1516        // still be waiting once the separator has done its job, so the starvation rule
1517        // below has no path around it.
1518        if enable_flags
1519            && w.starts_with('-')
1520            && out.flag_awaiting_value.last().is_some_and(|flag| {
1521                !flag.require_equals
1522                    && (flag.allow_hyphen_values()
1523                        || (flag
1524                            .arg
1525                            .as_ref()
1526                            .is_some_and(|arg| arg.allow_negative_numbers)
1527                            && is_negative_number(&w)))
1528            })
1529        {
1530            // A variadic argument collects here too: which token supplied its first
1531            // value says nothing about how many it takes.
1532            let should_return = bind_pending_flag_value(
1533                spec,
1534                &out.cmd,
1535                &mut out.errors,
1536                &mut out.flags,
1537                &mut out.flag_awaiting_value,
1538                &mut w,
1539                &mut input,
1540                custom_env,
1541                trace,
1542                argv,
1543                // The token a hyphen-valued flag takes is the following word, never attached.
1544                false,
1545            )?;
1546            if should_return {
1547                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1548                return Ok((out, overridden_flags));
1549            }
1550            continue;
1551        }
1552
1553        // A flag whose value may be omitted that cannot take this token as a detached
1554        // value finishes bare and leaves the token for whatever comes next:
1555        // `--color --verbose` colours with the missing value and still sets verbose,
1556        // and `--inspect 9229` with `require_equals` binds the missing value rather
1557        // than treating 9229 as the port.
1558        if enable_flags
1559            && !attached_continuation
1560            && !out.flag_awaiting_value.is_empty()
1561            && out.flag_awaiting_value.last().is_some_and(|flag| {
1562                (flag.default_missing.is_some() || flag.value_optional)
1563                    && (flag.require_equals
1564                        || (is_flag_like(&w)
1565                            && !flag.allow_hyphen_values()
1566                            && !(flag
1567                                .arg
1568                                .as_ref()
1569                                .is_some_and(|arg| arg.allow_negative_numbers)
1570                                && is_negative_number(&w))))
1571            })
1572        {
1573            try_bind_default_missing(
1574                &mut out.flags,
1575                &mut out.flag_awaiting_value,
1576                custom_env,
1577                &mut out.flag_origins,
1578            )?;
1579        }
1580
1581        // The first explicit `--` is still a separator after an `automatic` argument has
1582        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1583        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1584        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1585        // behavior as the bug it is.
1586        if w == "--" && !seen_double_dash {
1587            enable_flags = false;
1588
1589            // Only preserve the double dash token if we're collecting values for a variadic arg
1590            // in double_dash == `preserve` mode
1591            let should_preserve = out
1592                .cmd
1593                .args
1594                .get(next_arg_idx)
1595                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1596                .unwrap_or(false);
1597
1598            if should_preserve {
1599                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1600                // neither counts as one nor unlocks a `double_dash="required"` arg.
1601            } else {
1602                seen_double_dash = true;
1603                trace.record(argv, TokenRole::Separator);
1604
1605                // Everything after an explicit `--` belongs to the arg that requires one, so
1606                // jump the cursor there — past any earlier arg, including a greedy variadic
1607                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1608                // which is what `double_dash="required"` is generated from. Specs without such
1609                // an arg find nothing and keep the cursor where it was.
1610                let target = out.cmd.args.iter().position(|arg| {
1611                    arg.double_dash == SpecDoubleDashChoices::Required
1612                        && !out.args.contains_key(arg)
1613                });
1614                if let Some(target) = target {
1615                    // Forward only. An unfilled required arg declared *before* the cursor is
1616                    // left where it is rather than rewound to — words already assigned to
1617                    // later args would have to be taken back for that to mean anything, and
1618                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1619                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1620                    // one ahead of others is already outside what this models.
1621                    if target > next_arg_idx {
1622                        next_arg_idx = target;
1623                    }
1624                }
1625                continue;
1626            }
1627        }
1628
1629        // long flags
1630        if enable_flags && w.starts_with("--") {
1631            grouped_flag = false;
1632            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1633            // an empty value while `--jobs` supplies none. Collapsing the two lost
1634            // the flag entirely.
1635            let split = w.split_once('=');
1636            let word = split.map(|(word, _)| word).unwrap_or(&w);
1637            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1638            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1639                let command_level = binding
1640                    .as_ref()
1641                    .map(|(_, level)| *level)
1642                    .unwrap_or(out.cmds.len() - 1);
1643                parsed_flag_spellings
1644                    .entry(Arc::as_ptr(f) as usize)
1645                    .or_default()
1646                    .insert(word.to_string());
1647                // Recorded before the action check below: a token that named a flag named it
1648                // whether or not the parse can carry on afterwards.
1649                trace.record(
1650                    argv,
1651                    TokenRole::Flag {
1652                        flag: Arc::clone(f),
1653                        spelling: word.to_string(),
1654                        negated: f.negate.as_deref() == Some(word),
1655                    },
1656                );
1657                if f.action != crate::SpecFlagAction::Set {
1658                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1659                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1660                    return Ok((out, overridden_flags));
1661                }
1662                apply_flag_overrides(
1663                    f,
1664                    &out.available_flags,
1665                    &mut out.flags,
1666                    &mut out.flag_awaiting_value,
1667                    &mut overridden_flags,
1668                    &mut out.overridden_flags,
1669                );
1670                // An attached value only means something to a flag that takes one:
1671                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1672                // give a flag that holds no value. Handing that leftover to the
1673                // positionals would re-split one token into two, so `ex --force=yes`
1674                // would fill an argument the caller never typed a word for.
1675                if f.arg.is_some() {
1676                    record_scalar_flag_occurrence(
1677                        &out.cmds,
1678                        f,
1679                        command_level,
1680                        None,
1681                        &mut scalar_occurrences,
1682                        &mut out.errors,
1683                    );
1684                    let f = Arc::clone(f);
1685                    out.flag_awaiting_value.push(Arc::clone(&f));
1686                    // The `=` has already settled that this text is the value, so it
1687                    // binds here rather than going back on the queue to be read as a
1688                    // token again — where `--jobs=--force` looked like a flag of its
1689                    // own and bound `force`, leaving `jobs` unset.
1690                    if let Some((_, val)) = split {
1691                        // The `=` settles where the *first* value came from and nothing
1692                        // more, so a variadic argument goes on collecting from the words
1693                        // after it exactly as the detached form does.
1694                        let mut val = val.to_string();
1695                        let should_return = bind_pending_flag_value(
1696                            spec,
1697                            &out.cmd,
1698                            &mut out.errors,
1699                            &mut out.flags,
1700                            &mut out.flag_awaiting_value,
1701                            &mut val,
1702                            &mut input,
1703                            custom_env,
1704                            trace,
1705                            argv,
1706                            // The `=` settled that this text is the value, so it rode in on
1707                            // the flag's own token.
1708                            true,
1709                        )?;
1710                        if should_return {
1711                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1712                            return Ok((out, overridden_flags));
1713                        }
1714                    }
1715                } else if f.count {
1716                    let arr = out
1717                        .flags
1718                        .entry(Arc::clone(f))
1719                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1720                        .try_as_multi_bool_mut()
1721                        .unwrap();
1722                    arr.push(true);
1723                } else {
1724                    let negate = f.negate.clone().unwrap_or_default();
1725                    let negated_form = word == negate;
1726                    let value = if f.bool_value {
1727                        match split.map(|(_, value)| value) {
1728                            Some("true") => !negated_form,
1729                            Some("false") => negated_form,
1730                            Some(value) => {
1731                                out.errors.push(UsageErr::InvalidValue {
1732                                    name: f.name.clone(),
1733                                    value: value.to_string(),
1734                                    reason: "expected `true` or `false`".to_string(),
1735                                });
1736                                continue;
1737                            }
1738                            None => !negated_form,
1739                        }
1740                    } else {
1741                        !negated_form
1742                    };
1743                    // Which form was typed is a question about the name, so it is
1744                    // asked of `word` rather than the whole token: the attached value
1745                    // is dropped just above, and comparing `--no-color=yes` against
1746                    // `--no-color` would take the negation down with it.
1747                    record_scalar_flag_occurrence(
1748                        &out.cmds,
1749                        f,
1750                        command_level,
1751                        Some(!negated_form),
1752                        &mut scalar_occurrences,
1753                        &mut out.errors,
1754                    );
1755                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1756                }
1757                continue;
1758            }
1759            if is_help_arg(spec, &out.cmd, &w) {
1760                out.errors
1761                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1762                trace.record(
1763                    argv,
1764                    TokenRole::Builtin {
1765                        spelling: w.clone(),
1766                    },
1767                );
1768                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1769                return Ok((out, overridden_flags));
1770            }
1771            if is_version_arg(spec, &out.cmds, &w) {
1772                out.errors.push(render_version_err(spec, w.len() > 2));
1773                trace.record(
1774                    argv,
1775                    TokenRole::Builtin {
1776                        spelling: w.clone(),
1777                    },
1778                );
1779                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1780                return Ok((out, overridden_flags));
1781            }
1782            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1783                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1784                trace.close(&input);
1785                return Err(refused.into());
1786            }
1787        }
1788
1789        // short flags
1790        //
1791        // A fresh token is checked whole before any of it is applied: `-az` with only
1792        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
1793        // discovering that `z` names nothing. A grouped continuation is exempt — its
1794        // token was already checked when it arrived.
1795        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1796        let positional_negative_number = !declared_numeric_short
1797            && is_negative_number(&w)
1798            && out
1799                .cmd
1800                .args
1801                .get(next_arg_idx)
1802                .is_some_and(|arg| arg.allow_negative_numbers);
1803        if enable_flags
1804            && !grouped_flag
1805            // A word phase 1 already resolved to a flag needs no re-checking, and
1806            // the flags in scope have changed since, so re-checking would be wrong.
1807            && binding.is_none()
1808            && w.starts_with('-')
1809            && w.len() > 1
1810            && is_flag_like(&w)
1811            && !positional_negative_number
1812            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1813        {
1814            // Refused if this command asked for that; otherwise it carries on below
1815            // as one word, with none of its letters applied.
1816            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1817                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1818                trace.close(&input);
1819                return Err(refused.into());
1820            }
1821        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1822            let short = w.chars().nth(1).unwrap();
1823            if let Some(f) = binding
1824                .as_ref()
1825                .map(|(flag, _)| flag)
1826                .or_else(|| out.available_flags.get(&format!("-{short}")))
1827            {
1828                let command_level = binding
1829                    .as_ref()
1830                    .map(|(_, level)| *level)
1831                    .unwrap_or(out.cmds.len() - 1);
1832                if f.action != crate::SpecFlagAction::Set {
1833                    out.errors
1834                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1835                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1836                    return Ok((out, overridden_flags));
1837                }
1838                parsed_flag_spellings
1839                    .entry(Arc::as_ptr(f) as usize)
1840                    .or_default()
1841                    .insert(format!("-{short}"));
1842                trace.record(
1843                    argv,
1844                    TokenRole::Flag {
1845                        flag: Arc::clone(f),
1846                        spelling: format!("-{short}"),
1847                        // A short spelling is never the negated form: `negate` is a long.
1848                        negated: false,
1849                    },
1850                );
1851                apply_flag_overrides(
1852                    f,
1853                    &out.available_flags,
1854                    &mut out.flags,
1855                    &mut out.flag_awaiting_value,
1856                    &mut overridden_flags,
1857                    &mut out.overridden_flags,
1858                );
1859                let rest = &w[1 + short.len_utf8()..];
1860                if !rest.is_empty() {
1861                    // `-abc` is one token that names three flags, so the tail is read at the
1862                    // bundle's own position rather than at one of its own.
1863                    input.push_front(Token::new(format!("-{rest}"), argv));
1864                }
1865                // A fully consumed short is no longer a grouped continuation.
1866                // Leaving this set after `-ai` made `-i` skip `require_equals`
1867                // and bind the following word.
1868                grouped_flag = !rest.is_empty();
1869                if f.arg.is_some() {
1870                    record_scalar_flag_occurrence(
1871                        &out.cmds,
1872                        f,
1873                        command_level,
1874                        None,
1875                        &mut scalar_occurrences,
1876                        &mut out.errors,
1877                    );
1878                    out.flag_awaiting_value.push(Arc::clone(f));
1879                } else if f.count {
1880                    let arr = out
1881                        .flags
1882                        .entry(Arc::clone(f))
1883                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1884                        .try_as_multi_bool_mut()
1885                        .unwrap();
1886                    arr.push(true);
1887                } else {
1888                    let negate = f.negate.clone().unwrap_or_default();
1889                    let value = w != negate;
1890                    record_scalar_flag_occurrence(
1891                        &out.cmds,
1892                        f,
1893                        command_level,
1894                        Some(value),
1895                        &mut scalar_occurrences,
1896                        &mut out.errors,
1897                    );
1898                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1899                }
1900                continue;
1901            }
1902            // The letter nothing declared may still be one the parser supplies, and it may
1903            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
1904            // neither reaches the whole-token spellings below.
1905            if let Some(err) = supplied_short(spec, &out.cmds, short) {
1906                out.errors.push(err);
1907                trace.record(
1908                    argv,
1909                    TokenRole::Builtin {
1910                        spelling: format!("-{short}"),
1911                    },
1912                );
1913                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1914                return Ok((out, overridden_flags));
1915            }
1916            if is_help_arg(spec, &out.cmd, &w) {
1917                out.errors
1918                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1919                trace.record(
1920                    argv,
1921                    TokenRole::Builtin {
1922                        spelling: w.clone(),
1923                    },
1924                );
1925                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1926                return Ok((out, overridden_flags));
1927            }
1928            if is_version_arg(spec, &out.cmds, &w) {
1929                out.errors.push(render_version_err(spec, w.len() > 2));
1930                trace.record(
1931                    argv,
1932                    TokenRole::Builtin {
1933                        spelling: w.clone(),
1934                    },
1935                );
1936                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1937                return Ok((out, overridden_flags));
1938            }
1939            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1940                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1941                trace.close(&input);
1942                return Err(refused.into());
1943            }
1944            if grouped_flag {
1945                grouped_flag = false;
1946                w.remove(0);
1947                // What is left is a short flag's attached value, and one `=` between
1948                // the letter and the value is a separator: `-j=8` means 8. Only one,
1949                // so `-j==8` still means `=8`.
1950                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
1951                    w.remove(0);
1952                }
1953            }
1954        }
1955
1956        // Only while flags are still being read. A flag still waiting when the separator
1957        // was consumed is starved: its value would have to come from after the `--`,
1958        // where every token is data. Draining there gave `ex --jobs -- x` the word after
1959        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
1960        // was gone. Left waiting, it is reported as the missing value it is.
1961        // `require_equals` refuses a detached value: `--flag value` is a missing
1962        // value, not a flag of `"value"`. The attached form is still bound above.
1963        // Reported here rather than left waiting until the end of the line: falling
1964        // through would offer `value` to the positionals and call it an unexpected
1965        // word, which is the wrong error and a different one from usage-argv.
1966        if enable_flags
1967            && !attached_continuation
1968            && !out.flag_awaiting_value.is_empty()
1969            && out
1970                .flag_awaiting_value
1971                .last()
1972                .is_some_and(|flag| flag.require_equals)
1973        {
1974            let flag = out.flag_awaiting_value.last().unwrap();
1975            let token = flag
1976                .long
1977                .first()
1978                .map(|l| format!("--{l}"))
1979                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
1980                .unwrap_or_else(|| flag.name.clone());
1981            out.errors.push(UsageErr::InvalidFlag {
1982                token: token.clone(),
1983                reason: "requires an argument".to_string(),
1984                span: (0, 0).into(),
1985                input: format!("{token} {w}"),
1986            });
1987            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1988            return Ok((out, overridden_flags));
1989        }
1990        if enable_flags && !out.flag_awaiting_value.is_empty() {
1991            // Held before the drain pops it: a flag whose argument is variadic keeps
1992            // taking values after this first one.
1993            let should_return = bind_pending_flag_value(
1994                spec,
1995                &out.cmd,
1996                &mut out.errors,
1997                &mut out.flags,
1998                &mut out.flag_awaiting_value,
1999                &mut w,
2000                &mut input,
2001                custom_env,
2002                trace,
2003                argv,
2004                attached_continuation,
2005            )?;
2006            if should_return {
2007                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2008                return Ok((out, overridden_flags));
2009            }
2010            continue;
2011        }
2012
2013        if out.cmd.allow_missing_positional {
2014            while let Some(current) = out.cmd.args.get(next_arg_idx) {
2015                if current.required || out.args.contains_key(current) {
2016                    break;
2017                }
2018                let required_after = out.cmd.args[next_arg_idx + 1..]
2019                    .iter()
2020                    .filter(|arg| arg.required)
2021                    .count();
2022                if required_after == 0 {
2023                    break;
2024                }
2025                let remaining_values = 1 + input
2026                    .iter()
2027                    .filter(|token| !enable_flags || !is_flag_like(&token.word))
2028                    .count();
2029                if remaining_values > required_after {
2030                    break;
2031                }
2032                next_arg_idx += 1;
2033            }
2034        }
2035
2036        if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2037            if arg.var
2038                && out.args.contains_key(arg)
2039                && arg.value_terminator.as_deref() == Some(w.as_str())
2040            {
2041                trace.record(
2042                    argv,
2043                    TokenRole::ValueTerminator {
2044                        ends: arg.name.clone(),
2045                    },
2046                );
2047                next_arg_idx += 1;
2048                continue;
2049            }
2050            // Before anything else: an arg that requires `--` accepts nothing until one has been
2051            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2052            // reported as an invalid choice, and from reaching that function's help escape.
2053            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2054                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2055                trace.record(
2056                    argv,
2057                    TokenRole::Refused {
2058                        reason: format!("{} only accepts words after `--`", arg.name),
2059                    },
2060                );
2061                // Drop the word without filling the arg or advancing the cursor: every later
2062                // word hits the same arg and is rejected the same way, so the parse still ends
2063                // in an error rather than in `unexpected word`.
2064                continue;
2065            }
2066            // Split before judging, as the flag path does: after the split the word is
2067            // no longer one value, and `choices` has to be asked about each. Judging
2068            // first rejects `src:docs` against a list that both halves are on, and
2069            // names the whole word rather than the half that was wrong.
2070            let trailing_value =
2071                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2072            let suppress_trailing_delimiter =
2073                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2074            let delimiter = if suppress_trailing_delimiter && trailing_value {
2075                None
2076            } else {
2077                arg.delimiter
2078            };
2079            let parts: Vec<String> = match delimiter {
2080                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2081                None => vec![w.clone()],
2082            };
2083            let mut refused = false;
2084            for part in &parts {
2085                if validate_choices(
2086                    spec,
2087                    &out.cmd,
2088                    &mut out.errors,
2089                    ChoiceTarget::arg(arg),
2090                    part,
2091                    arg.choices.as_ref(),
2092                    custom_env,
2093                )? {
2094                    refused = true;
2095                    break;
2096                }
2097            }
2098            if refused {
2099                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2100                return Ok((out, overridden_flags));
2101            }
2102            // `double_dash="automatic"` means the first value this arg takes is the last
2103            // token read as anything but data: a wrapper declaring it can forward flags
2104            // without its caller typing a `--`. Set before the value is stored, so the
2105            // rest of the command line is already past flag parsing.
2106            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2107                enable_flags = false;
2108            }
2109            // A flag-like word reaching a positional while flags are still being read was
2110            // offered to every declaration and matched none: under the default
2111            // `unknown_flags="value"` it becomes data, and saying so is the difference
2112            // between "you have a typo" and "this argument took your typo".
2113            let unknown_flag = enable_flags
2114                && !positional_negative_number
2115                && is_flag_like(&w)
2116                && binding.is_none();
2117            trace.record(
2118                argv,
2119                if unknown_flag {
2120                    TokenRole::UnknownFlag {
2121                        bound_as: Some(Arc::new(arg.clone())),
2122                    }
2123                } else {
2124                    TokenRole::Arg {
2125                        arg: Arc::new(arg.clone()),
2126                        values: parts.clone(),
2127                    }
2128                },
2129            );
2130            if arg.var {
2131                let arr = out
2132                    .args
2133                    .entry(Arc::new(arg.clone()))
2134                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2135                    .try_as_multi_string_mut()
2136                    .unwrap();
2137                // The values this word carried, split above so that everything
2138                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2139                // counts the values the user meant rather than the words they typed.
2140                arr.extend(parts.iter().cloned());
2141                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2142                    next_arg_idx += 1;
2143                }
2144            } else {
2145                out.args
2146                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2147                next_arg_idx += 1;
2148            }
2149            continue;
2150        }
2151        if is_help_arg(spec, &out.cmd, &w) {
2152            out.errors
2153                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2154            trace.record(
2155                argv,
2156                TokenRole::Builtin {
2157                    spelling: w.clone(),
2158                },
2159            );
2160            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2161            return Ok((out, overridden_flags));
2162        }
2163        if is_version_arg(spec, &out.cmds, &w) {
2164            out.errors.push(render_version_err(spec, w.len() > 2));
2165            trace.record(
2166                argv,
2167                TokenRole::Builtin {
2168                    spelling: w.clone(),
2169                },
2170            );
2171            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2172            return Ok((out, overridden_flags));
2173        }
2174        trace.record(
2175            argv,
2176            TokenRole::Refused {
2177                reason: "no declaration takes this word".to_string(),
2178            },
2179        );
2180        trace.close(&input);
2181        bail!("unexpected word: {w}");
2182    }
2183
2184    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2185
2186    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2187    // declarations with the same canonical name therefore share one public value entry even
2188    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2189    // both, which is what exclusivity needs.
2190    let flag_was_parsed =
2191        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2192
2193    // The spellings the selected command's own declaration speaks for, on this object.
2194    //
2195    // Empty unless that declaration really is this object's: a parent and child may each
2196    // declare `--clean` without merging, leaving two flags that share a name, and the
2197    // ancestor's must not be read as the child's. The test is whether every spelling the child
2198    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2199    // an ancestor whose long form the child took over.
2200    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2201        let declared: HashSet<String> = out
2202            .cmd
2203            .flags
2204            .iter()
2205            .filter(|declared| declared.name == flag.name)
2206            .flat_map(flag_keys)
2207            .collect();
2208        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2209        // some other inherited global already owns — `-c --clean` beside an inherited
2210        // `-c --config` — and that collision is resolved in the other global's favor, so the
2211        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2212        // unrelated collision disown the child from the `--clean` it plainly does own.
2213        //
2214        // Still enough to tell the two-object case apart, which is what this guards: when a
2215        // child re-declares a global as global, the child's own spellings resolve to the
2216        // child's separate flag, so none of them lands on the ancestor's.
2217        let speaks_for_this_flag = declared.iter().any(|spelling| {
2218            out.available_flags
2219                .get(spelling)
2220                .is_some_and(|available| Arc::ptr_eq(available, flag))
2221        });
2222        if speaks_for_this_flag {
2223            declared
2224        } else {
2225            HashSet::new()
2226        }
2227    };
2228
2229    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2230    //
2231    // A child that re-declares an inherited global merges into one object answering to two
2232    // alias sets whose declarations may disagree, so there is no single owner to name: the
2233    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2234    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2235    // child's in one invocation — and each carries its own declaration's answer.
2236    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2237        let child = child_spellings(flag);
2238        let child_exclusive = !child.is_empty()
2239            && out
2240                .cmd
2241                .flags
2242                .iter()
2243                .any(|declared| declared.name == flag.name && declared.exclusive);
2244        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2245            Some(spellings) => (
2246                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2247                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2248            ),
2249            // An environment value has no spelling to attribute it by. The declaration the
2250            // selected command has in scope is the one that answers — which is the child's
2251            // when it re-declared the flag, and the ancestor's when it did not.
2252            None => (child_exclusive, flag.exclusive && child.is_empty()),
2253        }
2254    };
2255
2256    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2257        let (child, ancestor) = exclusivity_in_play(flag);
2258        child || ancestor
2259    };
2260
2261    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2262    // command that otherwise needs an input. Companions are still diagnosed below, but an
2263    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2264    // whether it was alone or not.
2265    let exclusive_present =
2266        unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2267            exclusive_occurrence(flag)
2268                && !overridden_flags.contains(&flag.name)
2269                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2270        });
2271    let requirements_apply = |command_index: usize| {
2272        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2273    };
2274
2275    if out.cmd.arg_required_else_help && !command_has_argv {
2276        out.errors.push(render_help_err(spec, &out.cmd, false));
2277    }
2278
2279    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2280    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2281    // the child would be here instead. The spec has carried `subcommand_required` since it was
2282    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2283    // complete invocation while usage-argv and clap both refused it.
2284    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2285        let mut names: Vec<&str> = out
2286            .cmd
2287            .subcommands
2288            .iter()
2289            // Aliases share a map entry with the name they point at; listing both would offer
2290            // the same command twice under two spellings.
2291            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2292            .map(|(name, _)| name.as_str())
2293            .collect();
2294        names.sort_unstable();
2295        out.errors.push(UsageErr::MissingSubcommand(
2296            out.cmd.name.clone(),
2297            names.join(", "),
2298        ));
2299    }
2300
2301    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2302    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2303    if !exclusive_present {
2304        for arg in out
2305            .cmds
2306            .iter()
2307            .enumerate()
2308            .filter(|(index, _)| requirements_apply(*index))
2309            .flat_map(|(_, cmd)| &cmd.args)
2310        {
2311            if out.args.contains_key(arg) {
2312                continue;
2313            }
2314            // Already reported as needing a `--`; one mistake should not yield two messages.
2315            if double_dash_violations.contains(&arg.name) {
2316                continue;
2317            }
2318            let required_if = arg.required_if.iter().any(|selector| {
2319                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2320            });
2321            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2322                selector_explicit_has_value(
2323                    &condition.selector,
2324                    &condition.value,
2325                    &out,
2326                    &overridden_flags,
2327                    custom_env,
2328                )
2329            });
2330            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2331                && arg.required_if_eq_all.iter().all(|condition| {
2332                    selector_explicit_has_value(
2333                        &condition.selector,
2334                        &condition.value,
2335                        &out,
2336                        &overridden_flags,
2337                        custom_env,
2338                    )
2339                });
2340            let unless_any = arg.required_unless.iter().any(|selector| {
2341                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2342            });
2343            let unless_all = !arg.required_unless_all.is_empty()
2344                && arg.required_unless_all.iter().all(|selector| {
2345                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2346                });
2347            let required_unless = (!arg.required_unless.is_empty()
2348                || !arg.required_unless_all.is_empty())
2349                && !(unless_any || unless_all);
2350            if (arg.required
2351                || required_if
2352                || required_if_eq
2353                || required_if_eq_all
2354                || required_unless)
2355                && arg.default.is_empty()
2356            {
2357                // Check if there's an env var available (custom env map takes precedence)
2358                let has_env = arg
2359                    .env
2360                    .as_ref()
2361                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2362                if !has_env {
2363                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2364                }
2365            }
2366        }
2367    }
2368
2369    // Conflicts are a question about the invocation as a whole rather than about any one
2370    // token, so they are checked here beside the requirement checks rather than at the
2371    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2372    // Its own loop: the requirement loop below skips the flags that *were* given, which
2373    // is exactly the set this needs.
2374    //
2375    // A value from the environment counts on both sides, matching what
2376    // `selector_is_explicit` says about the other flag: the question is whether a flag
2377    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2378    // would make the same pair of flags a conflict or not depending on which one
2379    // happened to be typed.
2380    for flag in unique_flags(out.available_flags.values()) {
2381        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2382        if !given || overridden_flags.contains(&flag.name) {
2383            continue;
2384        }
2385        for other in &flag.conflicts {
2386            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2387                out.errors.push(UsageErr::InvalidFlag {
2388                    token: format!("--{}", flag.name),
2389                    reason: format!("conflicts with {other}"),
2390                    span: (0, 0).into(),
2391                    input: format!("--{} {other}", flag.name),
2392                });
2393            }
2394        }
2395        // The positive form, checked in the same pass and under the same rule: a value
2396        // from the environment satisfies a requirement, because the question is whether
2397        // the other flag has a value rather than how it got one. A flag that was
2398        // overridden away has not been given, so it cannot satisfy anything either —
2399        // which is what `selector_is_explicit` already accounts for.
2400        //
2401        // Reported as the missing flag rather than as something wrong with the flag that
2402        // named it, which is what clap says too: an unmet `requires` is a required
2403        // argument that was not provided. Named by its own name, resolved through the
2404        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2405        let owner = out
2406            .cmds
2407            .iter()
2408            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2409            .unwrap_or(out.cmds.len() - 1);
2410        if !exclusive_present && requirements_apply(owner) {
2411            for other in &flag.requires {
2412                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2413                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2414                    if other.starts_with('-') {
2415                        out.errors.push(UsageErr::MissingFlag(name));
2416                    } else {
2417                        out.errors.push(UsageErr::MissingArg(name));
2418                    }
2419                }
2420            }
2421            for condition in &flag.requires_if {
2422                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2423                    && !selector_is_satisfied(
2424                        &condition.requires,
2425                        &out,
2426                        &overridden_flags,
2427                        custom_env,
2428                    )
2429                {
2430                    let name = selector_flag_name(&condition.requires, &out)
2431                        .unwrap_or_else(|| condition.requires.clone());
2432                    out.errors.push(UsageErr::MissingFlag(name));
2433                }
2434            }
2435        }
2436    }
2437
2438    // Positionals can declare the same pairwise conflict as flags. Their selector is
2439    // the bare argument name, while a flag keeps its dashed spelling.
2440    for (command_index, arg) in out
2441        .cmds
2442        .iter()
2443        .enumerate()
2444        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2445    {
2446        let given = arg_is_explicit(arg, &out, custom_env);
2447        if !given {
2448            continue;
2449        }
2450        for other in &arg.conflicts {
2451            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2452                out.errors.push(UsageErr::InvalidFlag {
2453                    token: arg.name.clone(),
2454                    reason: format!("conflicts with {other}"),
2455                    span: (0, 0).into(),
2456                    input: format!("{} {other}", arg.name),
2457                });
2458            }
2459        }
2460        if !exclusive_present && requirements_apply(command_index) {
2461            for other in &arg.requires {
2462                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2463                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2464                    if other.starts_with('-') {
2465                        out.errors.push(UsageErr::MissingFlag(name));
2466                    } else {
2467                        out.errors.push(UsageErr::MissingArg(name));
2468                    }
2469                }
2470            }
2471        }
2472    }
2473
2474    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2475    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2476    // positionals included, which is what distinguishes it from being in a group with
2477    // every other flag.
2478    //
2479    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2480    // exclusive one is nobody saying anything, and counting it would make the exclusive
2481    // flag unusable on any command that has a default. Environment values do count, also as
2482    // `conflicts` reads them, so the spec parser and the derive agree.
2483    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2484        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2485        // but re-declared aliases can leave distinct declarations with that same name in
2486        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2487        // compare the parser's `Arc`s by identity here.
2488        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2489        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2490            continue;
2491        }
2492        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2493            .find(|other| {
2494                !Arc::ptr_eq(other, flag)
2495                    && !overridden_flags.contains(&other.name)
2496                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2497            })
2498            .map(|other| format!("--{}", other.name));
2499        let other_arg = out.cmd.args.iter().find(|arg| {
2500            out.args.keys().any(|given| given.name == arg.name)
2501                || arg
2502                    .env
2503                    .as_ref()
2504                    .is_some_and(|env| env_contains(custom_env, env))
2505        });
2506        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2507        // exclusive flag belonging to the child itself does not conflict with the command word
2508        // needed to reach that child — so the question is not who owns the flag but whose
2509        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2510        // separated.
2511        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2512        let selected_subcommand =
2513            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2514        let other = other_flag
2515            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2516            .or(selected_subcommand);
2517        if let Some(other) = other {
2518            out.errors.push(UsageErr::InvalidFlag {
2519                token: format!("--{}", flag.name),
2520                reason: format!("must be given on its own, and {other} was given too"),
2521                span: (0, 0).into(),
2522                input: format!("--{} {other}", flag.name),
2523            });
2524        }
2525    }
2526
2527    // Groups, checked once per group rather than per flag: both questions a group asks —
2528    // how many members were given, and whether that is enough — are about the set, which
2529    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2530    //
2531    // The same "given" rule as everything else here, so a member filled from the
2532    // environment or a default counts.
2533    // Every command in the chain, not only the selected one: a group may name global
2534    // flags, which belong to an ancestor and are declared there.
2535    let mut group_errors: Vec<UsageErr> = Vec::new();
2536    for (command_index, group) in out
2537        .cmds
2538        .iter()
2539        .enumerate()
2540        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2541    {
2542        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2543        // `--file` are two spellings of one flag, and a group naming both — or naming one
2544        // flag twice — would otherwise report that flag as conflicting with itself the
2545        // moment it was given. Deduplicated rather than refused where the group is
2546        // written, because listing both spellings is redundant, not wrong.
2547        let mut given: Vec<&str> = Vec::new();
2548        let mut seen: Vec<String> = Vec::new();
2549        for selector in &group.members {
2550            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2551                continue;
2552            }
2553            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2554            if seen.contains(&name) {
2555                continue;
2556            }
2557            seen.push(name);
2558            given.push(selector.as_str());
2559        }
2560        if !group.multiple && given.len() > 1 {
2561            group_errors.push(UsageErr::InvalidFlag {
2562                token: given[1].to_string(),
2563                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2564                span: (0, 0).into(),
2565                input: format!("{} {}", given[0], given[1]),
2566            });
2567        }
2568        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2569        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2570        // exclusivity must count only what was supplied, or a defaulted member would
2571        // collide with the sibling the user actually typed.
2572        let satisfied = group
2573            .members
2574            .iter()
2575            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2576        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2577            // The members are what a user has to type, so they are in the message; the
2578            // group's name is there too, since a command with several groups would
2579            // otherwise report the same sentence twice with nothing to tell them apart.
2580            group_errors.push(UsageErr::MissingGroup {
2581                group: group.name.clone(),
2582                members: group.members.join(", "),
2583            });
2584        }
2585    }
2586    out.errors.extend(group_errors);
2587
2588    if !exclusive_present {
2589        for flag in unique_flags(out.available_flags.values()) {
2590            let owner = out
2591                .cmds
2592                .iter()
2593                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2594                .unwrap_or(out.cmds.len() - 1);
2595            if !requirements_apply(owner) {
2596                continue;
2597            }
2598            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2599                continue;
2600            }
2601            let has_default =
2602                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2603            let has_env = flag_has_env(flag, custom_env);
2604            let required_if = flag.required_if.iter().any(|selector| {
2605                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2606            });
2607            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2608                selector_explicit_has_value(
2609                    &condition.selector,
2610                    &condition.value,
2611                    &out,
2612                    &overridden_flags,
2613                    custom_env,
2614                )
2615            });
2616            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2617                && flag.required_if_eq_all.iter().all(|condition| {
2618                    selector_explicit_has_value(
2619                        &condition.selector,
2620                        &condition.value,
2621                        &out,
2622                        &overridden_flags,
2623                        custom_env,
2624                    )
2625                });
2626            let unless_any = flag.required_unless.iter().any(|selector| {
2627                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2628            });
2629            let unless_all = !flag.required_unless_all.is_empty()
2630                && flag.required_unless_all.iter().all(|selector| {
2631                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2632                });
2633            let required_unless = (!flag.required_unless.is_empty()
2634                || !flag.required_unless_all.is_empty())
2635                && !(unless_any || unless_all);
2636            if (flag.required
2637                || required_if
2638                || required_if_eq
2639                || required_if_eq_all
2640                || required_unless)
2641                && !has_default
2642                && !has_env
2643            {
2644                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2645            }
2646        }
2647    }
2648
2649    // Validate var_min/var_max constraints for variadic args
2650    for (arg, value) in &out.args {
2651        if arg.var {
2652            if let ParseValue::MultiString(values) = value {
2653                if let Some(min) = arg.var_min {
2654                    if values.len() < min {
2655                        out.errors.push(UsageErr::VarArgTooFew {
2656                            name: arg.name.clone(),
2657                            min,
2658                            got: values.len(),
2659                        });
2660                    }
2661                }
2662                if let Some(max) = arg.var_max {
2663                    if values.len() > max {
2664                        out.errors.push(UsageErr::VarArgTooMany {
2665                            name: arg.name.clone(),
2666                            max,
2667                            got: values.len(),
2668                        });
2669                    }
2670                }
2671            }
2672        }
2673    }
2674
2675    // Validate var_min/var_max constraints for variadic flags. These are bounds on
2676    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
2677    // by binding once per occurrence, where the per-occurrence count is still available.
2678    for flag in unique_flags(out.available_flags.values()) {
2679        if flag.var {
2680            let bound = match out.flags.get(flag) {
2681                Some(ParseValue::MultiString(values)) => values.len(),
2682                Some(ParseValue::MultiBool(values)) => values.len(),
2683                Some(_) => 1,
2684                None => 0,
2685            };
2686            // A partial parse deliberately leaves the final value-optional flag pending so
2687            // completion can still answer for it. It is nevertheless a real occurrence for
2688            // the repeated flag's bounds; the full parser closes it just after this phase.
2689            let pending = out
2690                .flag_awaiting_value
2691                .iter()
2692                .filter(|pending| {
2693                    Arc::ptr_eq(pending, flag)
2694                        && (pending.value_optional || pending.default_missing.is_some())
2695                })
2696                .count();
2697            let count = bound + pending;
2698            if count == 0 {
2699                continue;
2700            }
2701            if let Some(min) = flag.var_min {
2702                if count < min {
2703                    out.errors.push(UsageErr::VarFlagTooFew {
2704                        name: flag.name.clone(),
2705                        min,
2706                        got: count,
2707                    });
2708                }
2709            }
2710            if let Some(max) = flag.var_max {
2711                if count > max {
2712                    out.errors.push(UsageErr::VarFlagTooMany {
2713                        name: flag.name.clone(),
2714                        max,
2715                        got: count,
2716                    });
2717                }
2718            }
2719        }
2720    }
2721
2722    Ok((out, overridden_flags))
2723}
2724
2725fn validate_expression(
2726    name: &str,
2727    expression: Option<&str>,
2728    message: Option<&str>,
2729    parsed: &ParseValue,
2730    errors: &mut Vec<UsageErr>,
2731) {
2732    let Some(expression) = expression else {
2733        return;
2734    };
2735    #[cfg(not(feature = "validation"))]
2736    let _ = expression;
2737    let values: &[String] = match parsed {
2738        ParseValue::String(value) => std::slice::from_ref(value),
2739        ParseValue::MultiString(values) => values,
2740        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2741    };
2742    #[cfg(feature = "validation")]
2743    for value in values {
2744        let reason = match usage_validation::validate(expression, value) {
2745            Ok(true) => continue,
2746            Ok(false) => message
2747                .unwrap_or("does not satisfy the validation expression")
2748                .to_string(),
2749            Err(error) => format!("validation expression failed: {error}"),
2750        };
2751        errors.push(UsageErr::InvalidValue {
2752            name: name.to_string(),
2753            value: value.clone(),
2754            reason,
2755        });
2756        break;
2757    }
2758    #[cfg(not(feature = "validation"))]
2759    if let Some(value) = values.first() {
2760        let _ = message;
2761        errors.push(UsageErr::InvalidValue {
2762            name: name.to_string(),
2763            value: value.clone(),
2764            reason: "expression validation requires the `validation` feature".to_string(),
2765        });
2766    }
2767}
2768
2769#[cfg(all(test, not(feature = "validation")))]
2770mod optional_validation_tests {
2771    use crate::{parse, Spec};
2772
2773    #[test]
2774    fn validation_declarations_require_the_opt_in_runtime_feature() {
2775        let spec: Spec = r#"
2776name "ex"
2777bin "ex"
2778arg "<port>" validate="int(value) > 0"
2779        "#
2780        .parse()
2781        .unwrap();
2782        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2783        assert!(
2784            error
2785                .to_string()
2786                .contains("requires the `validation` feature"),
2787            "{error:?}"
2788        );
2789    }
2790}
2791
2792fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2793    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2794}
2795
2796fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2797    overrider
2798        .overrides
2799        .iter()
2800        .any(|selector| flag_matches_selector(overridden, selector))
2801}
2802
2803fn apply_prefix_flag_overrides(
2804    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2805    flag: Arc<SpecFlag>,
2806) {
2807    prefix_flags
2808        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2809}
2810
2811fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2812    prefix_flags
2813        .iter()
2814        .flat_map(|(_, words)| words.iter().cloned())
2815        .collect()
2816}
2817
2818fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2819    match custom_env {
2820        Some(env) => env.contains_key(env_var),
2821        None => std::env::var(env_var).is_ok(),
2822    }
2823}
2824
2825fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2826    flag.env_names()
2827        .any(|env_var| env_contains(custom_env, env_var))
2828}
2829
2830fn fallback_is_true(value: &str) -> bool {
2831    matches!(value, "1" | "true" | "True" | "TRUE")
2832}
2833
2834fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2835    match delimiter {
2836        Some(delimiter) => values
2837            .iter()
2838            .flat_map(|value| value.split(delimiter).map(str::to_string))
2839            .collect(),
2840        None => values.to_vec(),
2841    }
2842}
2843
2844fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2845    if let Some(min) = arg.var_min {
2846        if count < min {
2847            errors.push(UsageErr::VarArgTooFew {
2848                name: arg.name.clone(),
2849                min,
2850                got: count,
2851            });
2852        }
2853    }
2854    if let Some(max) = arg.var_max {
2855        if count > max {
2856            errors.push(UsageErr::VarArgTooMany {
2857                name: arg.name.clone(),
2858                max,
2859                got: count,
2860            });
2861        }
2862    }
2863}
2864
2865fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2866    if let Some(min) = flag.var_min {
2867        if count < min {
2868            errors.push(UsageErr::VarFlagTooFew {
2869                name: flag.name.clone(),
2870                min,
2871                got: count,
2872            });
2873        }
2874    }
2875    if let Some(max) = flag.var_max {
2876        if count > max {
2877            errors.push(UsageErr::VarFlagTooMany {
2878                name: flag.name.clone(),
2879                max,
2880                got: count,
2881            });
2882        }
2883    }
2884}
2885
2886fn validate_flag_arg_fallback_count(
2887    flag: &SpecFlag,
2888    arg: &SpecArg,
2889    count: usize,
2890    errors: &mut Vec<UsageErr>,
2891) {
2892    if let Some(min) = arg.var_min {
2893        if count < min {
2894            errors.push(UsageErr::VarFlagTooFew {
2895                name: flag.name.clone(),
2896                min,
2897                got: count,
2898            });
2899        }
2900    }
2901    if let Some(max) = arg.var_max {
2902        if count > max {
2903            errors.push(UsageErr::VarFlagTooMany {
2904                name: flag.name.clone(),
2905                max,
2906                got: count,
2907            });
2908        }
2909    }
2910}
2911
2912/// Bind a fallback the way an unconditional `default` does: one value, or several
2913/// for `var`, and choices checked the same way.
2914fn bind_flag_fallback(
2915    flag: &Arc<SpecFlag>,
2916    values: &[String],
2917    out: &mut ParseOutput,
2918    custom_env: Option<&HashMap<String, String>>,
2919    origin: ValueOrigin,
2920) -> Result<(), miette::Error> {
2921    if values.is_empty() {
2922        return Ok(());
2923    }
2924    if let Some(arg) = flag.arg.as_ref() {
2925        let values = split_fallback_values(values, arg.delimiter);
2926        if flag.var || arg.var {
2927            if flag.var {
2928                validate_flag_fallback_count(flag, values.len(), &mut out.errors);
2929            }
2930            if arg.var {
2931                validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
2932            }
2933            validate_choice_values(
2934                ChoiceTarget::option(flag),
2935                &values,
2936                arg.choices.as_ref(),
2937                custom_env,
2938            )?;
2939            out.flags
2940                .insert(Arc::clone(flag), ParseValue::MultiString(values));
2941        } else {
2942            let value = values.into_iter().next().unwrap_or_default();
2943            validate_choice_value(
2944                ChoiceTarget::option(flag),
2945                &value,
2946                arg.choices.as_ref(),
2947                custom_env,
2948            )?;
2949            out.flags
2950                .insert(Arc::clone(flag), ParseValue::String(value));
2951        }
2952    } else if flag.var {
2953        validate_flag_fallback_count(flag, values.len(), &mut out.errors);
2954        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
2955        out.flags
2956            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
2957    } else {
2958        out.flags.insert(
2959            Arc::clone(flag),
2960            ParseValue::Bool(fallback_is_true(&values[0])),
2961        );
2962    }
2963    out.flag_origins
2964        .entry(Arc::clone(flag))
2965        .or_default()
2966        .push(origin);
2967    Ok(())
2968}
2969
2970fn default_if_condition_matches(
2971    condition: &crate::SpecDefaultIf,
2972    out: &ParseOutput,
2973    overridden_flags: &HashSet<String>,
2974    custom_env: Option<&HashMap<String, String>>,
2975) -> bool {
2976    match &condition.when {
2977        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
2978        Some(when) => {
2979            let Some(flag) = out
2980                .available_flags
2981                .values()
2982                .chain(out.flags.keys())
2983                .find(|flag| flag_matches_selector(flag, &condition.selector))
2984            else {
2985                return false;
2986            };
2987            if overridden_flags.contains(&flag.name) {
2988                return false;
2989            }
2990            explicit_flag_has_value(flag, when, out, custom_env)
2991        }
2992    }
2993}
2994
2995/// Whether an explicitly supplied value of `flag` equals `expected`.
2996///
2997/// clap treats command-line and environment values as explicit for `requires_if`, but
2998/// not defaults. Keep that source distinction here instead of consulting the flag's
2999/// defaults through `selector_is_satisfied`.
3000fn explicit_flag_has_value(
3001    flag: &SpecFlag,
3002    expected: &str,
3003    out: &ParseOutput,
3004    custom_env: Option<&HashMap<String, String>>,
3005) -> bool {
3006    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3007        ParseValue::Bool(value) => value.to_string() == expected,
3008        ParseValue::String(value) => value == expected,
3009        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3010        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3011    });
3012    if out.flags.contains_key(flag) {
3013        return parsed_matches;
3014    }
3015
3016    let value = flag.env_names().find_map(|env| match custom_env {
3017        Some(values) => values.get(env).cloned(),
3018        None => std::env::var(env).ok(),
3019    });
3020    value.is_some_and(
3021        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3022            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3023            None if flag.arg.is_none() => {
3024                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3025            }
3026            None => value == expected,
3027        },
3028    )
3029}
3030
3031fn selector_explicit_has_value(
3032    selector: &str,
3033    expected: &str,
3034    out: &ParseOutput,
3035    overridden_flags: &HashSet<String>,
3036    custom_env: Option<&HashMap<String, String>>,
3037) -> bool {
3038    if let Some(flag) = out
3039        .available_flags
3040        .values()
3041        .chain(out.flags.keys())
3042        .find(|flag| flag_matches_selector(flag, selector))
3043    {
3044        return !overridden_flags.contains(&flag.name)
3045            && explicit_flag_has_value(flag, expected, out, custom_env);
3046    }
3047    let Some(arg) = selector_arg(selector, out) else {
3048        return false;
3049    };
3050    let parsed = out
3051        .args
3052        .iter()
3053        .find(|(given, _)| given.name == arg.name)
3054        .map(|(_, value)| value);
3055    if let Some(value) = parsed {
3056        return match value {
3057            ParseValue::String(value) => value == expected,
3058            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3059            ParseValue::Bool(value) => value.to_string() == expected,
3060            ParseValue::MultiBool(values) => {
3061                values.iter().any(|value| value.to_string() == expected)
3062            }
3063        };
3064    }
3065    let value = arg.env_names().find_map(|env| match custom_env {
3066        Some(values) => values.get(env).cloned(),
3067        None => std::env::var(env).ok(),
3068    });
3069    value.is_some_and(|value| match arg.delimiter {
3070        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3071        None => value == expected,
3072    })
3073}
3074
3075fn selector_is_explicit(
3076    selector: &str,
3077    out: &ParseOutput,
3078    overridden_flags: &HashSet<String>,
3079    custom_env: Option<&HashMap<String, String>>,
3080) -> bool {
3081    let flag_is_explicit = out
3082        .available_flags
3083        .values()
3084        .chain(out.flags.keys())
3085        .any(|flag| {
3086            flag_matches_selector(flag, selector)
3087                && !overridden_flags.contains(&flag.name)
3088                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3089        });
3090    flag_is_explicit
3091        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3092}
3093
3094/// The name of the flag a selector points at, for an error that has to name it.
3095///
3096/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3097/// about a flag that is *missing* has to say which one, and the selector may be a short
3098/// form or an alias rather than the name.
3099fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3100    out.available_flags
3101        .values()
3102        .chain(out.flags.keys())
3103        .find(|flag| flag_matches_selector(flag, selector))
3104        .map(|flag| flag.name.clone())
3105        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3106}
3107
3108/// Whether a selector's flag ended up with a value, however it got one.
3109///
3110/// The rule for a *positive* relationship, and the difference from
3111/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
3112/// exclusivity — has to count only what was given, or a flag with a default would
3113/// conflict with everything and no command line would parse. A positive one asks whether
3114/// the flag it names has a value, and a default is a value: that is already how plain
3115/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
3116/// otherwise would have made the same flag missing here and present ten lines below.
3117fn selector_is_satisfied(
3118    selector: &str,
3119    out: &ParseOutput,
3120    overridden_flags: &HashSet<String>,
3121    custom_env: Option<&HashMap<String, String>>,
3122) -> bool {
3123    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3124        return true;
3125    }
3126    let flag_is_satisfied = out
3127        .available_flags
3128        .values()
3129        .chain(out.flags.keys())
3130        .filter(|flag| flag_matches_selector(flag, selector))
3131        .any(|flag| {
3132            !overridden_flags.contains(&flag.name)
3133                && (!flag.default.is_empty()
3134                    || flag.arg.iter().any(|a| !a.default.is_empty())
3135                    || flag.default_if.iter().any(|condition| {
3136                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
3137                    }))
3138        });
3139    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3140}
3141
3142fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3143    // Bare words are positional selectors. Keep accepting a flag's internal name above
3144    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
3145    if selector.starts_with('-') {
3146        return None;
3147    }
3148    out.cmds
3149        .iter()
3150        .flat_map(|cmd| &cmd.args)
3151        .find(|arg| arg.name == selector)
3152}
3153
3154fn arg_is_explicit(
3155    arg: &SpecArg,
3156    out: &ParseOutput,
3157    custom_env: Option<&HashMap<String, String>>,
3158) -> bool {
3159    out.args.keys().any(|given| given.name == arg.name)
3160        || arg
3161            .env
3162            .as_ref()
3163            .is_some_and(|env| env_contains(custom_env, env))
3164}
3165
3166fn apply_flag_overrides(
3167    flag: &Arc<SpecFlag>,
3168    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3169    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3170    pending_flags: &mut Vec<Arc<SpecFlag>>,
3171    overridden_flags: &mut HashSet<String>,
3172    // The reportable half of the same fact: which flag did the overriding. The set above
3173    // only stops a default or an environment value restoring what was overridden, and
3174    // "`--quiet` is unset despite its default" has no answer without the name.
3175    attributed: &mut BTreeMap<String, String>,
3176) {
3177    let overridden_names: HashSet<String> = available_flags
3178        .values()
3179        .chain(parsed_flags.keys())
3180        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3181        .map(|other| other.name.clone())
3182        .collect();
3183
3184    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3185    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3186    for name in &overridden_names {
3187        attributed.insert(name.clone(), flag.name.clone());
3188    }
3189    overridden_flags.extend(overridden_names);
3190    // An explicit occurrence always restores this flag, including self-overrides.
3191    overridden_flags.remove(&flag.name);
3192    attributed.remove(&flag.name);
3193}
3194
3195#[cfg(feature = "docs")]
3196fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3197    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3198}
3199
3200#[cfg(feature = "docs")]
3201fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3202    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3203        if !out.is_empty() {
3204            out.push('\n');
3205        }
3206        out.push_str(&docs::cli::render_help(spec, cmd, true));
3207        let mut children: Vec<_> = cmd
3208            .subcommands
3209            .values()
3210            .filter(|child| !child.hide)
3211            .collect();
3212        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3213        for child in children {
3214            append(out, spec, child);
3215        }
3216    }
3217
3218    let mut out = String::new();
3219    append(&mut out, spec, cmd);
3220    UsageErr::Help(out)
3221}
3222
3223#[cfg(not(feature = "docs"))]
3224fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3225    UsageErr::Help("help".to_string())
3226}
3227
3228#[cfg(not(feature = "docs"))]
3229fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3230    UsageErr::Help("help".to_string())
3231}
3232
3233/// The version to answer with. `--version` prefers the long text and `-V` the concise
3234/// one, each falling back to the other when only one is declared.
3235fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3236    let value = if long {
3237        spec.long_version.as_ref().or(spec.version.as_ref())
3238    } else {
3239        spec.version.as_ref().or(spec.long_version.as_ref())
3240    };
3241    UsageErr::Version(value.cloned().unwrap_or_default())
3242}
3243
3244fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3245    use crate::SpecFlagAction;
3246    match flag.action {
3247        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3248        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3249        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3250        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3251        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3252        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3253    }
3254}
3255
3256#[derive(Copy, Clone)]
3257struct ChoiceTarget<'a> {
3258    kind: &'a str,
3259    name: &'a str,
3260}
3261
3262impl<'a> ChoiceTarget<'a> {
3263    fn arg(arg: &'a SpecArg) -> Self {
3264        Self {
3265            kind: "arg",
3266            name: &arg.name,
3267        }
3268    }
3269
3270    fn option(flag: &'a SpecFlag) -> Self {
3271        Self {
3272            kind: "option",
3273            name: &flag.name,
3274        }
3275    }
3276}
3277
3278/// Whether every letter of a short token names a flag in scope.
3279///
3280/// Scanning stops at the first letter whose flag takes a value, because everything
3281/// after it is that value rather than more letters.
3282fn short_bundle_is_known(
3283    spec: &Spec,
3284    cmds: &[SpecCommand],
3285    available: &BTreeMap<String, Arc<SpecFlag>>,
3286    token: &str,
3287) -> bool {
3288    for c in token.chars().skip(1) {
3289        match available.get(&format!("-{c}")) {
3290            // `-h` and `-V` are recognized letters even though no spec declares them, so a
3291            // bundle containing one is a bundle. Without this `-vh` was not read as one at
3292            // all and fell through to `unexpected word`, while usage-argv, usage-go and
3293            // clap all answer it with help.
3294            None if supplied_short(spec, cmds, c).is_some() => {}
3295            None => return false,
3296            Some(f) if f.arg.is_some() => return true,
3297            Some(_) => {}
3298        }
3299    }
3300    true
3301}
3302
3303/// The response `-h` or `-V` produces where nothing declares that letter.
3304///
3305/// The letter form of the flags the parser supplies rather than a spec declaring them,
3306/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
3307/// about here one letter at a time, because a bundle is read one letter at a time.
3308///
3309/// Always the short response: `-h` is short help however many letters share its token,
3310/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
3311/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
3312fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3313    let cmd = cmds.last()?;
3314    match letter {
3315        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3316        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3317        _ => None,
3318    }
3319}
3320
3321/// Refuse a flag-like token that named nothing, if this command asked for that.
3322///
3323/// Called from the flag branches, where the lookup has just failed and nothing from
3324/// the token has been applied yet — so a bundle like `-az` is refused whole rather
3325/// than after setting `-a`.
3326fn reject_unknown_flag_if_asked(
3327    spec: &Spec,
3328    path: &[SpecCommand],
3329    token: &str,
3330) -> Result<(), UsageErr> {
3331    // A lone `-` is a value by convention. A negative number reaches this only
3332    // when no pending value opted into the narrower exception.
3333    if !is_flag_like(token) {
3334        return Ok(());
3335    }
3336    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3337        return Ok(());
3338    }
3339    Err(UsageErr::InvalidFlag {
3340        token: token.to_string(),
3341        reason: "no such flag".to_string(),
3342        span: (0, 0).into(),
3343        input: token.to_string(),
3344    })
3345}
3346
3347/// Whether a flag-like token that matches nothing is a value or an error, here.
3348///
3349/// The nearest enclosing command that stated a preference wins, then the spec,
3350/// then the default. Inherited, unlike `effect`: it describes how a command line
3351/// is read, and a CLI that forwards options tends to forward them at every level.
3352fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3353    path.iter()
3354        .rev()
3355        .find_map(|cmd| cmd.unknown_flags)
3356        .or(spec.unknown_flags)
3357        .unwrap_or_default()
3358}
3359
3360/// Whether a token would be read as a flag, for the purpose of rejecting unknown
3361/// ones.
3362///
3363/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
3364/// a field may make the narrower negative-number exception.
3365fn is_flag_like(token: &str) -> bool {
3366    match token.strip_prefix('-') {
3367        None | Some("") => false,
3368        Some(_) => true,
3369    }
3370}
3371
3372fn is_negative_number(token: &str) -> bool {
3373    token.strip_prefix('-').is_some_and(is_number)
3374}
3375
3376fn record_scalar_flag_occurrence(
3377    cmds: &[SpecCommand],
3378    flag: &Arc<SpecFlag>,
3379    command_level: usize,
3380    bool_value: Option<bool>,
3381    occurrences: &mut HashMap<(usize, usize), u8>,
3382    errors: &mut Vec<UsageErr>,
3383) {
3384    let strict = cmds
3385        .get(command_level)
3386        .is_some_and(|cmd| !cmd.args_override_self);
3387    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3388    if !strict || flag.count || collects_values {
3389        return;
3390    }
3391
3392    let bit = match bool_value {
3393        Some(false) if flag.negate.is_some() => 0b10,
3394        _ => 0b01,
3395    };
3396    let key = (Arc::as_ptr(flag) as usize, command_level);
3397    let seen = occurrences.entry(key).or_default();
3398    if *seen & bit != 0 {
3399        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3400    }
3401    *seen |= bit;
3402}
3403
3404/// A token that can select a subcommand, trigger a mount, or be forwarded as an
3405/// external command.
3406///
3407/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
3408/// so it was never a candidate to *select* anything either. usage-argv uses the
3409/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
3410/// treated every token that `starts_with('-')` as a flag.
3411fn is_command_word(token: &str) -> bool {
3412    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3413}
3414
3415/// Whether an otherwise numeric-looking token is an exact declared short flag.
3416///
3417/// This check belongs in both parse phases: phase 1 must skip the flag while it
3418/// searches for a later subcommand, and phase 2 must bind it instead of offering
3419/// it to an `allow_negative_numbers` positional.
3420fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3421    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3422}
3423
3424/// Whether an unmatched word belongs to the root's default command.
3425///
3426/// Ordinary words always do. A negative number only does when the default command's
3427/// first positional explicitly accepts one; otherwise it stays at the root, matching
3428/// usage-argv and generated Go.
3429fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3430    !is_negative_number(token)
3431        || cmd
3432            .find_subcommand(default_name)
3433            .and_then(|default| default.args.first())
3434            .is_some_and(|arg| arg.allow_negative_numbers)
3435}
3436
3437/// Digits, at most one `.`, and an optional exponent.
3438///
3439/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
3440/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
3441/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
3442/// two disagreed about `-1e5` when one used a float parse and the other did not.
3443fn is_number(rest: &str) -> bool {
3444    let (mantissa, exponent) = match rest.find(['e', 'E']) {
3445        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3446        None => (rest, None),
3447    };
3448
3449    let mut seen_digit = false;
3450    let mut seen_dot = false;
3451    for c in mantissa.chars() {
3452        match c {
3453            '0'..='9' => seen_digit = true,
3454            '.' if !seen_dot => seen_dot = true,
3455            _ => return false,
3456        }
3457    }
3458    if !seen_digit {
3459        return false;
3460    }
3461
3462    match exponent {
3463        None => true,
3464        Some(exp) => {
3465            let digits = exp
3466                .strip_prefix('+')
3467                .or_else(|| exp.strip_prefix('-'))
3468                .unwrap_or(exp);
3469            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3470        }
3471    }
3472}
3473
3474/// Bind one value to the flag waiting for it, and let a variadic argument go on
3475/// collecting from the words that follow.
3476///
3477/// Every route to a flag's value comes through here — the following word, the text
3478/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
3479/// like — so that all three agree on how many values the flag ends up with.
3480#[allow(clippy::too_many_arguments)]
3481fn bind_pending_flag_value(
3482    spec: &Spec,
3483    cmd: &SpecCommand,
3484    errors: &mut Vec<UsageErr>,
3485    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3486    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3487    word: &mut String,
3488    input: &mut VecDeque<Token>,
3489    custom_env: Option<&HashMap<String, String>>,
3490    trace: &mut Trace,
3491    // Which token supplied `word`, and whether it rode along on the flag's own token
3492    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
3493    // their own positions and are recorded where they are read.
3494    argv: usize,
3495    attached: bool,
3496) -> miette::Result<bool> {
3497    // Held before the drain pops it, along with what the flag is already carrying: a
3498    // `var_max` bounds the values this occurrence takes, not the list they are appended
3499    // to, so a second `--include` starts counting again.
3500    let collecting = flag_awaiting_value
3501        .last()
3502        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3503        .cloned()
3504        .map(|flag| {
3505            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3506            (flag, carried)
3507        });
3508    let mut bound = vec![];
3509    let refused = drain_pending_flag_values(
3510        spec,
3511        cmd,
3512        errors,
3513        flags,
3514        flag_awaiting_value,
3515        word,
3516        custom_env,
3517        &mut bound,
3518    )?;
3519    for (flag, values) in bound {
3520        trace.record(
3521            argv,
3522            TokenRole::Value {
3523                flag,
3524                values,
3525                attached,
3526            },
3527        );
3528    }
3529    if refused {
3530        return Ok(true);
3531    }
3532    let Some((flag, carried)) = collecting else {
3533        return Ok(false);
3534    };
3535    collect_variadic_flag_values(
3536        spec,
3537        cmd,
3538        errors,
3539        flags,
3540        flag_awaiting_value,
3541        &flag,
3542        carried,
3543        input,
3544        custom_env,
3545        trace,
3546    )
3547}
3548
3549/// Keep feeding a flag whose argument is variadic from the words that follow it.
3550///
3551/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
3552/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
3553/// This is greedy by design — a command declaring both such a flag and positionals will
3554/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
3555///
3556/// `carried` is what the flag already held when this occurrence began, so the bound
3557/// counts this run rather than everything the flag has collected across the command
3558/// line. Each value goes through the same drain as the first, so choices are checked
3559/// and the value lands in the same list rather than by a second route that could
3560/// disagree.
3561#[allow(clippy::too_many_arguments)]
3562fn collect_variadic_flag_values(
3563    spec: &Spec,
3564    cmd: &SpecCommand,
3565    errors: &mut Vec<UsageErr>,
3566    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3567    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3568    flag: &Arc<SpecFlag>,
3569    carried: usize,
3570    input: &mut VecDeque<Token>,
3571    custom_env: Option<&HashMap<String, String>>,
3572    trace: &mut Trace,
3573) -> miette::Result<bool> {
3574    let max = flag
3575        .arg
3576        .as_ref()
3577        .and_then(|arg| arg.var_max)
3578        .unwrap_or(usize::MAX);
3579    while flags
3580        .get(flag)
3581        .map(value_count)
3582        .unwrap_or(0)
3583        .saturating_sub(carried)
3584        < max
3585    {
3586        let Some(next) = input.front().map(|token| token.word.as_str()) else {
3587            break;
3588        };
3589        if flag
3590            .arg
3591            .as_ref()
3592            .and_then(|arg| arg.value_terminator.as_deref())
3593            == Some(next)
3594        {
3595            let terminator = input.pop_front().unwrap();
3596            trace.record(
3597                terminator.argv,
3598                TokenRole::ValueTerminator {
3599                    ends: flag.name.clone(),
3600                },
3601            );
3602            break;
3603        }
3604        // The separator is left where it is: stopping here hands it to the arm that
3605        // knows what it means, rather than reading it as one more value.
3606        if next == "--"
3607            || (is_flag_like(next)
3608                && !(flag
3609                    .arg
3610                    .as_ref()
3611                    .is_some_and(|arg| arg.allow_negative_numbers)
3612                    && is_negative_number(next)))
3613        {
3614            break;
3615        }
3616        let taken = input.pop_front().unwrap();
3617        let argv = taken.argv;
3618        let mut word = taken.word;
3619        flag_awaiting_value.push(Arc::clone(flag));
3620        let mut bound = vec![];
3621        let refused = drain_pending_flag_values(
3622            spec,
3623            cmd,
3624            errors,
3625            flags,
3626            flag_awaiting_value,
3627            &mut word,
3628            custom_env,
3629            &mut bound,
3630        )?;
3631        for (flag, values) in bound {
3632            // A later word of the same occurrence is its own token, and never attached:
3633            // only the first value can ride along on the flag.
3634            trace.record(
3635                argv,
3636                TokenRole::Value {
3637                    flag,
3638                    values,
3639                    attached: false,
3640                },
3641            );
3642        }
3643        if refused {
3644            return Ok(true);
3645        }
3646    }
3647    // The loop stops once the occurrence has reached its bound, which without a delimiter is
3648    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
3649    // several values, so the run can end up *past* the bound rather than on it, and stopping
3650    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
3651    // case — three values out of the one word the loop was entitled to take.
3652    //
3653    // Counted against `carried` like the loop itself, so this stays a statement about the
3654    // occurrence rather than about the list the occurrences build up.
3655    let taken = flags
3656        .get(flag)
3657        .map(value_count)
3658        .unwrap_or(0)
3659        .saturating_sub(carried);
3660    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3661        if taken < min {
3662            errors.push(UsageErr::VarFlagTooFew {
3663                name: flag.name.clone(),
3664                min,
3665                got: taken,
3666            });
3667        }
3668    }
3669    if taken > max {
3670        errors.push(UsageErr::VarFlagTooMany {
3671            name: flag.name.clone(),
3672            max,
3673            got: taken,
3674        });
3675    }
3676    Ok(false)
3677}
3678
3679/// How many values a flag is holding, for a bound that counts them.
3680fn value_count(value: &ParseValue) -> usize {
3681    match value {
3682        ParseValue::MultiString(values) => values.len(),
3683        ParseValue::MultiBool(values) => values.len(),
3684        _ => 1,
3685    }
3686}
3687
3688/// Finish a value-optional flag that was given with no value.
3689///
3690/// Returns whether anything was bound. Completions keep the flag waiting — a
3691/// half-typed `--color ` is a question about the value — so this is asked only
3692/// once a full parse has decided the value is not coming, or once the next token
3693/// has made that decision.
3694///
3695/// The missing string is a real value: if the flag names `choices`, it has to
3696/// be one of them, the same way an env var or a `default` is checked. Binding
3697/// first and failing later would leave the flag set to a value the spec forbids.
3698fn try_bind_default_missing(
3699    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3700    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3701    custom_env: Option<&HashMap<String, String>>,
3702    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3703) -> miette::Result<bool> {
3704    let Some(flag) = flag_awaiting_value.last() else {
3705        return Ok(false);
3706    };
3707    let value = match flag.default_missing.clone() {
3708        Some(value) => value,
3709        None if flag.value_optional => {
3710            let flag = flag_awaiting_value.pop().unwrap();
3711            // Presence in the map distinguishes this from an absent flag; an
3712            // empty collection distinguishes it from an explicitly empty
3713            // `--flag=` string without inventing a sentinel value.
3714            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3715            origins
3716                .entry(Arc::clone(&flag))
3717                .or_default()
3718                .push(ValueOrigin::DefaultMissing);
3719            if flag.var {
3720                // A repeated bare occurrence is still an occurrence. The string collection
3721                // uses an empty value for it, just as the concrete `default_missing` path
3722                // pushes one value per occurrence; otherwise bounds and consumers silently
3723                // lose every bare repeat after the first.
3724                flags
3725                    .entry(flag)
3726                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3727                    .try_as_multi_string_mut()
3728                    .unwrap()
3729                    .push(String::new());
3730            } else if variadic_value {
3731                // A variadic occurrence stays pending after each value. Reaching the next
3732                // flag (or EOF) closes that same occurrence; it must not erase what it took.
3733                flags
3734                    .entry(flag)
3735                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3736            } else {
3737                // A scalar pending here is a new bare occurrence. The normal permissive
3738                // repeat policy makes the later occurrence a correction, including a
3739                // correction from an explicit value back to the bare tri-state.
3740                flags.insert(flag, ParseValue::MultiString(Vec::new()));
3741            }
3742            return Ok(true);
3743        }
3744        None => return Ok(false),
3745    };
3746    if let Some(arg) = flag.arg.as_ref() {
3747        validate_choice_value(
3748            ChoiceTarget::option(flag),
3749            &value,
3750            arg.choices.as_ref(),
3751            custom_env,
3752        )?;
3753    }
3754    let flag = flag_awaiting_value.pop().unwrap();
3755    origins
3756        .entry(Arc::clone(&flag))
3757        .or_default()
3758        .push(ValueOrigin::DefaultMissing);
3759    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3760    if collecting {
3761        let arr = flags
3762            .entry(flag)
3763            .or_insert_with(|| ParseValue::MultiString(vec![]))
3764            .try_as_multi_string_mut()
3765            .unwrap();
3766        arr.push(value);
3767    } else {
3768        flags.insert(flag, ParseValue::String(value));
3769    }
3770    Ok(true)
3771}
3772
3773/// `bound` collects what each drained flag took, in the order it took it. The values are
3774/// the word after any `delimiter` split, which is the only place that split is known: by the
3775/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
3776/// second occurrence has appended to the same list.
3777#[allow(clippy::too_many_arguments)]
3778fn drain_pending_flag_values(
3779    spec: &Spec,
3780    cmd: &SpecCommand,
3781    errors: &mut Vec<UsageErr>,
3782    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3783    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3784    word: &mut String,
3785    custom_env: Option<&HashMap<String, String>>,
3786    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3787) -> miette::Result<bool> {
3788    while let Some(flag) = flag_awaiting_value.pop() {
3789        let arg = flag.arg.as_ref().unwrap();
3790        // Split before anything judges the word, because after the split it is no longer
3791        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
3792        // Judging first would reject the whole word against a list neither half is on.
3793        let parts: Vec<String> = match arg.delimiter {
3794            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3795            None => vec![std::mem::take(word)],
3796        };
3797        for part in &parts {
3798            if validate_choices(
3799                spec,
3800                cmd,
3801                errors,
3802                ChoiceTarget::option(&flag),
3803                part,
3804                arg.choices.as_ref(),
3805                custom_env,
3806            )? {
3807                return Ok(true);
3808            }
3809        }
3810        word.clear();
3811        bound.push((Arc::clone(&flag), parts.clone()));
3812        // Two ways to hold several values, and both record a list: a `var` flag
3813        // collects one per occurrence, a variadic argument collects several from one.
3814        if flag.var || arg.var {
3815            let arr = flags
3816                .entry(flag)
3817                .or_insert_with(|| ParseValue::MultiString(vec![]))
3818                .try_as_multi_string_mut()
3819                .unwrap();
3820            arr.extend(parts);
3821        } else {
3822            // Nowhere for a second value to go, so the word stands as it was typed. A
3823            // delimiter on a flag that takes one value is refused where it is written.
3824            flags.insert(
3825                flag,
3826                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3827            );
3828        }
3829    }
3830    Ok(false)
3831}
3832
3833fn choice_error(
3834    target: ChoiceTarget<'_>,
3835    value: &str,
3836    choices: Option<&SpecChoices>,
3837    custom_env: Option<&HashMap<String, String>>,
3838) -> Option<String> {
3839    let choices = choices?;
3840    if !choices.strict {
3841        return None;
3842    }
3843    let values = choices.values_with_env(custom_env);
3844    if choices.matches_with_env(value, custom_env) {
3845        return None;
3846    }
3847    if let Some(env) = choices.env() {
3848        if values.is_empty() {
3849            return Some(format!(
3850                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3851                target.kind, target.name,
3852            ));
3853        }
3854    }
3855    Some(format!(
3856        "Invalid choice for {} {}: {value}, expected one of {}",
3857        target.kind,
3858        target.name,
3859        values.join(", ")
3860    ))
3861}
3862
3863fn validate_choices(
3864    spec: &Spec,
3865    cmd: &SpecCommand,
3866    errors: &mut Vec<UsageErr>,
3867    target: ChoiceTarget<'_>,
3868    value: &str,
3869    choices: Option<&SpecChoices>,
3870    custom_env: Option<&HashMap<String, String>>,
3871) -> miette::Result<bool> {
3872    if is_help_arg(spec, cmd, value)
3873        && choices
3874            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3875    {
3876        errors.push(render_help_err(spec, cmd, value.len() > 2));
3877        return Ok(true);
3878    }
3879
3880    if let Some(err) = choice_error(target, value, choices, custom_env) {
3881        bail!("{err}");
3882    }
3883    Ok(false)
3884}
3885
3886fn validate_choice_value(
3887    target: ChoiceTarget<'_>,
3888    value: &str,
3889    choices: Option<&SpecChoices>,
3890    custom_env: Option<&HashMap<String, String>>,
3891) -> miette::Result<()> {
3892    if let Some(err) = choice_error(target, value, choices, custom_env) {
3893        bail!("{err}");
3894    }
3895    Ok(())
3896}
3897
3898fn validate_choice_values(
3899    target: ChoiceTarget<'_>,
3900    values: &[String],
3901    choices: Option<&SpecChoices>,
3902    custom_env: Option<&HashMap<String, String>>,
3903) -> miette::Result<()> {
3904    for value in values {
3905        validate_choice_value(target, value, choices, custom_env)?;
3906    }
3907    Ok(())
3908}
3909
3910/// Everything a parse records about where it stopped: the positional cursor, so callers that
3911/// do not re-run the parse — completions, above all — agree with it, and the token trace.
3912///
3913/// Every exit from the binding phase comes through here, which is what makes it the right
3914/// place to close the trace: whatever is still queued was never read, and saying so is more
3915/// useful than leaving those words out of the report entirely.
3916fn record_stop(
3917    out: &mut ParseOutput,
3918    next_arg_idx: usize,
3919    seen_double_dash: bool,
3920    trace: &mut Trace,
3921    unread: &VecDeque<Token>,
3922) {
3923    out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
3924    out.double_dash_seen = seen_double_dash;
3925    trace.close(unread);
3926    out.tokens = std::mem::take(&mut trace.tokens);
3927}
3928
3929/// Record that `arg` was handed a word before the `--` it requires.
3930///
3931/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
3932/// message is emitted only the first time each arg is seen. The set is also what suppresses the
3933/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
3934/// end of the parse.
3935fn report_double_dash_violation(
3936    arg: &SpecArg,
3937    errors: &mut Vec<UsageErr>,
3938    violations: &mut HashSet<String>,
3939) {
3940    if violations.insert(arg.name.clone()) {
3941        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
3942    }
3943}
3944
3945/// `--version` and `-V`, which the parser supplies where the spec declares a version.
3946///
3947/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
3948/// command tables — both of which accepted these spellings while this parser called them
3949/// unknown words. The help page has always listed `-V, --version` under the same
3950/// condition, and said in as many words that it did so "only where a version is
3951/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
3952/// page advertising a flag the parse refused.
3953///
3954/// The root only, because that is where the page lists it: `version` is a property of
3955/// the program, and a subcommand answering with the program's version is a claim no spec
3956/// made. A declared flag wins by arriving first — every scan consults this only after
3957/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
3958/// keeps `--version` supplied beside it.
3959fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
3960    (spec.version.is_some() || spec.long_version.is_some())
3961        && cmds.len() == 1
3962        && !spec.cmd.disable_version_flag
3963        && (w == "--version" || w == "-V")
3964}
3965
3966fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
3967    spec.disable_help != Some(true)
3968        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
3969            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
3970}
3971
3972impl ParseOutput {
3973    pub fn as_env(&self) -> BTreeMap<String, String> {
3974        let mut env = BTreeMap::new();
3975        for (flag, val) in &self.flags {
3976            let key = format!("usage_{}", flag.name.to_snake_case());
3977            let val = match val {
3978                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
3979                ParseValue::String(s) => s.clone(),
3980                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
3981                ParseValue::MultiString(s) => shell_words::join(s),
3982            };
3983            env.insert(key, val);
3984        }
3985        for (arg, val) in &self.args {
3986            let key = format!("usage_{}", arg.name.to_snake_case());
3987            env.insert(key, val.to_string());
3988        }
3989        env
3990    }
3991}
3992
3993impl Display for ParseValue {
3994    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3995        match self {
3996            ParseValue::Bool(b) => write!(f, "{b}"),
3997            ParseValue::String(s) => write!(f, "{s}"),
3998            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
3999            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
4000        }
4001    }
4002}
4003
4004/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
4005fn render_token(token: &TokenBinding) -> String {
4006    let roles = token.roles.iter().map(render_role).join(", ");
4007    let synthesized = if token.synthesized { " (read as)" } else { "" };
4008    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4009}
4010
4011fn render_role(role: &TokenRole) -> String {
4012    match role {
4013        TokenRole::Program => "program".to_string(),
4014        TokenRole::Command { name } => format!("subcommand {name}"),
4015        TokenRole::Flag {
4016            flag,
4017            spelling,
4018            negated,
4019        } => {
4020            let negated = if *negated { ", negated" } else { "" };
4021            format!("flag {} as {spelling}{negated}", flag.name)
4022        }
4023        TokenRole::Value {
4024            flag,
4025            values,
4026            attached,
4027        } => {
4028            let attached = if *attached { ", attached" } else { "" };
4029            format!("value of {} = {values:?}{attached}", flag.name)
4030        }
4031        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4032        TokenRole::Separator => "separator".to_string(),
4033        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4034        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4035        TokenRole::Restart => "restart".to_string(),
4036        TokenRole::UnknownFlag { bound_as } => match bound_as {
4037            Some(arg) => format!("unknown flag, bound as {}", arg.name),
4038            None => "unknown flag".to_string(),
4039        },
4040        TokenRole::Refused { reason } => format!("refused: {reason}"),
4041        TokenRole::External => "external".to_string(),
4042        TokenRole::Unread => "unread".to_string(),
4043    }
4044}
4045
4046impl Debug for ParseOutput {
4047    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4048        f.debug_struct("ParseOutput")
4049            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4050            .field(
4051                "args",
4052                &self
4053                    .args
4054                    .iter()
4055                    .map(|(a, w)| format!("{}: {w}", a.name))
4056                    .collect_vec(),
4057            )
4058            .field(
4059                "available_flags",
4060                &self
4061                    .available_flags
4062                    .iter()
4063                    .map(|(f, w)| format!("{f}: {w}"))
4064                    .collect_vec(),
4065            )
4066            .field(
4067                "flags",
4068                &self
4069                    .flags
4070                    .iter()
4071                    .map(|(f, w)| format!("{}: {w}", f.name))
4072                    .collect_vec(),
4073            )
4074            .field("flag_awaiting_value", &self.flag_awaiting_value)
4075            .field("errors", &self.errors)
4076            .field("external", &self.external)
4077            // Provenance, one line per token and one per fallback. This is the parser's
4078            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
4079            // first — `usage explain` renders the same facts for a reader.
4080            .field(
4081                "tokens",
4082                &self.tokens.iter().map(render_token).collect_vec(),
4083            )
4084            .field(
4085                "origins",
4086                &self
4087                    .flag_origins
4088                    .iter()
4089                    .map(|(f, o)| format!("{}: {o:?}", f.name))
4090                    .chain(
4091                        self.arg_origins
4092                            .iter()
4093                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
4094                    )
4095                    .collect_vec(),
4096            )
4097            .field("overridden_flags", &self.overridden_flags)
4098            .finish()
4099    }
4100}
4101
4102#[cfg(test)]
4103mod tests {
4104    use super::*;
4105    use crate::SpecFlagAction;
4106
4107    fn input(words: &[&str]) -> Vec<String> {
4108        words.iter().map(|word| (*word).to_string()).collect()
4109    }
4110
4111    #[test]
4112    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4113        // The page has always listed `-V, --version` wherever a `version` is declared,
4114        // and usage-argv and usage-go have always accepted both. This parser called them
4115        // unknown words, so the one implementation the corpus measures the others against
4116        // was the one that disagreed.
4117        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4118            .parse()
4119            .unwrap();
4120
4121        for spelling in ["--version", "-V"] {
4122            let err = parse(&spec, &input(&["ex", spelling]))
4123                .expect_err("answering with a version ends the parse");
4124            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4125        }
4126    }
4127
4128    #[test]
4129    fn the_supplied_version_flag_is_the_roots_alone() {
4130        // `version` describes the program, and the page lists the entry on the program's
4131        // own page only. A subcommand answering with it would be a claim no spec made.
4132        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4133            .parse()
4134            .unwrap();
4135
4136        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4137        assert_eq!(err.to_string(), "unexpected word: --version");
4138    }
4139
4140    #[test]
4141    fn no_declared_version_supplies_nothing() {
4142        // A `--version` answering with nothing is worse than one that is not there, which
4143        // is why the entry is conditional on the page and the spelling on the parse.
4144        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4145
4146        for spelling in ["--version", "-V"] {
4147            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4148            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4149        }
4150    }
4151
4152    #[test]
4153    fn disable_version_flag_removes_the_supplied_spellings() {
4154        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4155            .parse()
4156            .unwrap();
4157
4158        for spelling in ["--version", "-V"] {
4159            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4160            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4161        }
4162    }
4163
4164    #[test]
4165    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4166        // The page drops each supplied spelling the CLI claimed and keeps the other; the
4167        // parse agrees without being told, because a declared flag is matched first.
4168        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4169            .parse()
4170            .unwrap();
4171
4172        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4173        assert_eq!(out.flags.len(), 1);
4174
4175        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4176        assert_eq!(err.to_string(), "1.2.3");
4177    }
4178
4179    #[test]
4180    fn the_supplied_spellings_split_the_two_version_texts() {
4181        // The same split `render_action_err` gives a declared version flag: the long
4182        // spelling prefers `long_version`, the short prefers the concise one.
4183        let spec: Spec =
4184            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4185                .parse()
4186                .unwrap();
4187
4188        assert_eq!(
4189            parse(&spec, &input(&["ex", "--version"]))
4190                .unwrap_err()
4191                .to_string(),
4192            "1.2.3 (abcdef)"
4193        );
4194        assert_eq!(
4195            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4196            "1.2.3"
4197        );
4198    }
4199
4200    #[test]
4201    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4202        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
4203        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
4204        // naming nothing: usage-argv and usage-go both resolve the letter through the
4205        // same lookup that finds a declared short, and clap prints help for it too.
4206        let spec: Spec =
4207            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4208                .parse()
4209                .unwrap();
4210
4211        for token in ["-vh", "-hv"] {
4212            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4213            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4214        }
4215        for token in ["-vV", "-Vv"] {
4216            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4217            assert_eq!(err.to_string(), "1.2.3", "{token}");
4218        }
4219    }
4220
4221    #[test]
4222    fn a_bundled_help_letter_asks_for_the_short_page() {
4223        // Whatever else shares the token: `-h` is the short spelling, and the letters
4224        // beside it say nothing about which page was asked for.
4225        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"
4226            .parse()
4227            .unwrap();
4228
4229        let short = parse(&spec, &input(&["ex", "-vh"]))
4230            .unwrap_err()
4231            .to_string();
4232        let long = parse(&spec, &input(&["ex", "--help"]))
4233            .unwrap_err()
4234            .to_string();
4235        assert!(short.contains("Be loud"), "{short}");
4236        assert!(!short.contains("at length"), "{short}");
4237        assert!(long.contains("at length"), "{long}");
4238    }
4239
4240    #[test]
4241    fn the_bundled_version_letter_is_the_roots_alone() {
4242        // The same rule the whole-token spelling follows, asked one letter at a time.
4243        let spec: Spec =
4244            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4245                .parse()
4246                .unwrap();
4247
4248        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4249        assert_eq!(err.to_string(), "unexpected word: -vV");
4250    }
4251
4252    #[test]
4253    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4254        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
4255        // this spec's own `-h`, taking its value from the rest of the token.
4256        let spec: Spec =
4257            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4258                .parse()
4259                .unwrap();
4260
4261        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4262        assert_eq!(out.flags.len(), 2);
4263        assert!(out
4264            .flags
4265            .iter()
4266            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4267    }
4268
4269    #[test]
4270    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4271        let spec: Spec =
4272            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4273                .parse()
4274                .unwrap();
4275
4276        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4277        assert_eq!(err.to_string(), "unexpected word: -vh");
4278    }
4279
4280    #[test]
4281    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4282        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
4283        // on the way to discovering that `z` names nothing.
4284        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4285            .parse()
4286            .unwrap();
4287
4288        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4289        assert!(out.flags.is_empty(), "{:?}", out.flags);
4290        assert_eq!(out.args.len(), 1);
4291    }
4292
4293    fn spec_with_arg(arg: SpecArg) -> Spec {
4294        let cmd = SpecCommand::builder().name("test").arg(arg).build();
4295        Spec {
4296            name: "test".to_string(),
4297            bin: "test".to_string(),
4298            cmd,
4299            ..Default::default()
4300        }
4301    }
4302
4303    fn spec_with_flag(flag: SpecFlag) -> Spec {
4304        let cmd = SpecCommand::builder().name("test").flag(flag).build();
4305        Spec {
4306            name: "test".to_string(),
4307            bin: "test".to_string(),
4308            cmd,
4309            ..Default::default()
4310        }
4311    }
4312
4313    fn parse_with_env(
4314        spec: &Spec,
4315        words: &[&str],
4316        env: &[(&str, &str)],
4317    ) -> Result<ParseOutput, miette::Error> {
4318        let env = env
4319            .iter()
4320            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4321            .collect();
4322        Parser::new(spec).with_env(env).parse(&input(words))
4323    }
4324
4325    fn first_string_value(parsed: &ParseOutput) -> &str {
4326        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4327            return value;
4328        }
4329        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4330            return value;
4331        }
4332        panic!("expected first parsed value to be ParseValue::String");
4333    }
4334
4335    #[test]
4336    fn custom_environment_parser_dispatches_executable_views() {
4337        let spec: Spec = r#"
4338bin "ex"
4339view "runner" root="run"
4340cmd "run" {
4341    flag "--token <token>" env="TOKEN"
4342}
4343        "#
4344        .parse()
4345        .unwrap();
4346        let parsed = Parser::new(&spec)
4347            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4348            .parse(&input(&["runner"]))
4349            .unwrap();
4350
4351        assert_eq!(parsed.cmd.name, "runner");
4352        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4353            && matches!(value, ParseValue::String(value) if value == "secret")));
4354    }
4355
4356    #[test]
4357    fn an_executable_view_keeps_the_hosts_version_action() {
4358        let spec: Spec = r#"
4359bin "ex"
4360version "1.2.3"
4361flag "-V --version" action="version"
4362flag "--verbose" global=#true
4363view "runner" root="run" globals=#true
4364cmd "run"
4365        "#
4366        .parse()
4367        .unwrap();
4368
4369        let error = Parser::new(&spec)
4370            .parse(&input(&["runner", "--version"]))
4371            .expect_err("the host version action should answer before view projection");
4372        assert_eq!(error.to_string(), "1.2.3");
4373
4374        let error = Parser::new(&spec)
4375            .parse(&input(&["runner", "--verbose", "--version"]))
4376            .expect_err("the host version action should remain after a carried global");
4377        assert_eq!(error.to_string(), "1.2.3");
4378    }
4379
4380    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4381        let flag = parsed
4382            .flags
4383            .keys()
4384            .find(|flag| flag.name == name)
4385            .unwrap_or_else(|| panic!("expected flag {name}"));
4386        let value = parsed
4387            .flags
4388            .get(flag)
4389            .unwrap_or_else(|| panic!("expected value for flag {name}"));
4390        match value {
4391            ParseValue::String(value) => value,
4392            _ => panic!("expected flag {name} to be ParseValue::String"),
4393        }
4394    }
4395
4396    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4397        let err = result.expect_err("expected parser error");
4398        assert_eq!(format!("{err}"), expected);
4399    }
4400
4401    #[test]
4402    fn a_short_version_action_falls_back_to_the_long_version() {
4403        let flag = SpecFlag::builder()
4404            .short('R')
4405            .action(SpecFlagAction::Version)
4406            .build();
4407        let spec = Spec {
4408            name: "test".to_string(),
4409            bin: "test".to_string(),
4410            long_version: Some("1.2.3\ncommit abc123".to_string()),
4411            ..Default::default()
4412        };
4413        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4414            panic!("expected version action")
4415        };
4416        assert_eq!(version, "1.2.3\ncommit abc123");
4417    }
4418
4419    #[cfg(feature = "unstable_choices_env")]
4420    fn spec_arg_choices_env(key: &str) -> Spec {
4421        spec_with_arg(
4422            SpecArg::builder()
4423                .name("env")
4424                .choices_env(key)
4425                .required(false)
4426                .build(),
4427        )
4428    }
4429
4430    #[cfg(feature = "unstable_choices_env")]
4431    fn spec_flag_choices_env(key: &str) -> Spec {
4432        spec_with_flag(
4433            SpecFlag::builder()
4434                .long("env")
4435                .arg(SpecArg::builder().name("env").choices_env(key).build())
4436                .build(),
4437        )
4438    }
4439
4440    #[test]
4441    fn test_parse() {
4442        let cmd = SpecCommand::builder()
4443            .name("test")
4444            .arg(SpecArg::builder().name("arg").build())
4445            .flag(SpecFlag::builder().long("flag").build())
4446            .build();
4447        let spec = Spec {
4448            name: "test".to_string(),
4449            bin: "test".to_string(),
4450            cmd,
4451            ..Default::default()
4452        };
4453        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4454        let parsed = parse(&spec, &input).unwrap();
4455        assert_eq!(parsed.cmds.len(), 1);
4456        assert_eq!(parsed.cmds[0].name, "test");
4457        assert_eq!(parsed.args.len(), 1);
4458        assert_eq!(parsed.flags.len(), 1);
4459        assert_eq!(parsed.available_flags.len(), 1);
4460    }
4461
4462    #[test]
4463    fn test_flag_overrides_last_occurrence_wins() {
4464        let spec: Spec = r#"
4465flag "--stdin" default=#true
4466flag "--file <file>" overrides="--stdin"
4467        "#
4468        .parse()
4469        .unwrap();
4470
4471        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4472        assert_eq!(file_wins.flags.len(), 1);
4473        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4474        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4475
4476        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4477        assert_eq!(stdin_wins.flags.len(), 1);
4478        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4479        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4480    }
4481
4482    #[test]
4483    fn test_flag_override_clears_pending_value() {
4484        let spec: Spec = r#"
4485flag "--file <file>" overrides="--stdin"
4486flag "--stdin"
4487arg "[input]"
4488        "#
4489        .parse()
4490        .unwrap();
4491
4492        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4493        assert_eq!(parsed.flags.len(), 1);
4494        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4495        assert_eq!(first_string_value(&parsed), "input.txt");
4496    }
4497
4498    #[cfg(unix)]
4499    #[test]
4500    fn a_mount_on_the_root_discovers_subcommands() {
4501        // The root is a command like any other, so it can find its own subcommands
4502        // by running something. Uses `echo` rather than a fixture because resolving
4503        // a mount is what is being tested.
4504        let spec: Spec = r#"
4505name "ex"
4506bin "ex"
4507cmd "declared"
4508mount run="echo 'cmd \"discovered\"'"
4509"#
4510        .parse()
4511        .unwrap();
4512
4513        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4514        assert_eq!(out.cmd.name, "discovered");
4515    }
4516
4517    #[test]
4518    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4519        let spec: Spec = r#"
4520name "ex"
4521bin "ex"
4522mount run="this command must never run"
4523cmd "declared"
4524"#
4525        .parse()
4526        .unwrap();
4527
4528        Parser::new(&spec)
4529            .with_mount_outputs(HashMap::new())
4530            .parse(&input(&["ex", "declared"]))
4531            .expect("a declared command does not resolve the mount");
4532
4533        let error = Parser::new(&spec)
4534            .with_mount_outputs(HashMap::new())
4535            .parse(&input(&["ex", "discovered"]))
4536            .unwrap_err();
4537        assert!(
4538            error
4539                .to_string()
4540                .contains("No injected output was provided for mount command"),
4541            "{error}"
4542        );
4543    }
4544
4545    #[cfg(unix)]
4546    #[test]
4547    fn completion_sees_root_mounted_commands_with_nothing_typed() {
4548        // The case a root mount exists for. `mycli <tab>` has no word to trigger
4549        // discovery with, so a completion has to resolve up front or the mounted
4550        // commands are never offered.
4551        let spec: Spec = r#"
4552name "ex"
4553bin "ex"
4554cmd "declared"
4555mount run="echo 'cmd \"discovered\"'"
4556"#
4557        .parse()
4558        .unwrap();
4559
4560        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4561        assert!(
4562            out.cmd.subcommands.contains_key("discovered"),
4563            "a completion should see mounted commands; got {:?}",
4564            out.cmd.subcommands.keys().collect::<Vec<_>>()
4565        );
4566    }
4567
4568    #[cfg(unix)]
4569    #[test]
4570    fn completion_and_execution_agree_about_discovery() {
4571        // Offering a command that a real parse would hand to the default instead is
4572        // worse than not offering it, so the gate applies to both paths. The mount
4573        // fails if it runs, which is how both halves are checked at once.
4574        let spec: Spec = r#"
4575name "ex"
4576bin "ex"
4577default_subcommand "run"
4578cmd "run" {
4579  arg "<task>"
4580}
4581mount run="exit 1"
4582"#
4583        .parse()
4584        .unwrap();
4585
4586        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4587        assert!(
4588            !out.cmd.subcommands.contains_key("discovered"),
4589            "a completion must not offer what execution will not route"
4590        );
4591
4592        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4593        assert_eq!(out.cmd.name, "run");
4594    }
4595
4596    #[cfg(unix)]
4597    #[test]
4598    fn a_default_subcommand_outranks_discovery() {
4599        // The default already says what an unmatched word means, and says it for
4600        // free. The mount fails if it runs, so parsing proves discovery was skipped.
4601        let spec: Spec = r#"
4602name "ex"
4603bin "ex"
4604default_subcommand "run"
4605cmd "run" {
4606  arg "<task>"
4607}
4608mount run="exit 1"
4609"#
4610        .parse()
4611        .unwrap();
4612
4613        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4614        assert_eq!(out.cmd.name, "run");
4615    }
4616
4617    #[cfg(unix)]
4618    #[test]
4619    fn a_mount_may_ask_to_outrank_the_default() {
4620        // Opting in, and paying for it: discovery runs first, so a discovered
4621        // command wins over the fallback.
4622        let spec: Spec = r#"
4623name "ex"
4624bin "ex"
4625default_subcommand "run"
4626cmd "run" {
4627  arg "<task>"
4628}
4629mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4630"#
4631        .parse()
4632        .unwrap();
4633
4634        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4635        assert_eq!(out.cmd.name, "discovered");
4636
4637        // A word it does not know still reaches the default.
4638        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4639        assert_eq!(out.cmd.name, "run");
4640    }
4641
4642    #[cfg(unix)]
4643    #[test]
4644    fn a_flag_does_not_run_the_mount() {
4645        // A flag matches no subcommand, which would have been enough to trigger
4646        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
4647        // so parsing at all is the proof that it did not.
4648        let spec: Spec = r#"
4649name "ex"
4650bin "ex"
4651flag "--verbose"
4652cmd "declared"
4653mount run="exit 1"
4654"#
4655        .parse()
4656        .unwrap();
4657
4658        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4659        assert_eq!(out.cmd.name, "ex");
4660    }
4661
4662    #[cfg(unix)]
4663    #[test]
4664    fn a_declared_subcommand_does_not_run_the_mount() {
4665        // The mount would fail if it ran, so this parsing at all is the proof that
4666        // discovery is skipped when the word is already known. Worth pinning: a root
4667        // mount that resolved eagerly would spawn a process on every invocation.
4668        let spec: Spec = r#"
4669name "ex"
4670bin "ex"
4671cmd "declared"
4672mount run="exit 1"
4673"#
4674        .parse()
4675        .unwrap();
4676
4677        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4678        assert_eq!(out.cmd.name, "declared");
4679    }
4680
4681    #[test]
4682    fn a_root_mount_survives_being_written_out() {
4683        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4684            .parse()
4685            .unwrap();
4686        assert_eq!(spec.cmd.mounts.len(), 1);
4687
4688        let reparsed: Spec = spec.to_string().parse().unwrap();
4689        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4690        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4691    }
4692
4693    #[test]
4694    fn test_mount_prefix_applies_flag_overrides() {
4695        let stdin = Arc::new(
4696            SpecFlag::builder()
4697                .name("stdin")
4698                .long("stdin")
4699                .global(true)
4700                .build(),
4701        );
4702        let file = Arc::new(
4703            SpecFlag::builder()
4704                .name("file")
4705                .long("file")
4706                .arg(SpecArg::builder().name("file").build())
4707                .global(true)
4708                .overrides_with(vec!["--stdin".to_string()])
4709                .build(),
4710        );
4711        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4712
4713        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4714        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4715
4716        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4717    }
4718
4719    #[test]
4720    fn test_flag_override_suppresses_env_value() {
4721        let spec: Spec = r#"
4722flag "--stdin" env="USE_STDIN"
4723flag "--file <file>" overrides="--stdin"
4724        "#
4725        .parse()
4726        .unwrap();
4727
4728        let parsed = parse_with_env(
4729            &spec,
4730            &["test", "--file", "input.txt"],
4731            &[("USE_STDIN", "true")],
4732        )
4733        .unwrap();
4734        assert_eq!(parsed.flags.len(), 1);
4735        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4736    }
4737
4738    #[test]
4739    fn test_flag_override_suppresses_required_check() {
4740        let spec: Spec = r#"
4741flag "--stdin" required=#true
4742flag "--file <file>" overrides="--stdin"
4743        "#
4744        .parse()
4745        .unwrap();
4746
4747        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4748        assert_eq!(parsed.flags.len(), 1);
4749        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4750    }
4751
4752    #[test]
4753    fn test_flag_required_if() {
4754        let spec: Spec = r#"
4755flag "--dir <dir>"
4756flag "--file <file>" required_if="--dir"
4757        "#
4758        .parse()
4759        .unwrap();
4760
4761        parse(&spec, &input(&["test"])).unwrap();
4762        assert_parse_err(
4763            parse(&spec, &input(&["test", "--dir", "src"])),
4764            "Missing required flag: --file <file>",
4765        );
4766        parse(
4767            &spec,
4768            &input(&["test", "--dir", "src", "--file", "input.txt"]),
4769        )
4770        .unwrap();
4771    }
4772
4773    #[test]
4774    fn test_flag_required_unless() {
4775        let spec: Spec = r#"
4776flag "--stdin"
4777flag "--file <file>" required_unless="--stdin"
4778        "#
4779        .parse()
4780        .unwrap();
4781
4782        assert_parse_err(
4783            parse(&spec, &input(&["test"])),
4784            "Missing required flag: --file <file>",
4785        );
4786        parse(&spec, &input(&["test", "--stdin"])).unwrap();
4787        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4788    }
4789
4790    #[test]
4791    fn complete_required_relationship_truth_tables() {
4792        let spec: Spec = r#"
4793name "ex"
4794bin "ex"
4795flag "--mode <mode>"
4796flag "--scope <scope>"
4797flag "--token <token>" {
4798    required_if_eq "--mode" "remote"
4799}
4800flag "--approval <approval>" {
4801    required_if_eq_all "--mode" "remote" "--scope" "global"
4802}
4803flag "--input <input>" {
4804    required_unless "--stdin" "--file"
4805}
4806flag "--checksum <checksum>" {
4807    required_unless_all "--stdin" "--file"
4808}
4809flag "--stdin"
4810flag "--file <file>"
4811arg "[request]" {
4812    requires "--mode" "--scope"
4813}
4814"#
4815        .parse()
4816        .unwrap();
4817        let parse_args = |args: &[&str]| {
4818            parse(
4819                &spec,
4820                &args
4821                    .iter()
4822                    .map(|arg| (*arg).to_string())
4823                    .collect::<Vec<_>>(),
4824            )
4825        };
4826
4827        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4828        assert!(parse_args(&[
4829            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4830        ])
4831        .is_err());
4832        parse_args(&[
4833            "ex",
4834            "--mode",
4835            "remote",
4836            "--token",
4837            "secret",
4838            "--scope",
4839            "global",
4840            "--approval",
4841            "yes",
4842            "--stdin",
4843            "--file",
4844            "in",
4845        ])
4846        .unwrap();
4847        parse_args(&[
4848            "ex",
4849            "--mode",
4850            "local",
4851            "--scope",
4852            "project",
4853            "--stdin",
4854            "--checksum",
4855            "sum",
4856            "request.json",
4857        ])
4858        .unwrap();
4859
4860        let reparsed: Spec = spec.to_string().parse().unwrap();
4861        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4862        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4863        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4864        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4865    }
4866
4867    #[test]
4868    fn test_conditional_requirements_treat_env_as_explicit() {
4869        let spec: Spec = r#"
4870flag "--dir <dir>" env="INPUT_DIR"
4871flag "--stdin" env="USE_STDIN"
4872flag "--file <file>" required_if="--dir" required_unless="--stdin"
4873        "#
4874        .parse()
4875        .unwrap();
4876
4877        assert_parse_err(
4878            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
4879            "Missing required flag: --file <file>",
4880        );
4881        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
4882    }
4883
4884    #[test]
4885    fn test_custom_env_does_not_fall_back_to_process_env() {
4886        assert!(std::env::var("PATH").is_ok());
4887        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
4888
4889        assert_parse_err(
4890            parse_with_env(&spec, &["test"], &[]),
4891            "Missing required flag: --file <file>",
4892        );
4893    }
4894
4895    #[test]
4896    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
4897        let spec: Spec = r#"
4898flag "--dir <dir>" default="src"
4899flag "--file <file>" required_if="--dir"
4900        "#
4901        .parse()
4902        .unwrap();
4903
4904        parse(&spec, &input(&["test"])).unwrap();
4905    }
4906
4907    #[test]
4908    fn test_conditional_requirements_see_overridden_flags_as_absent() {
4909        let spec: Spec = r#"
4910flag "--stdin"
4911flag "--dir <dir>" overrides="--stdin"
4912flag "--file <file>" required_unless="--stdin"
4913        "#
4914        .parse()
4915        .unwrap();
4916
4917        assert_parse_err(
4918            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
4919            "Missing required flag: --file <file>",
4920        );
4921    }
4922
4923    #[test]
4924    fn short_flag_is_one_character_not_one_byte() {
4925        // A short is declared and read by character. Counting bytes instead either
4926        // refuses the declaration or slices the token inside the character, and clap
4927        // — which many specs are generated from — accepts shorts like this one.
4928        let spec = spec_with_flag(
4929            SpecFlag::builder()
4930                .short('磨')
4931                .long("polish")
4932                .arg(SpecArg::builder().name("opt").build())
4933                .build(),
4934        );
4935        let attached = Parser::new(&spec)
4936            .parse(&input(&["test", "-磨VALUE"]))
4937            .unwrap();
4938        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
4939        let detached = Parser::new(&spec)
4940            .parse(&input(&["test", "-磨", "V"]))
4941            .unwrap();
4942        assert_eq!(flag_string_value(&detached, "polish"), "V");
4943    }
4944
4945    #[test]
4946    fn test_as_env() {
4947        let cmd = SpecCommand::builder()
4948            .name("test")
4949            .arg(SpecArg::builder().name("arg").build())
4950            .flag(SpecFlag::builder().long("flag").build())
4951            .flag(
4952                SpecFlag::builder()
4953                    .long("force")
4954                    .negate("--no-force")
4955                    .build(),
4956            )
4957            .build();
4958        let spec = Spec {
4959            name: "test".to_string(),
4960            bin: "test".to_string(),
4961            cmd,
4962            ..Default::default()
4963        };
4964        let input = vec![
4965            "test".to_string(),
4966            "--flag".to_string(),
4967            "--no-force".to_string(),
4968        ];
4969        let parsed = parse(&spec, &input).unwrap();
4970        let env = parsed.as_env();
4971        assert_eq!(env.len(), 2);
4972        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
4973        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
4974    }
4975
4976    #[test]
4977    fn test_arg_env_var() {
4978        let cmd = SpecCommand::builder()
4979            .name("test")
4980            .arg(
4981                SpecArg::builder()
4982                    .name("input")
4983                    .env("TEST_ARG_INPUT")
4984                    .required(true)
4985                    .build(),
4986            )
4987            .build();
4988        let spec = Spec {
4989            name: "test".to_string(),
4990            bin: "test".to_string(),
4991            cmd,
4992            ..Default::default()
4993        };
4994
4995        // Set env var
4996        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
4997
4998        let input = vec!["test".to_string()];
4999        let parsed = parse(&spec, &input).unwrap();
5000
5001        assert_eq!(parsed.args.len(), 1);
5002        let arg = parsed.args.keys().next().unwrap();
5003        assert_eq!(arg.name, "input");
5004        let value = parsed.args.values().next().unwrap();
5005        assert_eq!(value.to_string(), "test_file.txt");
5006
5007        // Clean up
5008        std::env::remove_var("TEST_ARG_INPUT");
5009    }
5010
5011    #[test]
5012    fn test_flag_env_var_with_arg() {
5013        let cmd = SpecCommand::builder()
5014            .name("test")
5015            .flag(
5016                SpecFlag::builder()
5017                    .long("output")
5018                    .env("TEST_FLAG_OUTPUT")
5019                    .arg(SpecArg::builder().name("file").build())
5020                    .build(),
5021            )
5022            .build();
5023        let spec = Spec {
5024            name: "test".to_string(),
5025            bin: "test".to_string(),
5026            cmd,
5027            ..Default::default()
5028        };
5029
5030        // Set env var
5031        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5032
5033        let input = vec!["test".to_string()];
5034        let parsed = parse(&spec, &input).unwrap();
5035
5036        assert_eq!(parsed.flags.len(), 1);
5037        let flag = parsed.flags.keys().next().unwrap();
5038        assert_eq!(flag.name, "output");
5039        let value = parsed.flags.values().next().unwrap();
5040        assert_eq!(value.to_string(), "output.txt");
5041
5042        // Clean up
5043        std::env::remove_var("TEST_FLAG_OUTPUT");
5044    }
5045
5046    #[test]
5047    fn test_flag_env_var_boolean() {
5048        let cmd = SpecCommand::builder()
5049            .name("test")
5050            .flag(
5051                SpecFlag::builder()
5052                    .long("verbose")
5053                    .env("TEST_FLAG_VERBOSE")
5054                    .build(),
5055            )
5056            .build();
5057        let spec = Spec {
5058            name: "test".to_string(),
5059            bin: "test".to_string(),
5060            cmd,
5061            ..Default::default()
5062        };
5063
5064        // Set env var to true
5065        std::env::set_var("TEST_FLAG_VERBOSE", "true");
5066
5067        let input = vec!["test".to_string()];
5068        let parsed = parse(&spec, &input).unwrap();
5069
5070        assert_eq!(parsed.flags.len(), 1);
5071        let flag = parsed.flags.keys().next().unwrap();
5072        assert_eq!(flag.name, "verbose");
5073        let value = parsed.flags.values().next().unwrap();
5074        assert_eq!(value.to_string(), "true");
5075
5076        // Clean up
5077        std::env::remove_var("TEST_FLAG_VERBOSE");
5078    }
5079
5080    #[test]
5081    fn test_env_var_precedence() {
5082        // CLI args should take precedence over env vars
5083        let cmd = SpecCommand::builder()
5084            .name("test")
5085            .arg(
5086                SpecArg::builder()
5087                    .name("input")
5088                    .env("TEST_PRECEDENCE_INPUT")
5089                    .required(true)
5090                    .build(),
5091            )
5092            .build();
5093        let spec = Spec {
5094            name: "test".to_string(),
5095            bin: "test".to_string(),
5096            cmd,
5097            ..Default::default()
5098        };
5099
5100        // Set env var
5101        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5102
5103        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5104        let parsed = parse(&spec, &input).unwrap();
5105
5106        assert_eq!(parsed.args.len(), 1);
5107        let value = parsed.args.values().next().unwrap();
5108        // CLI arg should take precedence
5109        assert_eq!(value.to_string(), "cli_file.txt");
5110
5111        // Clean up
5112        std::env::remove_var("TEST_PRECEDENCE_INPUT");
5113    }
5114
5115    #[test]
5116    fn test_flag_var_true_with_single_default() {
5117        // When var=true and default="bar", the default should be MultiString(["bar"])
5118        let cmd = SpecCommand::builder()
5119            .name("test")
5120            .flag(
5121                SpecFlag::builder()
5122                    .long("foo")
5123                    .var(true)
5124                    .arg(SpecArg::builder().name("foo").build())
5125                    .default_value("bar")
5126                    .build(),
5127            )
5128            .build();
5129        let spec = Spec {
5130            name: "test".to_string(),
5131            bin: "test".to_string(),
5132            cmd,
5133            ..Default::default()
5134        };
5135
5136        // User doesn't provide the flag
5137        let input = vec!["test".to_string()];
5138        let parsed = parse(&spec, &input).unwrap();
5139
5140        assert_eq!(parsed.flags.len(), 1);
5141        let flag = parsed.flags.keys().next().unwrap();
5142        assert_eq!(flag.name, "foo");
5143        let value = parsed.flags.values().next().unwrap();
5144        // Should be MultiString, not String
5145        match value {
5146            ParseValue::MultiString(v) => {
5147                assert_eq!(v.len(), 1);
5148                assert_eq!(v[0], "bar");
5149            }
5150            _ => panic!("Expected MultiString, got {:?}", value),
5151        }
5152    }
5153
5154    #[test]
5155    fn test_flag_var_true_with_multiple_defaults() {
5156        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
5157        let cmd = SpecCommand::builder()
5158            .name("test")
5159            .flag(
5160                SpecFlag::builder()
5161                    .long("foo")
5162                    .var(true)
5163                    .arg(SpecArg::builder().name("foo").build())
5164                    .default_values(["xyz", "bar"])
5165                    .build(),
5166            )
5167            .build();
5168        let spec = Spec {
5169            name: "test".to_string(),
5170            bin: "test".to_string(),
5171            cmd,
5172            ..Default::default()
5173        };
5174
5175        // User doesn't provide the flag
5176        let input = vec!["test".to_string()];
5177        let parsed = parse(&spec, &input).unwrap();
5178
5179        assert_eq!(parsed.flags.len(), 1);
5180        let value = parsed.flags.values().next().unwrap();
5181        // Should be MultiString with both values
5182        match value {
5183            ParseValue::MultiString(v) => {
5184                assert_eq!(v.len(), 2);
5185                assert_eq!(v[0], "xyz");
5186                assert_eq!(v[1], "bar");
5187            }
5188            _ => panic!("Expected MultiString, got {:?}", value),
5189        }
5190    }
5191
5192    #[test]
5193    fn test_flag_var_false_with_default_remains_string() {
5194        // When var=false (default), the default should still be String("bar")
5195        let cmd = SpecCommand::builder()
5196            .name("test")
5197            .flag(
5198                SpecFlag::builder()
5199                    .long("foo")
5200                    .var(false) // Default behavior
5201                    .arg(SpecArg::builder().name("foo").build())
5202                    .default_value("bar")
5203                    .build(),
5204            )
5205            .build();
5206        let spec = Spec {
5207            name: "test".to_string(),
5208            bin: "test".to_string(),
5209            cmd,
5210            ..Default::default()
5211        };
5212
5213        // User doesn't provide the flag
5214        let input = vec!["test".to_string()];
5215        let parsed = parse(&spec, &input).unwrap();
5216
5217        assert_eq!(parsed.flags.len(), 1);
5218        let value = parsed.flags.values().next().unwrap();
5219        // Should be String, not MultiString
5220        match value {
5221            ParseValue::String(s) => {
5222                assert_eq!(s, "bar");
5223            }
5224            _ => panic!("Expected String, got {:?}", value),
5225        }
5226    }
5227
5228    #[test]
5229    fn test_arg_var_true_with_single_default() {
5230        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
5231        let cmd = SpecCommand::builder()
5232            .name("test")
5233            .arg(
5234                SpecArg::builder()
5235                    .name("files")
5236                    .var(true)
5237                    .default_value("default.txt")
5238                    .required(false)
5239                    .build(),
5240            )
5241            .build();
5242        let spec = Spec {
5243            name: "test".to_string(),
5244            bin: "test".to_string(),
5245            cmd,
5246            ..Default::default()
5247        };
5248
5249        // User doesn't provide the arg
5250        let input = vec!["test".to_string()];
5251        let parsed = parse(&spec, &input).unwrap();
5252
5253        assert_eq!(parsed.args.len(), 1);
5254        let value = parsed.args.values().next().unwrap();
5255        // Should be MultiString, not String
5256        match value {
5257            ParseValue::MultiString(v) => {
5258                assert_eq!(v.len(), 1);
5259                assert_eq!(v[0], "default.txt");
5260            }
5261            _ => panic!("Expected MultiString, got {:?}", value),
5262        }
5263    }
5264
5265    #[test]
5266    fn test_arg_var_true_with_multiple_defaults() {
5267        // When arg has var=true and multiple defaults
5268        let cmd = SpecCommand::builder()
5269            .name("test")
5270            .arg(
5271                SpecArg::builder()
5272                    .name("files")
5273                    .var(true)
5274                    .default_values(["file1.txt", "file2.txt"])
5275                    .required(false)
5276                    .build(),
5277            )
5278            .build();
5279        let spec = Spec {
5280            name: "test".to_string(),
5281            bin: "test".to_string(),
5282            cmd,
5283            ..Default::default()
5284        };
5285
5286        // User doesn't provide the arg
5287        let input = vec!["test".to_string()];
5288        let parsed = parse(&spec, &input).unwrap();
5289
5290        assert_eq!(parsed.args.len(), 1);
5291        let value = parsed.args.values().next().unwrap();
5292        // Should be MultiString with both values
5293        match value {
5294            ParseValue::MultiString(v) => {
5295                assert_eq!(v.len(), 2);
5296                assert_eq!(v[0], "file1.txt");
5297                assert_eq!(v[1], "file2.txt");
5298            }
5299            _ => panic!("Expected MultiString, got {:?}", value),
5300        }
5301    }
5302
5303    #[test]
5304    fn test_arg_var_false_with_default_remains_string() {
5305        // When arg has var=false (default), the default should still be String
5306        let cmd = SpecCommand::builder()
5307            .name("test")
5308            .arg(
5309                SpecArg::builder()
5310                    .name("file")
5311                    .var(false)
5312                    .default_value("default.txt")
5313                    .required(false)
5314                    .build(),
5315            )
5316            .build();
5317        let spec = Spec {
5318            name: "test".to_string(),
5319            bin: "test".to_string(),
5320            cmd,
5321            ..Default::default()
5322        };
5323
5324        // User doesn't provide the arg
5325        let input = vec!["test".to_string()];
5326        let parsed = parse(&spec, &input).unwrap();
5327
5328        assert_eq!(parsed.args.len(), 1);
5329        let value = parsed.args.values().next().unwrap();
5330        // Should be String, not MultiString
5331        match value {
5332            ParseValue::String(s) => {
5333                assert_eq!(s, "default.txt");
5334            }
5335            _ => panic!("Expected String, got {:?}", value),
5336        }
5337    }
5338
5339    #[test]
5340    fn test_scalar_defaults_validate_only_first_default_choice() {
5341        let specs = [
5342            spec_with_arg(
5343                SpecArg::builder()
5344                    .name("env")
5345                    .var(false)
5346                    .default_values(["dev", "prod"])
5347                    .choices(["dev"])
5348                    .required(false)
5349                    .build(),
5350            ),
5351            spec_with_flag(
5352                SpecFlag::builder()
5353                    .long("env")
5354                    .arg(
5355                        SpecArg::builder()
5356                            .name("env")
5357                            .default_values(["dev", "prod"])
5358                            .choices(["dev"])
5359                            .build(),
5360                    )
5361                    .build(),
5362            ),
5363        ];
5364
5365        for spec in specs {
5366            let parsed = parse(&spec, &input(&["test"])).unwrap();
5367            assert_eq!(first_string_value(&parsed), "dev");
5368        }
5369    }
5370
5371    #[test]
5372    fn a_delimiter_turns_one_word_into_several_values() {
5373        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5374            .parse()
5375            .unwrap();
5376
5377        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5378        let multi = |value: &ParseValue| match value {
5379            ParseValue::MultiString(values) => values.clone(),
5380            other => panic!("expected several values, got {other:?}"),
5381        };
5382        let tags = parsed
5383            .flags
5384            .iter()
5385            .find(|(f, _)| f.name == "tags")
5386            .map(|(_, v)| v)
5387            .unwrap();
5388        assert_eq!(multi(tags), vec!["a", "b", "c"]);
5389        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5390    }
5391
5392    #[test]
5393    fn a_positional_splits_before_its_choices_are_asked() {
5394        // The flag path did this and the positional path did not, so a word whose parts
5395        // were all choices was rejected as one value, and a bad half was reported as the
5396        // whole word.
5397        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
5398            .parse()
5399            .unwrap();
5400
5401        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5402
5403        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5404        let message = err.to_string();
5405        assert!(message.contains("nowhere"), "{message}");
5406        assert!(
5407            !message.contains("src:nowhere"),
5408            "the bad half should be named, not the whole word: {message}"
5409        );
5410    }
5411
5412    #[test]
5413    fn a_split_value_is_counted_and_judged_as_values() {
5414        // Split during the parse rather than after it, so everything downstream sees the
5415        // values the user meant rather than the words they typed: `choices` judges each
5416        // one, and the bounds count them.
5417        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
5418            .parse()
5419            .unwrap();
5420
5421        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5422        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5423        assert!(err.to_string().contains("staging"), "{err}");
5424        assert!(
5425            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5426            "three values should breach var_max=2"
5427        );
5428    }
5429
5430    #[test]
5431    fn a_split_bound_counts_one_occurrence_at_a_time() {
5432        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
5433        // delimiter the collection simply stops at it, so it could never be exceeded; a word
5434        // carrying several values can carry an occurrence past it in one step, and that is
5435        // the only way this bound is ever breached.
5436        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
5437            .parse()
5438            .unwrap();
5439
5440        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5441        assert!(
5442            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5443            "three values out of one word is still three values"
5444        );
5445        // The rule the corpus documents for plain words, on split ones: a second occurrence
5446        // starts counting again rather than adding to the first.
5447        parse(
5448            &spec,
5449            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5450        )
5451        .expect("two per occurrence, twice, is within the bound");
5452    }
5453
5454    #[test]
5455    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5456        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5457            .parse()
5458            .unwrap();
5459
5460        parse(
5461            &spec,
5462            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5463        )
5464        .expect("each occurrence satisfies the bound independently");
5465
5466        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5467        assert!(
5468            error
5469                .to_string()
5470                .contains("requires at least 2 value(s), got 1"),
5471            "{error:?}"
5472        );
5473    }
5474
5475    #[test]
5476    fn an_exclusive_flag_has_to_be_alone() {
5477        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5478            .parse()
5479            .unwrap();
5480
5481        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5482
5483        // Any other flag.
5484        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5485        assert!(err.to_string().contains("on its own"), "{err}");
5486
5487        // And a positional, which is what makes this more than a conflict with every
5488        // other flag.
5489        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5490        assert!(err.to_string().contains("on its own"), "{err}");
5491
5492        // Not given, so it imposes nothing.
5493        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5494    }
5495
5496    #[test]
5497    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5498        // Only what was supplied counts, as `conflicts` reads it. A default counting as
5499        // company would make an exclusive flag unusable on any command that has one.
5500        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5501            .parse()
5502            .unwrap();
5503
5504        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5505        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5506    }
5507
5508    #[test]
5509    fn an_exclusive_flag_bypasses_required_siblings() {
5510        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5511            .parse()
5512            .unwrap();
5513
5514        parse(&spec, &input(&["ex", "--dump"]))
5515            .expect("exclusive is the command's requiredness escape");
5516        assert!(parse(
5517            &spec,
5518            &input(&["ex", "--dump", "--out", "somewhere", "target"])
5519        )
5520        .is_err());
5521    }
5522
5523    #[test]
5524    fn an_environment_value_counts_for_an_exclusive_flag() {
5525        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5526            .parse()
5527            .unwrap();
5528
5529        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5530        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5531    }
5532
5533    #[test]
5534    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5535        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5536            .parse()
5537            .unwrap();
5538
5539        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5540        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5541    }
5542
5543    #[test]
5544    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5545        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5546            .parse()
5547            .unwrap();
5548
5549        parse(&spec, &input(&["ex", "run", "--clean"]))
5550            .expect("the child flag is alone within the child command");
5551        assert!(
5552            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5553            "the parent flag still conflicts with selecting the child"
5554        );
5555    }
5556
5557    #[test]
5558    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5559        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5560            .parse()
5561            .unwrap();
5562
5563        parse(&spec, &input(&["ex", "run", "--clean"]))
5564            .expect("the child-local exclusive flag is alone inside the child command");
5565        assert!(
5566            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5567            "the ancestor spelling still conflicts with selecting the child"
5568        );
5569    }
5570
5571    #[test]
5572    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5573        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
5574            .parse()
5575            .unwrap();
5576
5577        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5578        assert!(
5579            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5580            "the distinct parent declaration is still company despite sharing a name"
5581        );
5582    }
5583
5584    #[test]
5585    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5586        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5587            .parse()
5588            .unwrap();
5589
5590        parse(&spec, &input(&["ex", "run", "--clean"]))
5591            .expect("the child exclusive flag is valid alone");
5592        assert!(
5593            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5594            "merging with the inherited global must not discard child exclusivity"
5595        );
5596    }
5597
5598    #[test]
5599    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5600        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5601            .parse()
5602            .unwrap();
5603
5604        parse(&spec, &input(&["ex", "run", "--clean"]))
5605            .expect("the typed long form belongs to the child declaration");
5606        assert!(
5607            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5608            "the inherited short form still belongs to the ancestor"
5609        );
5610        assert!(
5611            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5612            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5613        );
5614    }
5615
5616    #[test]
5617    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5618        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
5619            .parse()
5620            .unwrap();
5621
5622        parse(&spec, &input(&["ex", "run", "--clean"]))
5623            .expect("the child's spelling does not activate the orphan ancestor alias");
5624        assert!(
5625            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5626            "the inherited short alias still belongs to the ancestor exclusive flag"
5627        );
5628    }
5629
5630    #[test]
5631    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5632        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"
5633            .parse()
5634            .unwrap();
5635
5636        assert!(
5637            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5638            "the inherited negated alias still belongs to the ancestor exclusive flag"
5639        );
5640    }
5641
5642    #[test]
5643    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5644        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
5645        // an unrelated inherited global already owns. That collision is resolved in the other
5646        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
5647        // the `--clean` it declared, and its exclusivity holds.
5648        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"
5649            .parse()
5650            .unwrap();
5651
5652        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5653        assert!(
5654            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5655            "one unrelated alias collision cannot disown the child from its own flag"
5656        );
5657    }
5658
5659    #[test]
5660    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5661        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
5662        // subcommand word the flag can only be the ancestor's, because that is the only one in
5663        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
5664        // is set. The pair below differ in nothing else, which is what makes this one rule
5665        // rather than two behaviors.
5666        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5667            .parse()
5668            .unwrap();
5669        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5670            .expect("the ancestor owns this occurrence, and it is not exclusive");
5671        assert!(
5672            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5673            "after the subcommand word the child's declaration is in scope, and it is exclusive"
5674        );
5675
5676        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5677            .parse()
5678            .unwrap();
5679        assert!(
5680            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5681            "the same rule, with an exclusive ancestor: selecting the child is company for it"
5682        );
5683    }
5684
5685    #[test]
5686    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5687        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
5688        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
5689        // the ancestor ever declared, so the ancestor's answer still governs it.
5690        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
5691            .parse()
5692            .unwrap();
5693
5694        assert!(
5695            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5696            "the orphan ancestor alias is still the ancestor's exclusive flag"
5697        );
5698        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5699            .expect("the child's own spelling drops the exclusivity the child did not restate");
5700    }
5701
5702    #[test]
5703    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5704        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
5705        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
5706        // occurrence to the ancestor because `-c` appeared in it lost that.
5707        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5708            .parse()
5709            .unwrap();
5710
5711        assert!(
5712            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5713            "the child spelling is exclusive whatever it was typed beside"
5714        );
5715        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5716            .expect("the ancestor's own spelling was never exclusive");
5717    }
5718
5719    #[test]
5720    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5721        // An environment value has no spelling to attribute, so the declaration the selected
5722        // command has in scope answers — in both directions. Comparing whole alias sets asked
5723        // the ancestor instead, because the merged flag also carries its orphan `-c`.
5724        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"
5725            .parse()
5726            .unwrap();
5727
5728        assert!(
5729            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5730            "the child added exclusivity the environment value has to honor"
5731        );
5732
5733        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"
5734            .parse()
5735            .unwrap();
5736
5737        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5738            .expect("the child dropped the exclusivity, and the environment value follows it");
5739    }
5740
5741    #[test]
5742    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5743        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
5744        // the same reason the companion check does.
5745        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"
5746            .parse()
5747            .unwrap();
5748
5749        parse(&spec, &input(&["ex", "run", "--clean"]))
5750            .expect("a merged child exclusive flag is still the command's requiredness escape");
5751    }
5752
5753    #[test]
5754    fn a_group_allows_one_member_and_refuses_two() {
5755        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5756            .parse()
5757            .unwrap();
5758
5759        // One is fine, and so is none: a plain group says "at most one".
5760        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5761        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5762
5763        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5764        assert!(err.to_string().contains("group input"), "{err}");
5765    }
5766
5767    #[test]
5768    fn positional_selectors_work_in_conflicts_and_groups() {
5769        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5770            .parse()
5771            .unwrap();
5772        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5773            .expect("the flag alone is valid");
5774        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5775        assert!(parse(
5776            &conflicts,
5777            &input(&["ex", "--from-file", "vars.env", "literal"])
5778        )
5779        .is_err());
5780
5781        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5782            .parse()
5783            .unwrap();
5784        assert!(parse(
5785            &positional_source,
5786            &input(&["ex", "--from-file", "vars.env", "literal"])
5787        )
5788        .is_err());
5789
5790        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5791            .parse()
5792            .unwrap();
5793        assert!(parse(&group, &input(&["ex"])).is_err());
5794        parse(&group, &input(&["ex", "target-name"]))
5795            .expect("a positional satisfies a required group");
5796        assert!(parse(
5797            &group,
5798            &input(&["ex", "--file", "input.txt", "target-name"])
5799        )
5800        .is_err());
5801    }
5802
5803    #[test]
5804    fn a_required_group_needs_one_of_its_members() {
5805        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5806            .parse()
5807            .unwrap();
5808
5809        let err = parse(&spec, &input(&["ex"])).unwrap_err();
5810        // The members, because that is what a user has to type; the name, because a
5811        // command with several groups would otherwise report the same sentence twice.
5812        assert!(err.to_string().contains("--file, --url"), "{err}");
5813        assert!(err.to_string().contains("input"), "{err}");
5814
5815        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5816    }
5817
5818    #[test]
5819    fn a_multiple_group_only_polices_requiredness() {
5820        // `multiple` with `required` is "at least one of these", so two is fine and
5821        // none is not.
5822        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5823            .parse()
5824            .unwrap();
5825
5826        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5827        assert!(parse(&spec, &input(&["ex"])).is_err());
5828    }
5829
5830    #[test]
5831    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5832        // The two halves of a group are two kinds of rule, and they read a default
5833        // differently on purpose. Requiredness asks whether a member has a value, and a
5834        // default is a value — the rule `requires` follows. Exclusivity asks what the
5835        // user supplied, because a defaulted member counted as supplied would collide
5836        // with the sibling they actually typed and refuse a correct command line.
5837        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5838            .parse()
5839            .unwrap();
5840
5841        parse(&spec, &input(&["ex"])).expect("the default fills the group");
5842        parse(&spec, &input(&["ex", "--url", "u"]))
5843            .expect("the default must not conflict with the flag the user typed");
5844    }
5845
5846    #[test]
5847    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5848        // `-f` and `--file` are one flag. Counted by selector, giving it once would
5849        // report it as conflicting with itself.
5850        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5851            .parse()
5852            .unwrap();
5853
5854        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5855        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5856
5857        // A genuine collision is still one.
5858        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5859        assert!(err.to_string().contains("group input"), "{err}");
5860    }
5861
5862    #[test]
5863    fn a_group_reads_the_environment_as_given() {
5864        // The environment does count, which is the same asymmetry `conflicts` has: an
5865        // env var is somebody saying something, a default is nobody saying anything.
5866        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5867            .parse()
5868            .unwrap();
5869
5870        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5871    }
5872
5873    #[test]
5874    fn a_requirement_names_the_flag_that_is_missing() {
5875        // Reported as the missing flag rather than as something wrong with `--out`,
5876        // which is what clap says for an unmet `requires` and what a user can act on.
5877        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
5878            .parse()
5879            .unwrap();
5880
5881        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
5882        assert!(
5883            err.to_string().contains("format"),
5884            "the missing flag should be named: {err}"
5885        );
5886
5887        // Satisfied, in either order.
5888        for words in [
5889            &["ex", "--out", "a.txt", "--format", "json"][..],
5890            &["ex", "--format", "json", "--out", "a.txt"][..],
5891        ] {
5892            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
5893        }
5894
5895        // Nothing happens when the flag that imposes the rule is absent: a requirement
5896        // is a consequence of using the flag, not a rule about the command line.
5897        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
5898    }
5899
5900    #[test]
5901    fn a_value_activates_only_its_conditional_requirement() {
5902        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"
5903            .parse()
5904            .unwrap();
5905
5906        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
5907            .expect("an unrelated value requires nothing");
5908
5909        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
5910        assert!(key.to_string().contains("key"), "{key}");
5911        parse(
5912            &spec,
5913            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
5914        )
5915        .expect("the matching requirement is satisfied");
5916
5917        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
5918        assert!(token.to_string().contains("token"), "{token}");
5919    }
5920
5921    #[test]
5922    fn conditional_requirements_read_explicit_env_but_not_defaults() {
5923        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"
5924            .parse()
5925            .unwrap();
5926        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
5927        assert!(err.to_string().contains("key"), "{err}");
5928
5929        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"
5930            .parse()
5931            .unwrap();
5932        parse(&from_default, &input(&["ex"]))
5933            .expect("a default is not an explicit conditional value");
5934    }
5935
5936    #[test]
5937    fn command_line_values_override_env_for_conditional_requirements() {
5938        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
5939            .parse()
5940            .unwrap();
5941
5942        parse_with_env(
5943            &spec,
5944            &["ex", "--config", "ordinary.toml"],
5945            &[("EX_CONFIG", "special.toml")],
5946        )
5947        .expect("the command-line value takes precedence over the environment");
5948    }
5949
5950    #[test]
5951    fn conditional_requirements_normalize_boolean_env_values() {
5952        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
5953            .parse()
5954            .unwrap();
5955
5956        for value in ["1", "true", "True", "TRUE"] {
5957            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
5958            assert!(err.to_string().contains("key"), "{value}: {err}");
5959        }
5960        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
5961            .expect("a false environment value does not activate a true condition");
5962    }
5963
5964    #[test]
5965    fn a_default_satisfies_a_requirement() {
5966        // The flag it names has a value, which is the question a requirement asks. Read
5967        // any other way, `--format` would be missing here and present ten lines further
5968        // down, where plain required-ness reads the same default as filling it.
5969        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
5970            .parse()
5971            .unwrap();
5972
5973        parse(&spec, &input(&["ex", "--out", "a.txt"]))
5974            .expect("a defaulted flag is not a missing one");
5975    }
5976
5977    #[test]
5978    fn a_present_flag_binds_a_conditional_default() {
5979        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
5980            .parse()
5981            .unwrap();
5982
5983        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
5984        assert_eq!(
5985            with.as_env().get("usage_bin_names").map(String::as_str),
5986            Some("true")
5987        );
5988
5989        let without = parse(&spec, &input(&["ex"])).unwrap();
5990        assert!(
5991            !without.as_env().contains_key("usage_bin_names"),
5992            "IsPresent does nothing when the selector is absent"
5993        );
5994    }
5995
5996    #[test]
5997    fn an_equals_condition_binds_a_conditional_default() {
5998        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
5999            .parse()
6000            .unwrap();
6001
6002        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6003        assert_eq!(
6004            json.as_env().get("usage_style").map(String::as_str),
6005            Some("pretty")
6006        );
6007        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6008        assert!(!yaml.as_env().contains_key("usage_style"));
6009    }
6010
6011    #[test]
6012    fn an_equals_condition_reads_a_negated_flag() {
6013        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6014            .parse()
6015            .unwrap();
6016
6017        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6018        assert_eq!(
6019            off.as_env().get("usage_pretty").map(String::as_str),
6020            Some("true")
6021        );
6022        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6023        assert!(
6024            !on.as_env().contains_key("usage_pretty"),
6025            "--json is true, so when=false should miss"
6026        );
6027    }
6028
6029    #[test]
6030    fn the_first_matching_conditional_default_wins() {
6031        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"
6032            .parse()
6033            .unwrap();
6034
6035        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6036        assert_eq!(
6037            out.as_env().get("usage_style").map(String::as_str),
6038            Some("compact")
6039        );
6040    }
6041
6042    #[test]
6043    fn argv_and_env_suppress_a_conditional_default() {
6044        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6045            .parse()
6046            .unwrap();
6047
6048        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6049        assert_eq!(
6050            from_env.as_env().get("usage_bin_names").map(String::as_str),
6051            Some("false"),
6052            "the target's environment wins over default_if"
6053        );
6054    }
6055
6056    #[test]
6057    fn a_sibling_env_activates_a_conditional_default() {
6058        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6059            .parse()
6060            .unwrap();
6061
6062        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6063        assert_eq!(
6064            out.as_env().get("usage_bin_names").map(String::as_str),
6065            Some("true")
6066        );
6067    }
6068
6069    #[test]
6070    fn a_default_does_not_activate_a_conditional_default() {
6071        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
6072        // bind would put json's default into `out.flags` and then treat it as
6073        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
6074        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6075            .parse()
6076            .unwrap();
6077
6078        let out = parse(&spec, &input(&["ex"])).unwrap();
6079        assert_eq!(
6080            out.as_env().get("usage_json").map(String::as_str),
6081            Some("true")
6082        );
6083        assert!(
6084            !out.as_env().contains_key("usage_pretty"),
6085            "a default is not an explicit value for default_if"
6086        );
6087    }
6088
6089    #[test]
6090    fn a_conditional_default_does_not_activate_requires_if() {
6091        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"
6092            .parse()
6093            .unwrap();
6094
6095        parse(&spec, &input(&["ex", "--json"]))
6096            .expect("a default_if value is not explicit for requires_if");
6097        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6098    }
6099
6100    #[test]
6101    fn a_conditional_default_satisfies_a_requirement() {
6102        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6103            .parse()
6104            .unwrap();
6105
6106        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6107            .expect("default_if fills the required flag");
6108        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6109    }
6110
6111    #[test]
6112    fn an_environment_value_satisfies_a_requirement() {
6113        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6114            .parse()
6115            .unwrap();
6116
6117        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6118        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6119            .expect("the environment supplies it");
6120    }
6121
6122    #[test]
6123    fn a_requirement_is_satisfied_by_a_short_form() {
6124        // The selector may spell the other flag any way it answers to, so the check
6125        // resolves it the way every other selector is resolved rather than matching
6126        // text. The error names the flag, not the selector.
6127        let spec: Spec =
6128            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6129                .parse()
6130                .unwrap();
6131
6132        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6133
6134        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6135        assert!(err.to_string().contains("key"), "{err}");
6136    }
6137
6138    #[test]
6139    fn conflicting_flags_are_rejected_in_either_order() {
6140        // Declared once, on `--file`, which is all clap exposes — so the check has to
6141        // be order-independent by looking at every flag that was given rather than at
6142        // the one that declared the conflict.
6143        let spec: Spec =
6144            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6145                .parse()
6146                .unwrap();
6147
6148        for words in [
6149            &["ex", "--file", "a.txt", "--stdin"][..],
6150            &["ex", "--stdin", "--file", "a.txt"][..],
6151        ] {
6152            let err = parse(&spec, &input(words)).unwrap_err();
6153            assert!(
6154                err.to_string().contains("conflicts with --stdin"),
6155                "{words:?} should be refused: {err}"
6156            );
6157        }
6158
6159        // Either one alone is fine.
6160        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6161        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6162    }
6163
6164    #[test]
6165    fn unknown_flags_are_values_by_default() {
6166        // The default, and the reason it is the default: a spec often parses a
6167        // command line whose flags belong to something else.
6168        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6169            .parse()
6170            .unwrap();
6171        let out = parse(
6172            &spec,
6173            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6174        )
6175        .unwrap();
6176        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6177        assert_eq!(out.args[rest].to_string(), "--wat x");
6178    }
6179
6180    #[test]
6181    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6182        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6183            .parse()
6184            .unwrap();
6185        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6186            .expect("a repeat is a correction by default");
6187        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6188        assert_eq!(out.flags[jobs].to_string(), "2");
6189        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6190            .expect("switches use the same default");
6191
6192        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6193            .parse()
6194            .unwrap();
6195        for words in [
6196            &["ex", "--jobs", "1", "--jobs", "2"][..],
6197            &["ex", "--verbose", "--verbose"][..],
6198        ] {
6199            let err = parse(&strict, &input(words)).unwrap_err();
6200            assert!(
6201                err.to_string().contains("cannot be used multiple times"),
6202                "{err}"
6203            );
6204        }
6205
6206        let reparsed: Spec = strict.to_string().parse().unwrap();
6207        assert!(!reparsed.cmd.args_override_self);
6208    }
6209
6210    #[test]
6211    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6212        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6213            .parse()
6214            .unwrap();
6215
6216        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6217            .expect("opposite forms override each other");
6218        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6219        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6220
6221        for words in [
6222            &["ex", "--color", "--color"][..],
6223            &["ex", "--no-color", "--no-color"][..],
6224        ] {
6225            let err = parse(&spec, &input(words)).unwrap_err();
6226            assert!(err.to_string().contains("cannot be used multiple times"));
6227        }
6228    }
6229
6230    #[test]
6231    fn strict_global_flags_may_repeat_across_command_levels() {
6232        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"
6233            .parse()
6234            .unwrap();
6235
6236        let out = parse(
6237            &spec,
6238            &input(&[
6239                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6240            ]),
6241        )
6242        .expect("an inherited global is allowed once at each command level");
6243        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6244        assert_eq!(out.flags[jobs].to_string(), "2");
6245
6246        for words in [
6247            &["ex", "--color", "--color", "run", "--no-color"][..],
6248            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6249        ] {
6250            let err = parse(&spec, &input(words)).unwrap_err();
6251            assert!(err.to_string().contains("cannot be used multiple times"));
6252        }
6253    }
6254
6255    #[test]
6256    fn a_subcommand_can_negate_only_its_parents_requirements() {
6257        let base = r#"name "ex"
6258bin "ex"
6259flag "--config" required=#true
6260flag "--mode" requires="--config"
6261flag "--other"
6262arg "<input>"
6263group "source" "--config" "--other" required=#true
6264cmd "run" { flag "--child" required=#true }
6265"#;
6266        let strict: Spec = base.parse().unwrap();
6267        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6268        let message = err.to_string();
6269        assert!(
6270            message.contains("input") || message.contains("config"),
6271            "{message}"
6272        );
6273
6274        let negated: Spec = base
6275            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6276            .parse()
6277            .unwrap();
6278        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6279        assert!(
6280            err.to_string().contains("child"),
6281            "the selected command keeps its own requirements: {err}"
6282        );
6283
6284        let mut child_optional = negated.clone();
6285        child_optional.cmd.subcommands["run"].flags[0].required = false;
6286        parse(&child_optional, &input(&["ex", "run"]))
6287            .expect("the child selection satisfies all parent requirements");
6288        parse(&child_optional, &input(&["ex", "--mode", "run"]))
6289            .expect("parent requires relationships are negated too");
6290    }
6291
6292    #[test]
6293    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6294        let spec: Spec = r#"name "ex"
6295bin "ex"
6296args_conflicts_with_subcommands #true
6297flag "--verbose"
6298cmd "run"
6299"#
6300        .parse()
6301        .unwrap();
6302
6303        parse(&spec, &input(&["ex", "run"]))
6304            .expect("the subcommand is valid without a parent argument");
6305        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6306        assert!(
6307            err.to_string().contains("cannot be used with arguments"),
6308            "{err}"
6309        );
6310    }
6311
6312    #[test]
6313    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6314        let base = r#"name "ex"
6315bin "ex"
6316flag "--values <value>..."
6317cmd "run"
6318"#;
6319        let plain: Spec = base.parse().unwrap();
6320        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6321        assert_eq!(out.cmd.name, "ex");
6322
6323        let precedence: Spec = base
6324            .replacen(
6325                "bin \"ex\"",
6326                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6327                1,
6328            )
6329            .parse()
6330            .unwrap();
6331        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6332        assert_eq!(out.cmd.name, "run");
6333    }
6334
6335    #[test]
6336    fn a_required_positional_can_follow_an_unfilled_optional_one() {
6337        let base = r#"name "ex"
6338bin "ex"
6339arg "[optional]"
6340arg "<required>"
6341"#;
6342        let plain: Spec = base.parse().unwrap();
6343        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6344        assert!(err.to_string().contains("required"), "{err}");
6345
6346        let enabled: Spec = base
6347            .replacen(
6348                "bin \"ex\"",
6349                "bin \"ex\"\nallow_missing_positional #true",
6350                1,
6351            )
6352            .parse()
6353            .unwrap();
6354        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6355        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6356        let value = &out
6357            .args
6358            .iter()
6359            .find(|(arg, _)| arg.name == "required")
6360            .unwrap()
6361            .1;
6362        assert!(matches!(value, ParseValue::String(value) if value == "value"));
6363    }
6364
6365    #[test]
6366    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6367        let spec: Spec =
6368            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6369                .parse()
6370                .unwrap();
6371        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6372        assert!(
6373            err.to_string().contains("--wat"),
6374            "the message should name the token: {err}"
6375        );
6376
6377        // The positional opts into the narrower negative-number carve-out without
6378        // accepting arbitrary unknown flags.
6379        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6380        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6381        assert_eq!(out.args[rest].to_string(), "-1");
6382
6383        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6384        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6385        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6386    }
6387
6388    #[test]
6389    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6390        let spec: Spec = r#"
6391name "ex"
6392bin "ex"
6393unknown_flags "error"
6394flag "-0 --print0" global=#true
6395cmd "run" {
6396  flag "--force"
6397}
6398"#
6399        .parse()
6400        .unwrap();
6401        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6402        assert_eq!(out.cmd.name, "run");
6403        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6404        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6405        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6406        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6407    }
6408
6409    #[test]
6410    fn a_command_may_override_the_cli_wide_setting() {
6411        // Strict overall, lenient for the one command that forwards options.
6412        let spec: Spec = r#"
6413name "ex"
6414bin "ex"
6415unknown_flags "error"
6416cmd "exec" unknown_flags="value" {
6417  arg "[rest]..."
6418}
6419cmd "build" {
6420  arg "[rest]..."
6421}
6422"#
6423        .parse()
6424        .unwrap();
6425
6426        let out = parse(
6427            &spec,
6428            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6429        )
6430        .unwrap();
6431        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6432        assert_eq!(out.args[rest].to_string(), "--wat");
6433
6434        assert!(
6435            parse(
6436                &spec,
6437                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6438            )
6439            .is_err(),
6440            "a command that says nothing inherits the CLI's choice"
6441        );
6442    }
6443
6444    #[test]
6445    fn the_setting_survives_a_round_trip() {
6446        let spec: Spec =
6447            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6448                .parse()
6449                .unwrap();
6450        let reparsed: Spec = spec.to_string().parse().unwrap();
6451        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6452        assert_eq!(
6453            reparsed.cmd.subcommands["x"].unknown_flags,
6454            Some(UnknownFlags::Value)
6455        );
6456    }
6457
6458    #[test]
6459    fn test_default_subcommand() {
6460        // Test that default_subcommand routes to the specified subcommand
6461        let run_cmd = SpecCommand::builder()
6462            .name("run")
6463            .arg(SpecArg::builder().name("task").build())
6464            .build();
6465        let mut cmd = SpecCommand::builder().name("test").build();
6466        cmd.subcommands.insert("run".to_string(), run_cmd);
6467
6468        let spec = Spec {
6469            name: "test".to_string(),
6470            bin: "test".to_string(),
6471            cmd,
6472            default_subcommand: Some("run".to_string()),
6473            ..Default::default()
6474        };
6475
6476        // "test mytask" should be parsed as if it were "test run mytask"
6477        let input = vec!["test".to_string(), "mytask".to_string()];
6478        let parsed = parse(&spec, &input).unwrap();
6479
6480        // Should have two commands: root and "run"
6481        assert_eq!(parsed.cmds.len(), 2);
6482        assert_eq!(parsed.cmds[1].name, "run");
6483
6484        // Should have parsed the task argument
6485        assert_eq!(parsed.args.len(), 1);
6486        let arg = parsed.args.keys().next().unwrap();
6487        assert_eq!(arg.name, "task");
6488        let value = parsed.args.values().next().unwrap();
6489        assert_eq!(value.to_string(), "mytask");
6490    }
6491
6492    #[test]
6493    fn test_default_subcommand_explicit_still_works() {
6494        // Test that explicit subcommand takes precedence
6495        let run_cmd = SpecCommand::builder()
6496            .name("run")
6497            .arg(SpecArg::builder().name("task").build())
6498            .build();
6499        let other_cmd = SpecCommand::builder()
6500            .name("other")
6501            .arg(SpecArg::builder().name("other_arg").build())
6502            .build();
6503        let mut cmd = SpecCommand::builder().name("test").build();
6504        cmd.subcommands.insert("run".to_string(), run_cmd);
6505        cmd.subcommands.insert("other".to_string(), other_cmd);
6506
6507        let spec = Spec {
6508            name: "test".to_string(),
6509            bin: "test".to_string(),
6510            cmd,
6511            default_subcommand: Some("run".to_string()),
6512            ..Default::default()
6513        };
6514
6515        // "test other foo" should use "other" subcommand, not default
6516        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6517        let parsed = parse(&spec, &input).unwrap();
6518
6519        // Should have used "other" subcommand
6520        assert_eq!(parsed.cmds.len(), 2);
6521        assert_eq!(parsed.cmds[1].name, "other");
6522    }
6523
6524    #[test]
6525    fn test_default_subcommand_applies_only_at_the_root() {
6526        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
6527        // was being looked up wherever the parser happened to be standing, so a command with
6528        // an unrelated subcommand of the same name acquired a default of its own: with
6529        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
6530        // `zzz` there. Nothing declared that, and nothing could have.
6531        let mut config_ls = SpecCommand::builder().name("ls").build();
6532        config_ls.args.push(SpecArg::builder().name("what").build());
6533        let mut config_cmd = SpecCommand::builder().name("config").build();
6534        config_cmd.subcommands.insert("ls".to_string(), config_ls);
6535
6536        // The root's own `ls`, which is what its default points at. It takes an argument so
6537        // that a routed word has somewhere to land.
6538        let mut root_ls = SpecCommand::builder().name("ls").build();
6539        root_ls.args.push(SpecArg::builder().name("what").build());
6540        let mut cmd = SpecCommand::builder().name("ex").build();
6541        cmd.subcommands.insert("ls".to_string(), root_ls);
6542        cmd.subcommands.insert("config".to_string(), config_cmd);
6543
6544        let spec = Spec {
6545            name: "ex".to_string(),
6546            bin: "ex".to_string(),
6547            cmd,
6548            default_subcommand: Some("ls".to_string()),
6549            ..Default::default()
6550        };
6551
6552        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
6553        // own business — and `config` takes no argument, so this is an error rather than a
6554        // silent descent.
6555        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6556        assert!(
6557            parse(&spec, &input).is_err(),
6558            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6559        );
6560
6561        // At the root, where it is declared, it still applies.
6562        let input = vec!["ex".to_string(), "zzz".to_string()];
6563        let parsed = parse(&spec, &input).expect("the root's default applies");
6564        assert_eq!(
6565            parsed
6566                .cmds
6567                .iter()
6568                .map(|c| c.name.as_str())
6569                .collect::<Vec<_>>(),
6570            ["ex", "ls"]
6571        );
6572        assert_eq!(
6573            parsed.args.values().next().map(|v| v.to_string()),
6574            Some("zzz".to_string()),
6575            "and the word binds inside the command it reached"
6576        );
6577    }
6578
6579    #[test]
6580    fn test_default_subcommand_with_nested_subcommands() {
6581        // Test that default_subcommand works when the default subcommand has nested subcommands.
6582        // This is the mise use case: "mise say" should be parsed as "mise run say"
6583        // where "say" is a subcommand of "run" (a task).
6584        let say_cmd = SpecCommand::builder()
6585            .name("say")
6586            .arg(SpecArg::builder().name("name").build())
6587            .build();
6588        let mut run_cmd = SpecCommand::builder().name("run").build();
6589        run_cmd.subcommands.insert("say".to_string(), say_cmd);
6590
6591        let mut cmd = SpecCommand::builder().name("test").build();
6592        cmd.subcommands.insert("run".to_string(), run_cmd);
6593
6594        let spec = Spec {
6595            name: "test".to_string(),
6596            bin: "test".to_string(),
6597            cmd,
6598            default_subcommand: Some("run".to_string()),
6599            ..Default::default()
6600        };
6601
6602        // "test say hello" should be parsed as "test run say hello"
6603        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6604        let parsed = parse(&spec, &input).unwrap();
6605
6606        // Should have three commands: root, "run", and "say"
6607        assert_eq!(parsed.cmds.len(), 3);
6608        assert_eq!(parsed.cmds[0].name, "test");
6609        assert_eq!(parsed.cmds[1].name, "run");
6610        assert_eq!(parsed.cmds[2].name, "say");
6611
6612        // Should have parsed the "name" argument
6613        assert_eq!(parsed.args.len(), 1);
6614        let arg = parsed.args.keys().next().unwrap();
6615        assert_eq!(arg.name, "name");
6616        let value = parsed.args.values().next().unwrap();
6617        assert_eq!(value.to_string(), "hello");
6618    }
6619
6620    /// Build a spec equivalent to the post-mount structure produced by mise's
6621    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
6622    /// subcommand that re-declares the same flag as NON-global, and a mounted task
6623    /// (`sample:run`) carrying a positional arg with `choices`.
6624    ///
6625    /// We construct the merged structure directly instead of executing a real mount so the
6626    /// test stays hermetic and cross-platform while still exercising the parser defect.
6627    fn mounted_global_flag_spec() -> Spec {
6628        let task_cmd = SpecCommand::builder()
6629            .name("sample:run")
6630            .arg(
6631                SpecArg::builder()
6632                    .name("profile")
6633                    .choices(["alpha", "beta", "gamma"])
6634                    .build(),
6635            )
6636            .build();
6637        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
6638        let mut run_cmd = SpecCommand::builder()
6639            .name("run")
6640            .flag(
6641                SpecFlag::builder()
6642                    .name("cd")
6643                    .short('C')
6644                    .long("cd")
6645                    .arg(SpecArg::builder().name("dir").build())
6646                    .global(false)
6647                    .build(),
6648            )
6649            .build();
6650        run_cmd
6651            .subcommands
6652            .insert("sample:run".to_string(), task_cmd);
6653
6654        let mut cmd = SpecCommand::builder()
6655            .name("test")
6656            .flag(
6657                SpecFlag::builder()
6658                    .name("cd")
6659                    .short('C')
6660                    .long("cd")
6661                    .arg(SpecArg::builder().name("dir").build())
6662                    .global(true)
6663                    .build(),
6664            )
6665            .build();
6666        cmd.subcommands.insert("run".to_string(), run_cmd);
6667
6668        Spec {
6669            name: "test".to_string(),
6670            bin: "test".to_string(),
6671            cmd,
6672            ..Default::default()
6673        }
6674    }
6675
6676    #[test]
6677    fn test_prefix_global_flag_does_not_pollute_choices() {
6678        // Regression for the parser-side root cause referenced by jdx/mise#10069.
6679        //
6680        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
6681        // then into the mounted `sample:run`) used to drop the inherited global flag from
6682        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
6683        // mis-validated against the task's `choices` positional arg.
6684        let spec = mounted_global_flag_spec();
6685
6686        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
6687        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
6688        for words in [
6689            &["test", "-C", "/tmp", "run", "sample:run"][..],
6690            // Embedded-value form must behave identically.
6691            &["test", "--cd=/tmp", "run", "sample:run"][..],
6692        ] {
6693            let parsed = parse_partial(&spec, &input(words)).unwrap();
6694            assert_eq!(
6695                parsed
6696                    .cmds
6697                    .iter()
6698                    .map(|c| c.name.as_str())
6699                    .collect::<Vec<_>>(),
6700                vec!["test", "run", "sample:run"],
6701            );
6702            // No positional arg should have been consumed by the leftover global-flag tokens.
6703            assert!(
6704                parsed.args.is_empty(),
6705                "args should be empty, got {:?}",
6706                parsed.args
6707            );
6708
6709            // Fix (B): the inherited global flag survives the descent even though `run`
6710            // re-declares `-C/--cd` as non-global.
6711            let cd = parsed
6712                .available_flags
6713                .get("--cd")
6714                .expect("--cd should remain available after descending into the subcommand");
6715            assert!(cd.global, "--cd must stay global after descent");
6716            assert!(
6717                parsed.available_flags.get("-C").is_some_and(|f| f.global),
6718                "-C must stay global after descent",
6719            );
6720
6721            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
6722            // for normal execution and for the env passed to mount scripts. (Removing the
6723            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
6724            assert_eq!(
6725                parsed.as_env().get("usage_cd").map(String::as_str),
6726                Some("/tmp"),
6727                "global flag value must survive in as_env(), got {:?}",
6728                parsed.as_env(),
6729            );
6730        }
6731
6732        // A real, valid choice still parses through the global flag prefix.
6733        let parsed = parse_partial(
6734            &spec,
6735            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6736        )
6737        .unwrap();
6738        assert_eq!(parsed.args.len(), 1);
6739        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6740
6741        // And genuinely invalid choices are still rejected (we didn't disable validation).
6742        assert_parse_err(
6743            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6744            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6745        );
6746    }
6747
6748    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
6749    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
6750    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
6751    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
6752    fn mounted_orphan_short_spec() -> Spec {
6753        let task_cmd = SpecCommand::builder()
6754            .name("sample:run")
6755            .arg(
6756                SpecArg::builder()
6757                    .name("profile")
6758                    .choices(["alpha", "beta", "gamma"])
6759                    .build(),
6760            )
6761            .build();
6762        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
6763        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
6764        let mut run_cmd = SpecCommand::builder()
6765            .name("run")
6766            .flag(
6767                SpecFlag::builder()
6768                    .name("raw")
6769                    .short('r')
6770                    .long("raw")
6771                    .global(false)
6772                    .build(),
6773            )
6774            .flag(
6775                SpecFlag::builder()
6776                    .name("force")
6777                    .short('f')
6778                    .long("force")
6779                    .global(false)
6780                    .build(),
6781            )
6782            .build();
6783        run_cmd
6784            .subcommands
6785            .insert("sample:run".to_string(), task_cmd);
6786
6787        // Root global is LONG-ONLY: `--raw` with no short.
6788        let mut cmd = SpecCommand::builder()
6789            .name("test")
6790            .flag(
6791                SpecFlag::builder()
6792                    .name("raw")
6793                    .long("raw")
6794                    .global(true)
6795                    .build(),
6796            )
6797            .build();
6798        cmd.subcommands.insert("run".to_string(), run_cmd);
6799
6800        Spec {
6801            name: "test".to_string(),
6802            bin: "test".to_string(),
6803            cmd,
6804            ..Default::default()
6805        }
6806    }
6807
6808    #[test]
6809    fn test_orphan_short_alias_survives_merge() {
6810        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
6811        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
6812        // added short `-r` must be unioned onto the surviving inherited global flag instead of
6813        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
6814        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
6815        let spec = mounted_orphan_short_spec();
6816
6817        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6818        assert_eq!(
6819            parsed
6820                .cmds
6821                .iter()
6822                .map(|c| c.name.as_str())
6823                .collect::<Vec<_>>(),
6824            vec!["test", "run", "sample:run"],
6825        );
6826
6827        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
6828        // and the original long `--raw` is still global too.
6829        assert!(
6830            parsed.available_flags.get("-r").is_some_and(|f| f.global),
6831            "-r must be merged onto the inherited global flag and stay global after descent",
6832        );
6833        assert!(
6834            parsed
6835                .available_flags
6836                .get("--raw")
6837                .is_some_and(|f| f.global),
6838            "--raw must stay global after descent",
6839        );
6840
6841        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
6842        assert!(
6843            parsed.args.is_empty(),
6844            "args should be empty, got {:?}",
6845            parsed.args
6846        );
6847
6848        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
6849        assert_eq!(
6850            parsed.as_env().get("usage_raw").map(String::as_str),
6851            Some("true"),
6852            "merged short's value must survive in as_env(), got {:?}",
6853            parsed.as_env(),
6854        );
6855
6856        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
6857        // promoted/merged — it is correctly dropped when descending into the mount.
6858        assert!(
6859            !parsed.available_flags.contains_key("-f"),
6860            "purely-local -f must not be promoted onto a global",
6861        );
6862        assert!(
6863            !parsed.available_flags.contains_key("--force"),
6864            "purely-local --force must not be promoted onto a global",
6865        );
6866
6867        // A real, valid choice still parses through the merged short prefix.
6868        let parsed =
6869            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6870        assert_eq!(parsed.args.len(), 1);
6871        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6872
6873        // And genuinely invalid choices are still rejected.
6874        assert_parse_err(
6875            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6876            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6877        );
6878    }
6879
6880    #[test]
6881    fn test_orphan_short_does_not_clobber_unrelated_global() {
6882        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
6883        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
6884        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
6885        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
6886        let run_cmd = SpecCommand::builder()
6887            .name("run")
6888            .flag(
6889                SpecFlag::builder()
6890                    .name("raw")
6891                    .short('r')
6892                    .long("raw")
6893                    .global(false)
6894                    .build(),
6895            )
6896            .build();
6897        let mut cmd = SpecCommand::builder()
6898            .name("test")
6899            .flag(
6900                SpecFlag::builder()
6901                    .name("raw")
6902                    .long("raw")
6903                    .global(true)
6904                    .build(),
6905            )
6906            .flag(
6907                SpecFlag::builder()
6908                    .name("restrict")
6909                    .short('r')
6910                    .long("restrict")
6911                    .global(true)
6912                    .build(),
6913            )
6914            .build();
6915        cmd.subcommands.insert("run".to_string(), run_cmd);
6916        let spec = Spec {
6917            name: "test".to_string(),
6918            bin: "test".to_string(),
6919            cmd,
6920            ..Default::default()
6921        };
6922
6923        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
6924        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
6925        assert_eq!(
6926            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
6927            Some("restrict"),
6928            "-r must remain owned by the unrelated global it already belonged to",
6929        );
6930        // Both globals are still recognized and global after the descent.
6931        assert!(parsed
6932            .available_flags
6933            .get("--raw")
6934            .is_some_and(|f| f.global));
6935        assert!(parsed
6936            .available_flags
6937            .get("--restrict")
6938            .is_some_and(|f| f.global));
6939    }
6940
6941    #[test]
6942    fn test_redeclared_global_aliases_share_one_flag() {
6943        // A global declared with BOTH a short and a long, re-declared non-globally by a
6944        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
6945        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
6946        // the time `-y` is reached the long already points at the merged flag. That merged flag is
6947        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
6948        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
6949        let spec = r#"
6950flag "-y --yes" global=#true effect="write"
6951cmd "run" {
6952    flag "-y --yes --assume-yes"
6953}
6954"#
6955        .parse::<Spec>()
6956        .unwrap();
6957
6958        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
6959
6960        for key in ["-y", "--yes", "--assume-yes"] {
6961            let flag = parsed
6962                .available_flags
6963                .get(key)
6964                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
6965            assert!(flag.global, "{key} must stay global after the descent");
6966            assert_eq!(
6967                flag.long,
6968                vec!["yes".to_string(), "assume-yes".to_string()],
6969                "{key} must resolve to the flag carrying every alias",
6970            );
6971            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
6972        }
6973
6974        // One logical flag means one object: all three keys share a single `Arc`.
6975        assert_eq!(
6976            unique_flags(parsed.available_flags.values()).count(),
6977            1,
6978            "all aliases must point at one flag object, got {:?}",
6979            parsed.available_flags,
6980        );
6981
6982        // The global's effect survives the merge, so `-y` still marks the command as writing.
6983        assert_eq!(
6984            parsed.available_flags["-y"].effect,
6985            Some(crate::SpecCommandEffect::Write),
6986        );
6987    }
6988
6989    #[test]
6990    fn test_redeclared_global_keeps_hidden_alias_metadata() {
6991        let spec = r#"
6992flag "--yes" global=#true {
6993    alias "-q" "--quietly" hide=#true
6994}
6995cmd "run" {
6996    flag "--yes --assume-yes" {
6997        alias "-s" "--secret" hide=#true
6998    }
6999}
7000"#
7001        .parse::<Spec>()
7002        .unwrap();
7003
7004        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7005        let merged = &parsed.available_flags["--yes"];
7006        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7007        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7008        for key in ["-q", "-s", "--quietly", "--secret"] {
7009            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7010        }
7011    }
7012
7013    #[test]
7014    fn test_redeclared_global_can_promote_hidden_aliases() {
7015        let spec = r#"
7016flag "--yes" global=#true {
7017    alias "-q" "--quietly" hide=#true
7018}
7019cmd "run" {
7020    flag "-q --yes --quietly"
7021}
7022"#
7023        .parse::<Spec>()
7024        .unwrap();
7025
7026        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7027        let merged = &parsed.available_flags["--yes"];
7028        assert!(merged.hidden_short_aliases.is_empty());
7029        assert!(merged.hidden_aliases.is_empty());
7030        for key in ["-q", "--quietly"] {
7031            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7032        }
7033    }
7034
7035    #[test]
7036    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7037        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
7038        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
7039        // aliases the child omits are never visited by the merge loop, so they must be rebound to
7040        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
7041        // pre-merge global and miss the added `assume-yes`.
7042        let spec = r#"
7043flag "-y --yes --confirm" global=#true
7044cmd "run" {
7045    flag "--yes --assume-yes"
7046}
7047"#
7048        .parse::<Spec>()
7049        .unwrap();
7050
7051        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7052
7053        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7054            let flag = parsed
7055                .available_flags
7056                .get(key)
7057                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7058            assert!(flag.global, "{key} must stay global after the descent");
7059            assert_eq!(
7060                flag.long,
7061                vec![
7062                    "yes".to_string(),
7063                    "confirm".to_string(),
7064                    "assume-yes".to_string()
7065                ],
7066                "{key} must resolve to the flag carrying every alias",
7067            );
7068        }
7069
7070        assert_eq!(
7071            unique_flags(parsed.available_flags.values()).count(),
7072            1,
7073            "all aliases must point at one flag object, got {:?}",
7074            parsed.available_flags,
7075        );
7076    }
7077
7078    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
7079    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
7080    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
7081    ///
7082    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
7083    /// commands it merges in, so the test stays hermetic (no mount subprocess).
7084    fn mounted_task_flag_spec() -> Spec {
7085        let mut task_cmd = SpecCommand::builder()
7086            .name("mytask")
7087            .flag(
7088                SpecFlag::builder()
7089                    .name("env")
7090                    .long("env")
7091                    .arg(
7092                        SpecArg::builder()
7093                            .name("name")
7094                            .choices(["dev", "stage", "prod"])
7095                            .build(),
7096                    )
7097                    .global(false)
7098                    .build(),
7099            )
7100            .flag(
7101                SpecFlag::builder()
7102                    .name("bump")
7103                    .long("bump")
7104                    .arg(
7105                        SpecArg::builder()
7106                            .name("type")
7107                            .choices(["auto", "major"])
7108                            .build(),
7109                    )
7110                    .global(false)
7111                    .build(),
7112            )
7113            .build();
7114        task_cmd.mounted = true;
7115
7116        let mut run_cmd = SpecCommand::builder()
7117            .name("run")
7118            .flag(
7119                SpecFlag::builder()
7120                    .name("force")
7121                    .short('f')
7122                    .long("force")
7123                    .global(false)
7124                    .build(),
7125            )
7126            .build();
7127        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7128
7129        let mut cmd = SpecCommand::builder()
7130            .name("test")
7131            .flag(
7132                SpecFlag::builder()
7133                    .name("env")
7134                    .short('E')
7135                    .long("env")
7136                    .arg(SpecArg::builder().name("ENV").build())
7137                    .global(true)
7138                    .build(),
7139            )
7140            .flag(
7141                SpecFlag::builder()
7142                    .name("silent")
7143                    .long("silent")
7144                    .global(true)
7145                    .build(),
7146            )
7147            .build();
7148        cmd.subcommands.insert("run".to_string(), run_cmd);
7149
7150        Spec {
7151            name: "test".to_string(),
7152            bin: "test".to_string(),
7153            cmd,
7154            ..Default::default()
7155        }
7156    }
7157
7158    #[test]
7159    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7160        // The mounted program's own commands are ordinary commands relative to each other, so
7161        // descending *within* the mounted tree must follow the normal rules — including keeping
7162        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
7163        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
7164        // global, which the next descent's `retain(global)` then dropped entirely.
7165        let deep = SpecCommand::builder().name("deep").build();
7166        let mut sub = SpecCommand::builder()
7167            .name("sub")
7168            // Re-declares the mounted program's own global as non-global.
7169            .flag(
7170                SpecFlag::builder()
7171                    .name("cd")
7172                    .short('C')
7173                    .long("cd")
7174                    .arg(SpecArg::builder().name("dir").build())
7175                    .global(false)
7176                    .build(),
7177            )
7178            .build();
7179        sub.subcommands.insert("deep".to_string(), deep);
7180        let mut task = SpecCommand::builder()
7181            .name("task")
7182            .flag(
7183                SpecFlag::builder()
7184                    .name("cd")
7185                    .short('C')
7186                    .long("cd")
7187                    .arg(SpecArg::builder().name("dir").build())
7188                    .global(true)
7189                    .build(),
7190            )
7191            .build();
7192        task.subcommands.insert("sub".to_string(), sub);
7193        task.mark_mounted();
7194
7195        let mut run_cmd = SpecCommand::builder().name("run").build();
7196        run_cmd.subcommands.insert("task".to_string(), task);
7197        let mut cmd = SpecCommand::builder().name("test").build();
7198        cmd.subcommands.insert("run".to_string(), run_cmd);
7199        let spec = Spec {
7200            name: "test".to_string(),
7201            bin: "test".to_string(),
7202            cmd,
7203            ..Default::default()
7204        };
7205
7206        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7207        assert!(
7208            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7209            "the mounted program's own global must survive descents inside the mounted tree",
7210        );
7211        assert!(
7212            parsed.completion_flags().contains_key("--cd"),
7213            "and must still be offered there: it belongs to the mounted program",
7214        );
7215        assert!(
7216            parsed.completion_flags().contains_key("-C"),
7217            "including the short the nested command re-declared",
7218        );
7219    }
7220
7221    #[test]
7222    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7223        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
7224        // into the command the mount sits on. They belong to the mounted program, so they must
7225        // be offered inside the mounted commands rather than filtered out with the mounting
7226        // CLI's own flags.
7227        let mut task = SpecCommand::builder()
7228            .name("task")
7229            .flag(
7230                SpecFlag::builder()
7231                    .name("bump")
7232                    .long("bump")
7233                    .global(false)
7234                    .build(),
7235            )
7236            .build();
7237        task.mark_mounted();
7238
7239        let mut run_cmd = SpecCommand::builder().name("run").build();
7240        run_cmd.subcommands.insert("task".to_string(), task);
7241        // What `mount()` leaves behind when the mounted spec's root declares flags.
7242        run_cmd.flags = vec![
7243            SpecFlag::builder()
7244                .name("tglobal")
7245                .long("tglobal")
7246                .global(true)
7247                .build(),
7248            SpecFlag::builder()
7249                .name("tlocal")
7250                .long("tlocal")
7251                .global(false)
7252                .build(),
7253        ];
7254        run_cmd.flags_from_mount = true;
7255
7256        let mut cmd = SpecCommand::builder()
7257            .name("test")
7258            .flag(
7259                SpecFlag::builder()
7260                    .name("silent")
7261                    .long("silent")
7262                    .global(true)
7263                    .build(),
7264            )
7265            .build();
7266        cmd.subcommands.insert("run".to_string(), run_cmd);
7267        let spec = Spec {
7268            name: "test".to_string(),
7269            bin: "test".to_string(),
7270            cmd,
7271            ..Default::default()
7272        };
7273
7274        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7275        assert_eq!(
7276            parsed.completion_flags().keys().collect::<Vec<_>>(),
7277            vec!["--bump", "--tglobal"],
7278            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7279             `--silent` does not, and the mount's non-global root flag is not inherited",
7280        );
7281    }
7282
7283    #[test]
7284    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7285        // Regression for jdx/mise#11282. A mounted command describes another program, which
7286        // does not accept the mounting CLI's globals (mise forwards everything after a task
7287        // name to the task). They must stay recognized — they may appear before the mounted
7288        // command — but must not be offered in completions there.
7289        let spec = mounted_task_flag_spec();
7290        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7291
7292        // Still recognized for parsing...
7293        assert!(parsed.available_flags.contains_key("--silent"));
7294        assert!(parsed.available_flags.contains_key("-E"));
7295        // ...but belonging to a command above the mount, so not offered.
7296        assert_eq!(
7297            parsed.completion_flags().keys().collect::<Vec<_>>(),
7298            vec!["--bump", "--env"],
7299            "only the mounted command's own flags may be offered",
7300        );
7301
7302        // `run`'s own non-global flag is dropped on descent, as it always was.
7303        assert!(!parsed.available_flags.contains_key("--force"));
7304    }
7305
7306    #[test]
7307    fn test_mounted_cmd_flag_wins_over_inherited_global() {
7308        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
7309        // by the root's `--env` global, so completing its value fell back to file completion.
7310        let spec = mounted_task_flag_spec();
7311        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7312
7313        let awaiting = parsed
7314            .flag_awaiting_value
7315            .first()
7316            .expect("--env should await a value");
7317        assert_eq!(
7318            awaiting
7319                .arg
7320                .as_ref()
7321                .and_then(|a| a.choices.as_ref())
7322                .map(|c| c.choices.clone()),
7323            Some(vec![
7324                "dev".to_string(),
7325                "stage".to_string(),
7326                "prod".to_string()
7327            ]),
7328            "the mounted command's own --env must win over the inherited global",
7329        );
7330
7331        // The global's short is not declared by the mounted command, so it keeps pointing at
7332        // the global and a value passed before the mounted command still parses.
7333        let parsed =
7334            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7335        assert!(
7336            parsed.args.is_empty(),
7337            "prefix global tokens must not be consumed as positionals, got {:?}",
7338            parsed.args
7339        );
7340        assert_eq!(
7341            parsed.as_env().get("usage_env").map(String::as_str),
7342            Some("anything"),
7343        );
7344    }
7345
7346    #[test]
7347    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7348        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
7349        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
7350        // global's value would be validated against the mounted flag's choices and a legitimate
7351        // value would be rejected.
7352        let spec = mounted_task_flag_spec();
7353        let parsed = parse_partial(
7354            &spec,
7355            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7356        )
7357        .unwrap();
7358        assert!(
7359            parsed.errors.is_empty(),
7360            "prefix global value must not be validated against the mounted flag: {:?}",
7361            parsed
7362                .errors
7363                .iter()
7364                .map(|e| e.to_string())
7365                .collect::<Vec<_>>(),
7366        );
7367        assert_eq!(
7368            parsed.as_env().get("usage_env").map(String::as_str),
7369            Some("not-a-task-choice"),
7370        );
7371
7372        // The embedded-value form binds the same way.
7373        let parsed = parse_partial(
7374            &spec,
7375            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7376        )
7377        .unwrap();
7378        assert!(parsed.errors.is_empty());
7379        assert_eq!(
7380            parsed.as_env().get("usage_env").map(String::as_str),
7381            Some("not-a-task-choice"),
7382        );
7383
7384        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
7385        // the same name was already used before it.
7386        let parsed = parse_partial(
7387            &spec,
7388            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7389        )
7390        .unwrap();
7391        let awaiting = parsed
7392            .flag_awaiting_value
7393            .first()
7394            .expect("--env should await a value");
7395        assert_eq!(
7396            awaiting
7397                .arg
7398                .as_ref()
7399                .and_then(|a| a.choices.as_ref())
7400                .map(|c| c.choices.clone()),
7401            Some(vec![
7402                "dev".to_string(),
7403                "stage".to_string(),
7404                "prod".to_string()
7405            ]),
7406            "the mounted command's --env must own the name after the mounted command",
7407        );
7408    }
7409
7410    #[test]
7411    fn test_non_global_flag_does_not_hide_subcommand() {
7412        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
7413        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
7414        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
7415        let spec = mounted_task_flag_spec();
7416
7417        for words in [
7418            // `run` declares `-f/--force` as non-global.
7419            &["test", "run", "--force", "mytask"][..],
7420            &["test", "run", "-f", "mytask"][..],
7421            // Mixed with a global before the subcommand.
7422            &["test", "-E", "prod", "run", "--force", "mytask"][..],
7423        ] {
7424            let parsed = parse_partial(&spec, &input(words)).unwrap();
7425            assert_eq!(
7426                parsed
7427                    .cmds
7428                    .iter()
7429                    .map(|c| c.name.as_str())
7430                    .collect::<Vec<_>>(),
7431                vec!["test", "run", "mytask"],
7432                "{words:?} should descend into the mounted command",
7433            );
7434            assert!(
7435                parsed.args.is_empty(),
7436                "{words:?} should not consume a positional, got {:?}",
7437                parsed.args,
7438            );
7439            assert_eq!(
7440                parsed.as_env().get("usage_force").map(String::as_str),
7441                Some("true"),
7442                "the non-global flag must still be recorded for {words:?}",
7443            );
7444        }
7445
7446        // A non-global flag that takes a value consumes it, rather than reading the value as the
7447        // subcommand.
7448        let mut run_cmd = SpecCommand::builder()
7449            .name("run")
7450            .flag(
7451                SpecFlag::builder()
7452                    .name("output")
7453                    .short('o')
7454                    .long("output")
7455                    .arg(SpecArg::builder().name("mode").build())
7456                    .global(false)
7457                    .build(),
7458            )
7459            .build();
7460        run_cmd.subcommands.insert(
7461            "task".to_string(),
7462            SpecCommand::builder().name("task").build(),
7463        );
7464        let mut cmd = SpecCommand::builder().name("test").build();
7465        cmd.subcommands.insert("run".to_string(), run_cmd);
7466        let spec = Spec {
7467            name: "test".to_string(),
7468            bin: "test".to_string(),
7469            cmd,
7470            ..Default::default()
7471        };
7472
7473        let parsed =
7474            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7475        assert_eq!(
7476            parsed
7477                .cmds
7478                .iter()
7479                .map(|c| c.name.as_str())
7480                .collect::<Vec<_>>(),
7481            vec!["test", "run", "task"],
7482        );
7483        assert_eq!(
7484            parsed.as_env().get("usage_output").map(String::as_str),
7485            Some("quiet"),
7486        );
7487
7488        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
7489        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
7490        assert_parse_err(
7491            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7492            "unexpected word: --nope",
7493        );
7494    }
7495
7496    #[test]
7497    fn test_non_mounted_subcommand_offers_inherited_globals() {
7498        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
7499        // still both recognized and offered.
7500        let mut run_cmd = SpecCommand::builder().name("run").build();
7501        run_cmd.subcommands.insert(
7502            "nested".to_string(),
7503            SpecCommand::builder().name("nested").build(),
7504        );
7505        let mut cmd = SpecCommand::builder()
7506            .name("test")
7507            .flag(
7508                SpecFlag::builder()
7509                    .name("silent")
7510                    .long("silent")
7511                    .global(true)
7512                    .build(),
7513            )
7514            .build();
7515        cmd.subcommands.insert("run".to_string(), run_cmd);
7516        let spec = Spec {
7517            name: "test".to_string(),
7518            bin: "test".to_string(),
7519            cmd,
7520            ..Default::default()
7521        };
7522
7523        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7524        assert_eq!(
7525            parsed.completion_flags().keys().collect::<Vec<_>>(),
7526            parsed.available_flags.keys().collect::<Vec<_>>(),
7527        );
7528        assert!(parsed.completion_flags().contains_key("--silent"));
7529    }
7530
7531    #[test]
7532    fn test_subcommand_alias_collision_keeps_last_owner() {
7533        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
7534        // share an alias are resolved. Historically the flattened flag map gave the shared
7535        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
7536        let run_cmd = SpecCommand::builder()
7537            .name("run")
7538            .flag(
7539                SpecFlag::builder()
7540                    .name("alpha")
7541                    .short('x')
7542                    .long("alpha")
7543                    .global(false)
7544                    .build(),
7545            )
7546            .flag(
7547                SpecFlag::builder()
7548                    .name("beta")
7549                    .short('x')
7550                    .long("beta")
7551                    .global(false)
7552                    .build(),
7553            )
7554            .build();
7555        let mut cmd = SpecCommand::builder().name("test").build();
7556        cmd.subcommands.insert("run".to_string(), run_cmd);
7557        let spec = Spec {
7558            name: "test".to_string(),
7559            bin: "test".to_string(),
7560            cmd,
7561            ..Default::default()
7562        };
7563
7564        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7565        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
7566        assert_eq!(
7567            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7568            Some("beta"),
7569            "the last-declared flag must keep a shared short alias",
7570        );
7571        // Both distinct long aliases remain recognized and point to their own flag.
7572        assert_eq!(
7573            parsed
7574                .available_flags
7575                .get("--alpha")
7576                .map(|f| f.name.as_str()),
7577            Some("alpha"),
7578        );
7579        assert_eq!(
7580            parsed
7581                .available_flags
7582                .get("--beta")
7583                .map(|f| f.name.as_str()),
7584            Some("beta"),
7585        );
7586    }
7587
7588    #[test]
7589    fn test_default_subcommand_same_name_child() {
7590        // Test that default_subcommand doesn't cause issues when the default subcommand
7591        // has a child with the same name (e.g., "run" has a task named "run").
7592        // This verifies we don't switch multiple times or get stuck in a loop.
7593        let run_task = SpecCommand::builder()
7594            .name("run")
7595            .arg(SpecArg::builder().name("args").build())
7596            .build();
7597        let mut run_cmd = SpecCommand::builder().name("run").build();
7598        run_cmd.subcommands.insert("run".to_string(), run_task);
7599
7600        let mut cmd = SpecCommand::builder().name("test").build();
7601        cmd.subcommands.insert("run".to_string(), run_cmd);
7602
7603        let spec = Spec {
7604            name: "test".to_string(),
7605            bin: "test".to_string(),
7606            cmd,
7607            default_subcommand: Some("run".to_string()),
7608            ..Default::default()
7609        };
7610
7611        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
7612        let input = vec!["test".to_string(), "run".to_string()];
7613        let parsed = parse(&spec, &input).unwrap();
7614
7615        // Should have two commands: root and "run"
7616        assert_eq!(parsed.cmds.len(), 2);
7617        assert_eq!(parsed.cmds[0].name, "test");
7618        assert_eq!(parsed.cmds[1].name, "run");
7619
7620        // "test run run" should descend into the "run" task (child of "run" subcommand)
7621        let input = vec![
7622            "test".to_string(),
7623            "run".to_string(),
7624            "run".to_string(),
7625            "hello".to_string(),
7626        ];
7627        let parsed = parse(&spec, &input).unwrap();
7628
7629        assert_eq!(parsed.cmds.len(), 3);
7630        assert_eq!(parsed.cmds[0].name, "test");
7631        assert_eq!(parsed.cmds[1].name, "run");
7632        assert_eq!(parsed.cmds[2].name, "run");
7633        assert_eq!(parsed.args.len(), 1);
7634        let value = parsed.args.values().next().unwrap();
7635        assert_eq!(value.to_string(), "hello");
7636
7637        // Key test case: "test other" should switch to default subcommand "run"
7638        // and treat "other" as a positional arg (not try to switch again because
7639        // "run" also has a "run" child).
7640        let mut run_cmd = SpecCommand::builder()
7641            .name("run")
7642            .arg(SpecArg::builder().name("task").build())
7643            .build();
7644        let run_task = SpecCommand::builder().name("run").build();
7645        run_cmd.subcommands.insert("run".to_string(), run_task);
7646
7647        let mut cmd = SpecCommand::builder().name("test").build();
7648        cmd.subcommands.insert("run".to_string(), run_cmd);
7649
7650        let spec = Spec {
7651            name: "test".to_string(),
7652            bin: "test".to_string(),
7653            cmd,
7654            default_subcommand: Some("run".to_string()),
7655            ..Default::default()
7656        };
7657
7658        let input = vec!["test".to_string(), "other".to_string()];
7659        let parsed = parse(&spec, &input).unwrap();
7660
7661        // Should have two commands: root and "run" (the default)
7662        // We should NOT have switched again to the "run" task child
7663        assert_eq!(parsed.cmds.len(), 2);
7664        assert_eq!(parsed.cmds[0].name, "test");
7665        assert_eq!(parsed.cmds[1].name, "run");
7666
7667        // "other" should be parsed as a positional arg
7668        assert_eq!(parsed.args.len(), 1);
7669        let value = parsed.args.values().next().unwrap();
7670        assert_eq!(value.to_string(), "other");
7671    }
7672
7673    #[test]
7674    fn test_restart_token() {
7675        // Test that restart_token resets argument parsing
7676        let run_cmd = SpecCommand::builder()
7677            .name("run")
7678            .arg(SpecArg::builder().name("task").build())
7679            .restart_token(":::".to_string())
7680            .build();
7681        let mut cmd = SpecCommand::builder().name("test").build();
7682        cmd.subcommands.insert("run".to_string(), run_cmd);
7683
7684        let spec = Spec {
7685            name: "test".to_string(),
7686            bin: "test".to_string(),
7687            cmd,
7688            ..Default::default()
7689        };
7690
7691        // "test run task1 ::: task2" - should end up with task2 as the arg
7692        let input = vec![
7693            "test".to_string(),
7694            "run".to_string(),
7695            "task1".to_string(),
7696            ":::".to_string(),
7697            "task2".to_string(),
7698        ];
7699        let parsed = parse(&spec, &input).unwrap();
7700
7701        // After restart, args were cleared and task2 was parsed
7702        assert_eq!(parsed.args.len(), 1);
7703        let value = parsed.args.values().next().unwrap();
7704        assert_eq!(value.to_string(), "task2");
7705    }
7706
7707    #[test]
7708    fn test_restart_token_multiple() {
7709        // Test multiple restart tokens
7710        let run_cmd = SpecCommand::builder()
7711            .name("run")
7712            .arg(SpecArg::builder().name("task").build())
7713            .restart_token(":::".to_string())
7714            .build();
7715        let mut cmd = SpecCommand::builder().name("test").build();
7716        cmd.subcommands.insert("run".to_string(), run_cmd);
7717
7718        let spec = Spec {
7719            name: "test".to_string(),
7720            bin: "test".to_string(),
7721            cmd,
7722            ..Default::default()
7723        };
7724
7725        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
7726        let input = vec![
7727            "test".to_string(),
7728            "run".to_string(),
7729            "task1".to_string(),
7730            ":::".to_string(),
7731            "task2".to_string(),
7732            ":::".to_string(),
7733            "task3".to_string(),
7734        ];
7735        let parsed = parse(&spec, &input).unwrap();
7736
7737        // After multiple restarts, args were cleared and task3 was parsed
7738        assert_eq!(parsed.args.len(), 1);
7739        let value = parsed.args.values().next().unwrap();
7740        assert_eq!(value.to_string(), "task3");
7741    }
7742
7743    #[test]
7744    fn test_restart_token_clears_flag_awaiting_value() {
7745        // Test that restart_token clears pending flag values
7746        let run_cmd = SpecCommand::builder()
7747            .name("run")
7748            .arg(SpecArg::builder().name("task").build())
7749            .flag(
7750                SpecFlag::builder()
7751                    .name("jobs")
7752                    .long("jobs")
7753                    .arg(SpecArg::builder().name("count").build())
7754                    .build(),
7755            )
7756            .restart_token(":::".to_string())
7757            .build();
7758        let mut cmd = SpecCommand::builder().name("test").build();
7759        cmd.subcommands.insert("run".to_string(), run_cmd);
7760
7761        let spec = Spec {
7762            name: "test".to_string(),
7763            bin: "test".to_string(),
7764            cmd,
7765            ..Default::default()
7766        };
7767
7768        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
7769        let input = vec![
7770            "test".to_string(),
7771            "run".to_string(),
7772            "task1".to_string(),
7773            "--jobs".to_string(),
7774            ":::".to_string(),
7775            "task2".to_string(),
7776        ];
7777        let parsed = parse(&spec, &input).unwrap();
7778
7779        // task2 should be parsed as the task arg, not as --jobs value
7780        assert_eq!(parsed.args.len(), 1);
7781        let value = parsed.args.values().next().unwrap();
7782        assert_eq!(value.to_string(), "task2");
7783        // --jobs should not have a value
7784        assert!(parsed.flag_awaiting_value.is_empty());
7785    }
7786
7787    #[test]
7788    fn test_restart_token_resets_double_dash() {
7789        // Test that restart_token resets the -- separator effect
7790        let run_cmd = SpecCommand::builder()
7791            .name("run")
7792            .arg(SpecArg::builder().name("task").build())
7793            .arg(SpecArg::builder().name("extra_args").var(true).build())
7794            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7795            .restart_token(":::".to_string())
7796            .build();
7797        let mut cmd = SpecCommand::builder().name("test").build();
7798        cmd.subcommands.insert("run".to_string(), run_cmd);
7799
7800        let spec = Spec {
7801            name: "test".to_string(),
7802            bin: "test".to_string(),
7803            cmd,
7804            ..Default::default()
7805        };
7806
7807        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
7808        let input = vec![
7809            "test".to_string(),
7810            "run".to_string(),
7811            "task1".to_string(),
7812            "--".to_string(),
7813            "extra".to_string(),
7814            ":::".to_string(),
7815            "--verbose".to_string(),
7816            "task2".to_string(),
7817        ];
7818        let parsed = parse(&spec, &input).unwrap();
7819
7820        // --verbose should be parsed as a flag (not an arg) after the restart
7821        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7822        // task2 should be the arg after restart
7823        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7824        let value = parsed.args.get(task_arg).unwrap();
7825        assert_eq!(value.to_string(), "task2");
7826    }
7827
7828    #[test]
7829    fn test_double_dashes_without_preserve() {
7830        // Only the first `--` is a separator; a later one is a value, because flag
7831        // parsing has already stopped and there is nothing left for it to do.
7832        // `preserve` is about the *first* one — see the test below, where none is
7833        // consumed at all.
7834        let run_cmd = SpecCommand::builder()
7835            .name("run")
7836            .arg(SpecArg::builder().name("args").var(true).build())
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 arg1 -- arg2 -- arg3": the first separates, the second is a value
7849        let input = vec![
7850            "test".to_string(),
7851            "run".to_string(),
7852            "arg1".to_string(),
7853            "--".to_string(),
7854            "arg2".to_string(),
7855            "--".to_string(),
7856            "arg3".to_string(),
7857        ];
7858        let parsed = parse(&spec, &input).unwrap();
7859
7860        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7861        let value = parsed.args.get(args_arg).unwrap();
7862        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7863    }
7864
7865    #[test]
7866    fn test_double_dashes_with_preserve() {
7867        // Test that variadic args WITH `preserve` keep all double dashes
7868        let run_cmd = SpecCommand::builder()
7869            .name("run")
7870            .arg(
7871                SpecArg::builder()
7872                    .name("args")
7873                    .var(true)
7874                    .double_dash(SpecDoubleDashChoices::Preserve)
7875                    .build(),
7876            )
7877            .build();
7878        let mut cmd = SpecCommand::builder().name("test").build();
7879        cmd.subcommands.insert("run".to_string(), run_cmd);
7880
7881        let spec = Spec {
7882            name: "test".to_string(),
7883            bin: "test".to_string(),
7884            cmd,
7885            ..Default::default()
7886        };
7887
7888        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
7889        let input = vec![
7890            "test".to_string(),
7891            "run".to_string(),
7892            "arg1".to_string(),
7893            "--".to_string(),
7894            "arg2".to_string(),
7895            "--".to_string(),
7896            "arg3".to_string(),
7897        ];
7898        let parsed = parse(&spec, &input).unwrap();
7899
7900        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7901        let value = parsed.args.get(args_arg).unwrap();
7902        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
7903    }
7904
7905    #[test]
7906    fn test_double_dashes_with_preserve_only_dashes() {
7907        // Test that variadic args WITH `preserve` keep all double dashes even
7908        // if the values are just double dashes
7909        let run_cmd = SpecCommand::builder()
7910            .name("run")
7911            .arg(
7912                SpecArg::builder()
7913                    .name("args")
7914                    .var(true)
7915                    .double_dash(SpecDoubleDashChoices::Preserve)
7916                    .build(),
7917            )
7918            .build();
7919        let mut cmd = SpecCommand::builder().name("test").build();
7920        cmd.subcommands.insert("run".to_string(), run_cmd);
7921
7922        let spec = Spec {
7923            name: "test".to_string(),
7924            bin: "test".to_string(),
7925            cmd,
7926            ..Default::default()
7927        };
7928
7929        // "test run -- --" - all double dashes should be preserved
7930        let input = vec![
7931            "test".to_string(),
7932            "run".to_string(),
7933            "--".to_string(),
7934            "--".to_string(),
7935        ];
7936        let parsed = parse(&spec, &input).unwrap();
7937
7938        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7939        let value = parsed.args.get(args_arg).unwrap();
7940        assert_eq!(value.to_string(), "-- --");
7941    }
7942
7943    #[test]
7944    fn test_double_dashes_with_preserve_multiple_args() {
7945        // Test with multiple args where only the second has has `preserve`
7946        let run_cmd = SpecCommand::builder()
7947            .name("run")
7948            .arg(SpecArg::builder().name("task").build())
7949            .arg(
7950                SpecArg::builder()
7951                    .name("extra_args")
7952                    .var(true)
7953                    .double_dash(SpecDoubleDashChoices::Preserve)
7954                    .build(),
7955            )
7956            .build();
7957        let mut cmd = SpecCommand::builder().name("test").build();
7958        cmd.subcommands.insert("run".to_string(), run_cmd);
7959
7960        let spec = Spec {
7961            name: "test".to_string(),
7962            bin: "test".to_string(),
7963            cmd,
7964            ..Default::default()
7965        };
7966
7967        // The first arg "task1" is captured normally
7968        // Then extra_args with `preserve` captures everything, including the "--" tokens
7969        let input = vec![
7970            "test".to_string(),
7971            "run".to_string(),
7972            "task1".to_string(),
7973            "--".to_string(),
7974            "arg1".to_string(),
7975            "--".to_string(),
7976            "--foo".to_string(),
7977        ];
7978        let parsed = parse(&spec, &input).unwrap();
7979
7980        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7981        let task_value = parsed.args.get(task_arg).unwrap();
7982        assert_eq!(task_value.to_string(), "task1");
7983
7984        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
7985        let extra_value = parsed.args.get(extra_arg).unwrap();
7986        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
7987    }
7988
7989    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
7990        let cmd = SpecCommand::builder().name("test").args(args).build();
7991        Spec {
7992            name: "test".to_string(),
7993            bin: "test".to_string(),
7994            cmd,
7995            ..Default::default()
7996        }
7997    }
7998
7999    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8000        let arg = parsed
8001            .args
8002            .keys()
8003            .find(|a| a.name == name)
8004            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8005        parsed.args.get(arg).unwrap().to_string()
8006    }
8007
8008    fn required_arg(name: &str) -> SpecArg {
8009        SpecArg::builder()
8010            .name(name)
8011            .var(true)
8012            .required(false)
8013            .double_dash(SpecDoubleDashChoices::Required)
8014            .build()
8015    }
8016
8017    #[test]
8018    fn test_double_dash_required_reports_error_once_for_variadic() {
8019        // A variadic arg is offered every remaining word, but the mistake is one mistake.
8020        let spec = spec_with_args([required_arg("files")]);
8021
8022        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8023
8024        assert!(parsed.args.is_empty());
8025        assert_eq!(parsed.errors.len(), 1);
8026        assert!(
8027            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8028        );
8029    }
8030
8031    #[test]
8032    fn test_double_dash_required_suppresses_missing_arg() {
8033        // The arg is never filled, so the end-of-parse check would also call it missing.
8034        let spec = spec_with_args([SpecArg::builder()
8035            .name("file")
8036            .required(true)
8037            .double_dash(SpecDoubleDashChoices::Required)
8038            .build()]);
8039
8040        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8041
8042        assert_eq!(parsed.errors.len(), 1);
8043        assert!(matches!(
8044            &parsed.errors[0],
8045            UsageErr::ArgRequiresDoubleDash(_)
8046        ));
8047        // The cursor stays put, so a completion keeps offering the same arg.
8048        assert_eq!(
8049            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8050            Some("file")
8051        );
8052        assert!(!parsed.double_dash_seen);
8053    }
8054
8055    #[test]
8056    fn test_double_dash_routes_to_required_arg() {
8057        // Everything after `--` belongs to the arg that requires it, even though the greedy
8058        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
8059        let spec = spec_with_args([
8060            SpecArg::builder()
8061                .name("tool")
8062                .var(true)
8063                .required(false)
8064                .build(),
8065            required_arg("command"),
8066        ]);
8067
8068        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8069
8070        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8071        assert_eq!(arg_value(&parsed, "command"), "node app.js");
8072        assert!(parsed.double_dash_seen);
8073    }
8074
8075    #[test]
8076    fn test_double_dash_routes_with_gap_reports_missing_arg() {
8077        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
8078        // "is it filled?" check cannot be a count of how many args were filled.
8079        let spec = spec_with_args([
8080            SpecArg::builder()
8081                .name("tool")
8082                .var(true)
8083                .required(true)
8084                .build(),
8085            required_arg("command"),
8086        ]);
8087
8088        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8089
8090        assert_eq!(arg_value(&parsed, "command"), "ls");
8091        assert!(parsed.args.keys().all(|a| a.name != "tool"));
8092        assert!(parsed
8093            .errors
8094            .iter()
8095            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8096    }
8097
8098    #[test]
8099    fn test_double_dash_gap_applies_defaults() {
8100        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
8101        let spec = spec_with_args([
8102            SpecArg::builder()
8103                .name("tool")
8104                .var(true)
8105                .required(false)
8106                .default_value("node@20")
8107                .build(),
8108            required_arg("command"),
8109        ]);
8110
8111        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8112
8113        assert_eq!(arg_value(&parsed, "command"), "ls");
8114        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8115    }
8116
8117    fn spec_with_restart_token_and_required_arg() -> Spec {
8118        let run_cmd = SpecCommand::builder()
8119            .name("run")
8120            .arg(SpecArg::builder().name("task").build())
8121            .arg(required_arg("run_args"))
8122            .restart_token(":::".to_string())
8123            .build();
8124        let mut cmd = SpecCommand::builder().name("test").build();
8125        cmd.subcommands.insert("run".to_string(), run_cmd);
8126        Spec {
8127            name: "test".to_string(),
8128            bin: "test".to_string(),
8129            cmd,
8130            ..Default::default()
8131        }
8132    }
8133
8134    #[test]
8135    fn test_double_dash_required_restart_token_resets_separator() {
8136        // The `--` before `:::` belongs to the previous invocation only.
8137        let spec = spec_with_restart_token_and_required_arg();
8138
8139        let parsed = parse_partial(
8140            &spec,
8141            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8142        )
8143        .unwrap();
8144
8145        assert_eq!(arg_value(&parsed, "task"), "task2");
8146        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8147        // Reported once even though the arg was violated after already succeeding once.
8148        assert_eq!(
8149            parsed
8150                .errors
8151                .iter()
8152                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8153                .count(),
8154            1
8155        );
8156    }
8157
8158    #[test]
8159    fn test_double_dash_required_restart_token_accepts_new_separator() {
8160        let spec = spec_with_restart_token_and_required_arg();
8161
8162        let parsed = parse(
8163            &spec,
8164            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8165        )
8166        .unwrap();
8167
8168        assert_eq!(arg_value(&parsed, "task"), "task2");
8169        assert_eq!(arg_value(&parsed, "run_args"), "c");
8170    }
8171
8172    #[test]
8173    fn test_double_dash_preserve_is_not_a_separator() {
8174        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
8175        // arg that requires a separator. Deliberate: one token cannot be both.
8176        let spec = spec_with_args([
8177            SpecArg::builder()
8178                .name("kept")
8179                .var(true)
8180                .var_max(1)
8181                .required(false)
8182                .double_dash(SpecDoubleDashChoices::Preserve)
8183                .build(),
8184            required_arg("rest"),
8185        ]);
8186
8187        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8188
8189        assert_eq!(arg_value(&parsed, "kept"), "--");
8190        assert!(parsed.args.keys().all(|a| a.name != "rest"));
8191        assert!(!parsed.double_dash_seen);
8192        assert_eq!(parsed.errors.len(), 1);
8193    }
8194
8195    #[test]
8196    fn test_double_dash_required_does_not_bail_in_parse_partial() {
8197        // Completions parse half-typed command lines; they must still get a result.
8198        let spec = spec_with_args([required_arg("file")]);
8199
8200        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8201        assert!(parse(&spec, &input(&["test", "x"])).is_err());
8202    }
8203
8204    #[test]
8205    fn test_double_dash_without_required_arg_does_not_move_cursor() {
8206        // Specs with no `double_dash="required"` arg are untouched by the jump.
8207        let spec = spec_with_args([
8208            SpecArg::builder().name("first").required(false).build(),
8209            SpecArg::builder().name("second").required(false).build(),
8210        ]);
8211
8212        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8213
8214        assert_eq!(arg_value(&parsed, "first"), "a");
8215        assert_eq!(arg_value(&parsed, "second"), "b");
8216        assert!(parsed.next_arg.is_none());
8217    }
8218
8219    #[test]
8220    fn test_parser_with_custom_env_for_required_arg() {
8221        let spec = spec_with_arg(
8222            SpecArg::builder()
8223                .name("name")
8224                .env("NAME")
8225                .required(true)
8226                .build(),
8227        );
8228        std::env::remove_var("NAME");
8229
8230        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8231            .expect("parse should succeed with custom env");
8232        assert_eq!(parsed.args.len(), 1);
8233        assert_eq!(first_string_value(&parsed), "john");
8234    }
8235
8236    #[test]
8237    fn test_parser_with_custom_env_for_required_flag() {
8238        let spec = spec_with_flag(
8239            SpecFlag::builder()
8240                .long("name")
8241                .env("NAME")
8242                .required(true)
8243                .arg(SpecArg::builder().name("name").build())
8244                .build(),
8245        );
8246        std::env::remove_var("NAME");
8247
8248        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8249            .expect("parse should succeed with custom env");
8250        assert_eq!(parsed.flags.len(), 1);
8251        assert_eq!(first_string_value(&parsed), "jane");
8252    }
8253
8254    #[test]
8255    fn test_flag_environment_fallbacks_preserve_declaration_order() {
8256        let spec = spec_with_flag(
8257            SpecFlag::builder()
8258                .long("name")
8259                .env("NAME")
8260                .env_fallback("OLD_NAME")
8261                .env_fallback("OLDER_NAME")
8262                .deprecated_env("DEPRECATED_NAME")
8263                .arg(SpecArg::builder().name("name").build())
8264                .build(),
8265        );
8266
8267        let parsed = parse_with_env(
8268            &spec,
8269            &["test"],
8270            &[
8271                ("NAME", "canonical"),
8272                ("OLD_NAME", "fallback"),
8273                ("DEPRECATED_NAME", "deprecated"),
8274            ],
8275        )
8276        .unwrap();
8277        assert_eq!(first_string_value(&parsed), "canonical");
8278
8279        let parsed = parse_with_env(
8280            &spec,
8281            &["test"],
8282            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8283        )
8284        .unwrap();
8285        assert_eq!(first_string_value(&parsed), "old");
8286
8287        let parsed =
8288            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8289        assert_eq!(first_string_value(&parsed), "deprecated");
8290    }
8291
8292    #[test]
8293    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8294        let spec = spec_with_flag(
8295            SpecFlag::builder()
8296                .long("name")
8297                .env("NAME")
8298                .deprecated_env("DEPRECATED_NAME")
8299                .arg(SpecArg::builder().name("name").build())
8300                .build(),
8301        );
8302
8303        // The current name is not a deprecated one, and says nothing.
8304        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8305        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8306
8307        let parsed =
8308            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8309        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8310        assert_eq!(
8311            parsed.warnings[0].kind,
8312            crate::warn::WarningKind::DeprecatedEnv
8313        );
8314        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8315        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8316        // Reported, not printed, and the value still arrives.
8317        assert_eq!(first_string_value(&parsed), "deprecated");
8318    }
8319
8320    #[test]
8321    fn a_deprecated_flag_reports_only_when_it_was_used() {
8322        let spec = spec_with_flag(
8323            SpecFlag::builder()
8324                .long("output")
8325                .deprecated("use --out")
8326                .deprecated_remove_at("3.0.0")
8327                .arg(SpecArg::builder().name("output").build())
8328                .build(),
8329        );
8330
8331        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8332        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8333
8334        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8335        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8336        assert_eq!(
8337            parsed.warnings[0].kind,
8338            crate::warn::WarningKind::DeprecatedFlag
8339        );
8340        // Named the way it was typed, dashes and all.
8341        assert_eq!(parsed.warnings[0].name, "--output");
8342        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8343        assert_eq!(
8344            parsed.warnings[0].render(),
8345            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8346        );
8347    }
8348
8349    #[test]
8350    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8351        let flag = SpecFlag::builder()
8352            .long("output")
8353            .deprecated("use --out")
8354            .deprecated_warn_at("9.0.0")
8355            .arg(SpecArg::builder().name("output").build())
8356            .build();
8357        let mut spec = spec_with_flag(flag);
8358        spec.version = Some("2.0.0".to_string());
8359
8360        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8361        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8362
8363        // And once the CLI is the release that was named, it speaks up.
8364        spec.version = Some("9.0.0".to_string());
8365        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8366        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8367    }
8368
8369    #[test]
8370    fn test_parser_with_custom_env_still_fails_when_missing() {
8371        let spec = spec_with_arg(
8372            SpecArg::builder()
8373                .name("name")
8374                .env("NAME")
8375                .required(true)
8376                .build(),
8377        );
8378        std::env::remove_var("NAME");
8379        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8380    }
8381
8382    #[test]
8383    fn test_parser_does_not_treat_env_choice_value_as_help() {
8384        let spec = spec_with_arg(
8385            SpecArg::builder()
8386                .name("env")
8387                .env("CURRENT_ENV")
8388                .choices(["dev", "staging"])
8389                .required(false)
8390                .build(),
8391        );
8392
8393        assert_parse_err(
8394            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8395            "Invalid choice for arg env: --help, expected one of dev, staging",
8396        );
8397    }
8398
8399    #[test]
8400    fn test_parser_does_not_treat_default_choice_value_as_help() {
8401        let spec = spec_with_flag(
8402            SpecFlag::builder()
8403                .long("env")
8404                .arg(
8405                    SpecArg::builder()
8406                        .name("env")
8407                        .choices(["dev", "staging"])
8408                        .build(),
8409                )
8410                .default_value("--help")
8411                .build(),
8412        );
8413
8414        assert_parse_err(
8415            parse_with_env(&spec, &["test"], &[]),
8416            "Invalid choice for option env: --help, expected one of dev, staging",
8417        );
8418    }
8419
8420    /// argv as `parse` wants it, program name included.
8421    fn words(of: &[&str]) -> Vec<String> {
8422        of.iter().map(|s| s.to_string()).collect()
8423    }
8424
8425    #[test]
8426    fn a_command_that_needs_a_subcommand_says_so() {
8427        // The spec has carried `subcommand_required` since the derive needed it, and this parser
8428        // never read it — so `mise generate`, which declares it, parsed as a complete
8429        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
8430        let spec: Spec = r#"
8431name "ex"
8432bin "ex"
8433cmd "gen" subcommand_required=#true {
8434    cmd "two" {}
8435    cmd "one" {}
8436    cmd "secret" hide=#true {}
8437    alias "g"
8438}
8439cmd "open" {
8440    cmd "sub" {}
8441}
8442"#
8443        .parse()
8444        .unwrap();
8445
8446        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8447        // Sorted, so the message does not depend on map order; hidden commands left out,
8448        // because a message telling someone to type a hidden name is worse than a vague one;
8449        // and the alias not listed beside the name it points at.
8450        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8451
8452        // Reached through its alias, and still about the command rather than the spelling.
8453        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8454        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8455
8456        // Given one: fine.
8457        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8458
8459        // And a command that has subcommands without declaring them required is untouched —
8460        // this is the half that keeps the check from being "any command with children".
8461        parse(&spec, &words(&["ex", "open"])).unwrap();
8462        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8463    }
8464
8465    #[test]
8466    fn arg_required_else_help_observes_the_selected_commands_argv() {
8467        let spec: Spec = r#"
8468name "ex"
8469bin "ex"
8470flag "--verbose" global=#true
8471cmd "run" arg_required_else_help=#true {
8472    flag "--all"
8473}
8474"#
8475        .parse()
8476        .unwrap();
8477        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8478
8479        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8480        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8481
8482        // A global before the command belongs to the ancestor. It selected `run`, but did not
8483        // give `run` an argument of its own.
8484        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8485        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8486
8487        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8488    }
8489
8490    #[test]
8491    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8492        let spec: Spec = r#"
8493name "ex"
8494bin "ex"
8495unknown_flags "error"
8496external_subcommand #true
8497cmd "install"
8498flag "-v --verbose" global=#true
8499"#
8500        .parse()
8501        .unwrap();
8502
8503        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8504        assert_eq!(
8505            parsed.external,
8506            Some(vec!["foo".into(), "--help".into(), "bar".into()])
8507        );
8508        assert!(parsed.flags.is_empty());
8509
8510        // Known subcommands still win.
8511        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8512        assert_eq!(parsed.cmd.name, "install");
8513        assert!(parsed.external.is_none());
8514
8515        // A global flag before the unmatched word still binds on the parent.
8516        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8517        assert_eq!(
8518            parsed.external,
8519            Some(vec!["foo".into(), "--verbose".into()])
8520        );
8521        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8522
8523        // An unknown flag on the parent is still an error, which is what clap does.
8524        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8525
8526        // A negative number is a value, not a flag, so it can be the unmatched word.
8527        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
8528        // token as a flag and never reach the catch-all.
8529        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8530        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8531    }
8532
8533    #[test]
8534    fn an_external_subcommand_satisfies_subcommand_required() {
8535        let mut spec: Spec = r#"
8536name "ex"
8537bin "ex"
8538external_subcommand #true
8539cmd "install"
8540"#
8541        .parse()
8542        .unwrap();
8543        spec.cmd.subcommand_required = true;
8544
8545        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8546        assert!(parse(&spec, &input(&["ex"])).is_err());
8547    }
8548
8549    #[test]
8550    fn a_default_subcommand_outranks_an_external_one() {
8551        let spec: Spec = r#"
8552name "ex"
8553bin "ex"
8554default_subcommand "run"
8555external_subcommand #true
8556cmd "run" {
8557    arg "[task]"
8558}
8559"#
8560        .parse()
8561        .unwrap();
8562
8563        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8564        assert_eq!(parsed.cmd.name, "run");
8565        assert!(parsed.external.is_none());
8566        assert_eq!(first_string_value(&parsed), "build");
8567    }
8568
8569    #[test]
8570    fn multicall_basename_strips_a_path_and_exe() {
8571        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8572        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8573        assert_eq!(multicall_basename("LS.EXE"), "LS");
8574        assert_eq!(multicall_basename("busybox"), "busybox");
8575    }
8576
8577    #[test]
8578    fn a_multicall_applet_is_the_first_word() {
8579        let spec: Spec = r#"
8580name "busybox"
8581bin "busybox"
8582multicall #true
8583cmd "ls" {
8584    arg "[ARGS]" var=#true
8585}
8586cmd "cat"
8587"#
8588        .parse()
8589        .unwrap();
8590
8591        // A symlink: argv[0] is the applet.
8592        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8593        assert_eq!(parsed.cmd.name, "ls");
8594        match parsed.args.values().next() {
8595            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8596            other => panic!("expected ARGS to collect -l, got {other:?}"),
8597        }
8598
8599        // A dispatcher invocation still skips argv[0].
8600        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8601        assert_eq!(parsed.cmd.name, "ls");
8602
8603        // Configured dispatcher values receive the same path and extension normalization.
8604        let mut configured = spec.clone();
8605        configured.name = "BusyBox".to_string();
8606        configured.bin = "/opt/bin/busybox.exe".to_string();
8607        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8608        assert_eq!(parsed.cmd.name, "ls");
8609
8610        // `.exe` is stripped so Windows and Unix agree.
8611        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8612        assert_eq!(parsed.cmd.name, "ls");
8613
8614        // Without the property, argv[0] is discarded as usual.
8615        let mut plain = spec.clone();
8616        plain.multicall = false;
8617        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8618        assert_eq!(parsed.cmd.name, "ls");
8619    }
8620
8621    #[test]
8622    fn a_multicall_unknown_applet_can_be_external() {
8623        let spec: Spec = r#"
8624name "busybox"
8625bin "busybox"
8626multicall #true
8627unknown_flags "error"
8628external_subcommand #true
8629cmd "ls"
8630"#
8631        .parse()
8632        .unwrap();
8633
8634        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8635        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8636
8637        let mut closed = spec.clone();
8638        closed.cmd.external_subcommand = false;
8639        assert!(parse(&closed, &input(&["wat"])).is_err());
8640    }
8641
8642    #[cfg(feature = "unstable_choices_env")]
8643    #[test]
8644    fn test_parser_arg_choices_from_custom_env() {
8645        let spec = spec_arg_choices_env("DEPLOY_ENVS");
8646
8647        let parsed =
8648            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8649        assert_eq!(first_string_value(&parsed), "bar");
8650
8651        assert_parse_err(
8652            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8653            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8654        );
8655        assert_parse_err(
8656            parse_with_env(&spec, &["test", "prod"], &[]),
8657            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8658        );
8659    }
8660
8661    #[cfg(feature = "unstable_choices_env")]
8662    #[test]
8663    fn test_parser_validates_flag_choices_from_custom_env() {
8664        let spec = spec_flag_choices_env("DEPLOY_ENVS");
8665        let parsed = parse_with_env(
8666            &spec,
8667            &["test", "--env", "baz"],
8668            &[("DEPLOY_ENVS", "foo,bar baz")],
8669        )
8670        .unwrap();
8671        assert_eq!(first_string_value(&parsed), "baz");
8672    }
8673
8674    #[cfg(feature = "unstable_choices_env")]
8675    #[test]
8676    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8677        let arg_env_spec = spec_with_arg(
8678            SpecArg::builder()
8679                .name("env")
8680                .env("CURRENT_ENV")
8681                .choices_env("DEPLOY_ENVS")
8682                .build(),
8683        );
8684        assert_parse_err(
8685            parse_with_env(
8686                &arg_env_spec,
8687                &["test"],
8688                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8689            ),
8690            "Invalid choice for arg env: prod, expected one of dev, staging",
8691        );
8692
8693        let flag_default_spec = spec_with_flag(
8694            SpecFlag::builder()
8695                .long("env")
8696                .arg(
8697                    SpecArg::builder()
8698                        .name("env")
8699                        .choices_env("DEPLOY_ENVS")
8700                        .build(),
8701                )
8702                .default_value("prod")
8703                .build(),
8704        );
8705        assert_parse_err(
8706            parse_with_env(
8707                &flag_default_spec,
8708                &["test"],
8709                &[("DEPLOY_ENVS", "dev,staging")],
8710            ),
8711            "Invalid choice for option env: prod, expected one of dev, staging",
8712        );
8713    }
8714
8715    #[test]
8716    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8717        let spec: Spec = r#"
8718            flag "-v --verbose" var=#true
8719            arg "[database]" default="myapp_dev"
8720            arg "[args...]"
8721        "#
8722        .parse()
8723        .unwrap();
8724        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8725            .into_iter()
8726            .map(String::from)
8727            .collect();
8728        let parsed = parse(&spec, &input).unwrap();
8729        let env = parsed.as_env();
8730        assert_eq!(env.get("usage_database").unwrap(), "mydb");
8731        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8732    }
8733
8734    #[test]
8735    fn test_variadic_arg_captures_unknown_flags() {
8736        let cmd = SpecCommand::builder()
8737            .name("test")
8738            .flag(SpecFlag::builder().short('v').long("verbose").build())
8739            .arg(SpecArg::builder().name("database").required(false).build())
8740            .arg(
8741                SpecArg::builder()
8742                    .name("args")
8743                    .required(false)
8744                    .var(true)
8745                    .build(),
8746            )
8747            .build();
8748        let spec = Spec {
8749            name: "test".to_string(),
8750            bin: "test".to_string(),
8751            cmd,
8752            ..Default::default()
8753        };
8754
8755        // Unknown --host flag and its value should be captured by [args...]
8756        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8757            .into_iter()
8758            .map(String::from)
8759            .collect();
8760        let parsed = parse(&spec, &input).unwrap();
8761        assert_eq!(parsed.args.len(), 2);
8762        let args_val = parsed
8763            .args
8764            .iter()
8765            .find(|(a, _)| a.name == "args")
8766            .unwrap()
8767            .1;
8768        match args_val {
8769            ParseValue::MultiString(v) => {
8770                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8771            }
8772            _ => panic!("Expected MultiString, got {:?}", args_val),
8773        }
8774    }
8775
8776    #[test]
8777    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8778        let cmd = SpecCommand::builder()
8779            .name("test")
8780            .flag(SpecFlag::builder().short('v').long("verbose").build())
8781            .arg(SpecArg::builder().name("database").required(false).build())
8782            .arg(
8783                SpecArg::builder()
8784                    .name("args")
8785                    .required(false)
8786                    .var(true)
8787                    .build(),
8788            )
8789            .build();
8790        let spec = Spec {
8791            name: "test".to_string(),
8792            bin: "test".to_string(),
8793            cmd,
8794            ..Default::default()
8795        };
8796
8797        // With explicit -- separator
8798        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8799            .into_iter()
8800            .map(String::from)
8801            .collect();
8802        let parsed = parse(&spec, &input).unwrap();
8803        assert_eq!(parsed.args.len(), 2);
8804        let args_val = parsed
8805            .args
8806            .iter()
8807            .find(|(a, _)| a.name == "args")
8808            .unwrap()
8809            .1;
8810        match args_val {
8811            ParseValue::MultiString(v) => {
8812                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8813            }
8814            _ => panic!("Expected MultiString, got {:?}", args_val),
8815        }
8816    }
8817
8818    #[test]
8819    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8820        // Regression: --flag=value should be treated as a single positional token when
8821        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
8822        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8823
8824        // Single unknown --flag=value: must not produce a stray "3" positional.
8825        // as_env() shell-joins via shell_words::join, so "=" gets quoted.
8826        let input: Vec<String> = vec!["test", "--option=3"]
8827            .into_iter()
8828            .map(String::from)
8829            .collect();
8830        let parsed = parse(&spec, &input).unwrap();
8831        let env = parsed.as_env();
8832        assert_eq!(
8833            env.get("usage_other_args").map(String::as_str),
8834            Some("'--option=3'"),
8835            "expected a single --option=3 token, got {:?}",
8836            env.get("usage_other_args"),
8837        );
8838
8839        // Multiple unknown --flag=value args should each be kept intact
8840        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8841            .into_iter()
8842            .map(String::from)
8843            .collect();
8844        let parsed2 = parse(&spec, &input2).unwrap();
8845        let env2 = parsed2.as_env();
8846        assert_eq!(
8847            env2.get("usage_other_args").map(String::as_str),
8848            Some("'--foo=bar' '--baz=qux'"),
8849            "expected two intact tokens, got {:?}",
8850            env2.get("usage_other_args"),
8851        );
8852
8853        // Mix of plain positional args and unknown --flag=value tokens
8854        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8855            .into_iter()
8856            .map(String::from)
8857            .collect();
8858        let parsed3 = parse(&spec, &input3).unwrap();
8859        let env3 = parsed3.as_env();
8860        assert_eq!(
8861            env3.get("usage_other_args").map(String::as_str),
8862            Some("positional1 '--option=3' positional2"),
8863            "expected positional args and intact flag token, got {:?}",
8864            env3.get("usage_other_args"),
8865        );
8866    }
8867
8868    #[test]
8869    fn test_allow_hyphen_values_consumes_short_flag_collision() {
8870        let spec = r#"
8871flag "-d --working-dir <DIR>"
8872flag "-a --args <ARGS>" allow_hyphen_values=#true
8873"#
8874        .parse::<Spec>()
8875        .unwrap();
8876
8877        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
8878
8879        assert_eq!(parsed.flags.len(), 1);
8880        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8881    }
8882
8883    #[test]
8884    fn test_allow_hyphen_values_consumes_embedded_long_value() {
8885        let spec = r#"
8886flag "-d --working-dir <DIR>"
8887flag "-a --args <ARGS>" allow_hyphen_values=#true
8888"#
8889        .parse::<Spec>()
8890        .unwrap();
8891
8892        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
8893
8894        assert_eq!(parsed.flags.len(), 1);
8895        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8896    }
8897
8898    #[test]
8899    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
8900        // The flag is declared to accept a token that looks like a flag, and `--` looks
8901        // like one, so it binds — which is what clap does with the same declaration.
8902        // Letting the separator arm run first consumed it and left the flag hungry, and
8903        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
8904        let spec = r#"
8905flag "-a --args <ARGS>" allow_hyphen_values=#true
8906arg "[rest]..."
8907"#
8908        .parse::<Spec>()
8909        .unwrap();
8910
8911        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
8912
8913        assert_eq!(flag_string_value(&parsed, "args"), "--");
8914        let rest = parsed
8915            .args
8916            .values()
8917            .next()
8918            .expect("expected the word after the separator to reach the argument");
8919        assert_eq!(rest.to_string(), "-x");
8920    }
8921
8922    #[test]
8923    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
8924        // Which token supplied the first value says nothing about how many the argument
8925        // takes, so collection carries on from a hyphenated one exactly as from a plain
8926        // one. It still stops at the next flag-like token, which is what keeps a second
8927        // occurrence of the flag from being eaten as a value.
8928        let spec = r#"
8929flag "-a --args <ARGS>..." allow_hyphen_values=#true
8930"#
8931        .parse::<Spec>()
8932        .unwrap();
8933
8934        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
8935
8936        let flag = parsed
8937            .flags
8938            .keys()
8939            .find(|flag| flag.name == "args")
8940            .expect("expected args flag");
8941        match parsed.flags.get(flag).expect("expected args value") {
8942            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
8943            other => panic!("expected a list of values, got {other:?}"),
8944        }
8945    }
8946
8947    #[test]
8948    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
8949        let spec = r#"
8950flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
8951"#
8952        .parse::<Spec>()
8953        .unwrap();
8954
8955        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
8956
8957        let flag = parsed
8958            .flags
8959            .keys()
8960            .find(|flag| flag.name == "args")
8961            .expect("expected args flag");
8962        let value = parsed.flags.get(flag).expect("expected args value");
8963        match value {
8964            ParseValue::MultiString(values) => {
8965                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
8966            }
8967            _ => panic!("expected MultiString, got {value:?}"),
8968        }
8969    }
8970
8971    #[test]
8972    fn test_require_equals_accepts_attached_and_refuses_detached() {
8973        let spec = r#"
8974flag "--inspect <PORT>" require_equals=#true
8975"#
8976        .parse::<Spec>()
8977        .unwrap();
8978
8979        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
8980        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
8981
8982        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
8983        let msg = format!("{err}");
8984        assert!(
8985            msg.contains("requires an argument") || msg.contains("inspect"),
8986            "detached value must be refused: {msg}"
8987        );
8988    }
8989
8990    #[test]
8991    fn boolean_flags_can_accept_attached_values_when_enabled() {
8992        let spec: Spec = r#"
8993name "ex"
8994bin "ex"
8995flag "--color" negate="--no-color" bool_value=#true
8996arg "[rest]"
8997"#
8998        .parse()
8999        .unwrap();
9000
9001        for (token, expected) in [
9002            ("--color", true),
9003            ("--color=true", true),
9004            ("--color=false", false),
9005            ("--no-color", false),
9006            ("--no-color=false", true),
9007        ] {
9008            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9009            assert!(
9010                matches!(
9011                    parsed.flags.get(&spec.cmd.flags[0]),
9012                    Some(ParseValue::Bool(value)) if *value == expected
9013                ),
9014                "{token}"
9015            );
9016        }
9017
9018        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9019        assert!(matches!(
9020            parsed.args.get(&spec.cmd.args[0]),
9021            Some(ParseValue::String(value)) if value == "word"
9022        ));
9023        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9024        assert!(err.to_string().contains("expected `true` or `false`"));
9025
9026        let strict: Spec = r#"
9027name "ex"
9028bin "ex"
9029args_override_self #false
9030flag "--color" negate="--no-color" bool_value=#true
9031"#
9032        .parse()
9033        .unwrap();
9034        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9035        let parsed = parse(
9036            &strict,
9037            &input(&["ex", "--color=false", "--no-color=false"]),
9038        )
9039        .unwrap();
9040        assert!(matches!(
9041            parsed.flags.get(&strict.cmd.flags[0]),
9042            Some(ParseValue::Bool(true))
9043        ));
9044    }
9045
9046    #[test]
9047    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9048        let spec = r#"
9049flag "-a --all"
9050flag "-i --inspect <PORT>" require_equals=#true
9051"#
9052        .parse::<Spec>()
9053        .unwrap();
9054
9055        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9056        let msg = format!("{err}");
9057        assert!(
9058            msg.contains("requires an argument") || msg.contains("inspect"),
9059            "bundled short must refuse the following word: {msg}"
9060        );
9061    }
9062
9063    #[test]
9064    fn test_default_missing_binds_when_the_value_is_left_off() {
9065        let spec = r#"
9066flag "-c --color <WHEN>" default_missing="always"
9067flag "-v --verbose"
9068"#
9069        .parse::<Spec>()
9070        .unwrap();
9071
9072        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9073        assert_eq!(flag_string_value(&parsed, "color"), "always");
9074
9075        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9076        assert_eq!(flag_string_value(&parsed, "color"), "never");
9077
9078        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9079        assert_eq!(flag_string_value(&parsed, "color"), "never");
9080
9081        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9082        assert_eq!(flag_string_value(&parsed, "color"), "always");
9083        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9084
9085        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9086        assert_eq!(flag_string_value(&parsed, "color"), "");
9087
9088        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9089        assert_eq!(flag_string_value(&parsed, "color"), "never");
9090
9091        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9092        assert_eq!(flag_string_value(&parsed, "color"), "always");
9093        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9094    }
9095
9096    #[test]
9097    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9098        let spec = r#"
9099flag "--bump [LEVEL]" value_optional=#true
9100flag "--verbose"
9101arg "[FILE]"
9102"#
9103        .parse::<Spec>()
9104        .unwrap();
9105
9106        let absent = parse(&spec, &input(&["test"])).unwrap();
9107        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9108
9109        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9110        let bump = bare
9111            .flags
9112            .iter()
9113            .find(|(flag, _)| flag.name == "bump")
9114            .map(|(_, value)| value)
9115            .unwrap();
9116        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9117        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9118        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9119
9120        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9121        assert_eq!(flag_string_value(&explicit, "bump"), "");
9122
9123        let corrected = parse(
9124            &spec,
9125            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9126        )
9127        .unwrap();
9128        let bump = corrected
9129            .flags
9130            .iter()
9131            .find(|(flag, _)| flag.name == "bump")
9132            .map(|(_, value)| value)
9133            .unwrap();
9134        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9135
9136        let collecting = r#"
9137flag "--tag [TAG]..." value_optional=#true
9138flag "--verbose"
9139"#
9140        .parse::<Spec>()
9141        .unwrap();
9142        let valued = parse(
9143            &collecting,
9144            &input(&["test", "--tag", "one", "two", "--verbose"]),
9145        )
9146        .unwrap();
9147        let tag = valued
9148            .flags
9149            .iter()
9150            .find(|(flag, _)| flag.name == "tag")
9151            .map(|(_, value)| value)
9152            .unwrap();
9153        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9154    }
9155
9156    #[test]
9157    fn test_repeatable_bare_optional_values_count_each_occurrence() {
9158        let spec = r#"
9159flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9160"#
9161        .parse::<Spec>()
9162        .unwrap();
9163
9164        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9165        let tag = parsed
9166            .flags
9167            .iter()
9168            .find(|(flag, _)| flag.name == "tag")
9169            .map(|(_, value)| value)
9170            .unwrap();
9171        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9172
9173        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9174        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9175    }
9176
9177    #[test]
9178    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9179        let spec = r#"
9180flag "--tag [TAG]..." var=#true value_optional=#true
9181flag "--verbose"
9182"#
9183        .parse::<Spec>()
9184        .unwrap();
9185
9186        for argv in [
9187            &["test", "--tag", "one", "two"][..],
9188            &["test", "--tag", "one", "two", "--verbose"][..],
9189            &["test", "--tag", "one", "--tag", "two"][..],
9190        ] {
9191            let parsed = parse(&spec, &input(argv)).unwrap();
9192            let tag = parsed
9193                .flags
9194                .iter()
9195                .find(|(flag, _)| flag.name == "tag")
9196                .map(|(_, value)| value)
9197                .unwrap();
9198            assert!(
9199                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9200                "argv={argv:?}: {tag:?}"
9201            );
9202        }
9203
9204        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9205        let tag = bare
9206            .flags
9207            .iter()
9208            .find(|(flag, _)| flag.name == "tag")
9209            .map(|(_, value)| value)
9210            .unwrap();
9211        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9212    }
9213
9214    #[test]
9215    fn test_default_missing_with_require_equals_refuses_the_following_word() {
9216        let spec = r#"
9217flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9218arg "[rest]"
9219"#
9220        .parse::<Spec>()
9221        .unwrap();
9222
9223        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9224        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9225
9226        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9227        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9228
9229        // The following word is not the value; the missing value is, and 80 is a positional.
9230        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9231        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9232        assert_eq!(
9233            parsed
9234                .args
9235                .values()
9236                .next()
9237                .map(|v| v.to_string())
9238                .as_deref(),
9239            Some("80")
9240        );
9241
9242        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9243        assert_eq!(flag_string_value(&parsed, "inspect"), "");
9244    }
9245
9246    #[test]
9247    fn test_default_missing_must_be_a_choice() {
9248        let spec = r#"
9249flag "--color <WHEN>" default_missing="always" {
9250    choices "auto" "always" "never"
9251}
9252"#
9253        .parse::<Spec>()
9254        .unwrap();
9255
9256        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9257        assert_eq!(flag_string_value(&parsed, "color"), "always");
9258
9259        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9260        assert_eq!(flag_string_value(&parsed, "color"), "never");
9261
9262        let spec = r#"
9263flag "--color <WHEN>" default_missing="wat" {
9264    choices "auto" "always" "never"
9265}
9266"#
9267        .parse::<Spec>()
9268        .unwrap();
9269
9270        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9271        let msg = format!("{err}");
9272        assert!(
9273            msg.contains("Invalid choice for option color: wat"),
9274            "missing default has to pass choices the same way a typed value does: {msg}"
9275        );
9276
9277        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9278        let msg = format!("{err}");
9279        assert!(
9280            msg.contains("Invalid choice for option color: wat"),
9281            "an attached value that is not a choice is still refused: {msg}"
9282        );
9283
9284        let spec = r#"
9285flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9286    choices "9229" "80"
9287}
9288arg "[rest]"
9289"#
9290        .parse::<Spec>()
9291        .unwrap();
9292
9293        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9294        let msg = format!("{err}");
9295        assert!(
9296            msg.contains("Invalid choice for option inspect: wat"),
9297            "require_equals still binds the missing string, so the error is the choice: {msg}"
9298        );
9299    }
9300
9301    #[test]
9302    fn test_hyphen_values_still_default_to_short_flag_parsing() {
9303        let spec = r#"
9304flag "-d --working-dir <DIR>"
9305flag "-a --args <ARGS>"
9306"#
9307        .parse::<Spec>()
9308        .unwrap();
9309
9310        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9311
9312        assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
9313    }
9314
9315    /// `available_flags` has to agree with what an actual parse accepts, since
9316    /// its whole reason to exist is answering that question without one.
9317    mod available_flags {
9318        use super::*;
9319
9320        fn spec() -> Spec {
9321            r#"
9322bin "test"
9323flag "-v --verbose" global=#true
9324flag "--raw" global=#true effect="write"
9325flag "--local-only"
9326cmd "run" {
9327    flag "-r --raw"
9328    flag "-w --watch"
9329    cmd "once"
9330}
9331"#
9332            .parse::<Spec>()
9333            .unwrap()
9334        }
9335
9336        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9337            let mut chain = vec![&spec.cmd];
9338            for segment in path {
9339                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9340            }
9341            chain
9342        }
9343
9344        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9345            let mut names: Vec<_> = available_flags(&chain(spec, path))
9346                .iter()
9347                .map(|f| f.name.clone())
9348                .collect();
9349            names.sort();
9350            names
9351        }
9352
9353        #[test]
9354        fn an_empty_chain_yields_nothing() {
9355            assert!(available_flags(&[]).is_empty());
9356        }
9357
9358        #[test]
9359        fn the_root_gets_its_own_flags() {
9360            let spec = spec();
9361            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9362        }
9363
9364        #[test]
9365        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9366            let spec = spec();
9367            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9368        }
9369
9370        #[test]
9371        fn a_re_declared_global_is_listed_once() {
9372            // The merge can leave the long key on the merged flag and the short
9373            // key on the pre-merge one. Same flag; it must not be listed twice.
9374            let spec = r#"
9375bin "test"
9376flag "-y --yes" global=#true effect="write"
9377cmd "rm" {
9378    flag "-y --yes"
9379}
9380"#
9381            .parse::<Spec>()
9382            .unwrap();
9383            let flags = available_flags(&chain(&spec, &["rm"]));
9384            assert_eq!(flags.len(), 1, "{flags:?}");
9385            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9386        }
9387
9388        #[test]
9389        fn a_re_declared_global_keeps_the_globals_declaration() {
9390            // `run` re-declares the long-only global `--raw` as `-r --raw`
9391            // without `global`. That is the same flag: the global's `effect`
9392            // survives, the orphan short is unioned in, and it stays global.
9393            let spec = spec();
9394            let flags = available_flags(&chain(&spec, &["run"]));
9395            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9396            assert!(raw.global);
9397            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9398            assert_eq!(raw.short, ['r']);
9399        }
9400
9401        #[test]
9402        fn it_matches_what_a_parse_accepts() {
9403            // The invariant. If these ever disagree, one of them is lying to a
9404            // caller about which flags a command takes.
9405            let spec = spec();
9406            for path in [vec![], vec!["run"], vec!["run", "once"]] {
9407                let argv = std::iter::once("test".to_string())
9408                    .chain(path.iter().map(|s| s.to_string()))
9409                    .collect::<Vec<_>>();
9410                let parsed = parse_partial(&spec, &argv).unwrap();
9411
9412                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9413                    .map(|f| f.name.clone())
9414                    .collect();
9415                from_parse.sort();
9416                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9417            }
9418        }
9419    }
9420
9421    // Provenance: which token bound what, and where a value came from when no token did.
9422
9423    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
9424    /// assert on the whole picture rather than on one field at a time.
9425    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9426        parsed
9427            .tokens
9428            .iter()
9429            .find(|token| token.index == index)
9430            .unwrap_or_else(|| panic!("no token at {index}"))
9431            .roles
9432            .iter()
9433            .map(render_role)
9434            .collect()
9435    }
9436
9437    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9438        parsed
9439            .flag_origins
9440            .iter()
9441            .find(|(f, _)| f.name == flag)
9442            .map(|(_, origins)| origins.clone())
9443            .unwrap_or_default()
9444    }
9445
9446    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9447        let env = env
9448            .iter()
9449            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9450            .collect();
9451        Parser::new(spec)
9452            .with_env(env)
9453            .explain(&input(words))
9454            .unwrap()
9455    }
9456
9457    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9458        explain_with_env(spec, words, &[])
9459    }
9460
9461    #[test]
9462    fn an_attached_long_value_is_recorded_on_the_flag_token() {
9463        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9464            .parse()
9465            .unwrap();
9466
9467        let parsed = explain(&spec, &["ex", "--env=prod"]);
9468
9469        assert_eq!(roles(&parsed, 0), ["program"]);
9470        assert_eq!(
9471            roles(&parsed, 1),
9472            ["flag env as --env", "value of env = [\"prod\"], attached"]
9473        );
9474        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
9475        // form while the detached one worked, and nothing could show the difference.
9476        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9477    }
9478
9479    #[test]
9480    fn a_detached_long_value_is_recorded_on_its_own_token() {
9481        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9482            .parse()
9483            .unwrap();
9484
9485        let parsed = explain(&spec, &["ex", "--env", "prod"]);
9486
9487        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9488        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9489    }
9490
9491    #[test]
9492    fn a_short_bundle_is_attributed_to_the_bundle_token() {
9493        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9494            .parse()
9495            .unwrap();
9496
9497        let parsed = explain(&spec, &["ex", "-abj8"]);
9498
9499        // One word the caller wrote, four things it did — and the re-queued tails are
9500        // folded back onto it rather than appearing as tokens nobody typed.
9501        assert_eq!(
9502            roles(&parsed, 1),
9503            [
9504                "flag a as -a",
9505                "flag b as -b",
9506                "flag j as -j",
9507                "value of j = [\"8\"], attached",
9508            ]
9509        );
9510        assert_eq!(parsed.tokens.len(), 2);
9511    }
9512
9513    #[test]
9514    fn a_delimiter_splits_one_token_into_several_values() {
9515        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9516            .parse()
9517            .unwrap();
9518
9519        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9520
9521        assert_eq!(
9522            roles(&parsed, 2),
9523            ["value of tags = [\"a\", \"b\", \"c\"]"],
9524            "the values meant, not the word typed"
9525        );
9526    }
9527
9528    #[test]
9529    fn a_separator_and_the_words_after_it_are_distinguished() {
9530        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9531            .parse()
9532            .unwrap();
9533
9534        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9535
9536        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9537        assert_eq!(roles(&parsed, 2), ["separator"]);
9538        // Past the separator `-x` is data, not an unknown flag.
9539        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9540    }
9541
9542    #[test]
9543    fn a_second_separator_is_data() {
9544        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9545            .parse()
9546            .unwrap();
9547
9548        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9549
9550        assert_eq!(roles(&parsed, 1), ["separator"]);
9551        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9552    }
9553
9554    #[test]
9555    fn an_unknown_flag_says_what_took_it() {
9556        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9557            .parse()
9558            .unwrap();
9559
9560        let parsed = explain(&spec, &["ex", "--wat"]);
9561
9562        // The default is lax, so the word became data. Which is the useful thing to be
9563        // told: the alternative reading is "you have a typo".
9564        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9565    }
9566
9567    #[test]
9568    fn a_subcommand_word_is_not_a_positional() {
9569        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
9570            .parse()
9571            .unwrap();
9572
9573        let parsed = explain(&spec, &["ex", "build", "a"]);
9574
9575        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9576        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9577    }
9578
9579    #[test]
9580    fn a_multicall_applet_is_read_at_argv0() {
9581        let spec: Spec =
9582            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
9583                .parse()
9584                .unwrap();
9585
9586        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9587
9588        // argv[0] is both the program and the word that selected the applet, and the word
9589        // read there is not the word the caller wrote.
9590        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9591        assert!(parsed.tokens[0].synthesized);
9592        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9593    }
9594
9595    #[test]
9596    fn words_the_parse_never_reached_say_so() {
9597        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9598            .parse()
9599            .unwrap();
9600
9601        let parsed = Parser::new(&spec)
9602            .explain(&input(&["ex", "--help", "a"]))
9603            .unwrap();
9604
9605        assert_eq!(roles(&parsed, 2), ["unread"]);
9606    }
9607
9608    #[test]
9609    fn an_env_origin_names_the_variable_that_fired() {
9610        let spec: Spec =
9611            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9612                .parse()
9613                .unwrap();
9614
9615        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9616        assert_eq!(
9617            origins(&primary, "token"),
9618            [ValueOrigin::Env("EX_TOKEN".to_string())]
9619        );
9620
9621        // The fallback firing is a different fact from the primary firing, and which one it
9622        // was is what says which declaration to delete.
9623        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9624        assert_eq!(
9625            origins(&fallback, "token"),
9626            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9627        );
9628    }
9629
9630    #[test]
9631    fn a_default_origin_is_recorded_for_flags_and_args() {
9632        let spec: Spec =
9633            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9634                .parse()
9635                .unwrap();
9636
9637        let parsed = explain(&spec, &["ex"]);
9638
9639        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9640        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9641        assert_eq!(arg.name, "src");
9642        assert_eq!(origins, &[ValueOrigin::Default]);
9643    }
9644
9645    #[test]
9646    fn a_default_if_origin_carries_the_condition_that_fired() {
9647        let spec: Spec = r#"
9648name "ex"
9649bin "ex"
9650flag "--profile <p>"
9651flag "--strict" {
9652    default_if "--profile" "prod" "true"
9653}
9654        "#
9655        .parse()
9656        .unwrap();
9657
9658        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9659
9660        // The selector alone is ambiguous: several conditions may name it with different
9661        // `when` values, so the report has to say which one matched.
9662        assert_eq!(
9663            origins(&parsed, "strict"),
9664            [ValueOrigin::DefaultIf {
9665                selector: "--profile".to_string(),
9666                when: Some("prod".to_string()),
9667            }]
9668        );
9669    }
9670
9671    #[test]
9672    fn a_bare_optional_value_flag_records_default_missing() {
9673        let spec: Spec =
9674            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9675                .parse()
9676                .unwrap();
9677
9678        let parsed = explain(&spec, &["ex", "--color", "-v"]);
9679
9680        // The flag was typed and the value was not, which is the distinction a spec author
9681        // is asking about when they ask why `--color` came out `always`.
9682        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9683        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9684        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9685    }
9686
9687    #[test]
9688    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9689        let spec: Spec =
9690            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9691                .parse()
9692                .unwrap();
9693
9694        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9695
9696        // Why origins are a list: one declaration, two occurrences, two different answers.
9697        assert_eq!(
9698            roles(&parsed, 1),
9699            [
9700                "flag color as --color",
9701                "value of color = [\"red\"], attached"
9702            ]
9703        );
9704        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9705    }
9706
9707    #[test]
9708    fn an_override_names_the_flag_that_did_it() {
9709        let spec: Spec =
9710            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9711                .parse()
9712                .unwrap();
9713
9714        let parsed = explain(&spec, &["ex", "--loud"]);
9715
9716        // Without the overriding name, "`--quiet` is unset despite its default" has no
9717        // answer: the fallback phase silently declines to fill an overridden flag.
9718        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9719        assert!(origins(&parsed, "quiet").is_empty());
9720    }
9721
9722    #[test]
9723    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9724        let spec: Spec = r#"
9725name "ex"
9726bin "ex"
9727cmd "run" restart_token=":::" {
9728    arg "<task>" default="build"
9729}
9730        "#
9731        .parse()
9732        .unwrap();
9733
9734        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9735
9736        // The values belong to the last invocation, so provenance must too — but the words
9737        // of the first were still read, and a report that dropped them would show a command
9738        // line with a hole in it.
9739        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9740        // And the token that did the resetting says so: without a role of its own it reads
9741        // as a word that did nothing, next to a `lint` that filled an arg now empty.
9742        assert_eq!(roles(&parsed, 3), ["restart"]);
9743        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9744        assert!(parsed.arg_origins.is_empty());
9745    }
9746
9747    #[test]
9748    fn a_value_terminator_says_which_run_it_ended() {
9749        let spec: Spec = r#"
9750name "ex"
9751bin "ex"
9752flag "--exec <cmd>..." value_terminator=";"
9753arg "<src>"
9754        "#
9755        .parse()
9756        .unwrap();
9757
9758        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9759
9760        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9761        // The terminator is consumed and is not one of the values, which is the whole reason
9762        // it was declared — so it needs a row saying that rather than an empty one.
9763        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9764        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9765    }
9766
9767    #[test]
9768    fn an_args_value_terminator_says_which_run_it_ended() {
9769        let spec: Spec = r#"
9770name "ex"
9771bin "ex"
9772arg "<files>..." value_terminator=";"
9773arg "[dest]"
9774        "#
9775        .parse()
9776        .unwrap();
9777
9778        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9779
9780        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9781        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9782        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9783    }
9784
9785    #[test]
9786    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9787        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9788            .parse()
9789            .unwrap();
9790
9791        let parsed = Parser::new(&spec)
9792            .explain(&input(&["ex", "--env=prod"]))
9793            .unwrap();
9794
9795        // `parse` reports "missing required <src>" and nothing else, which is the report the
9796        // caller already had. This is the case the whole thing exists for.
9797        assert!(Parser::new(&spec)
9798            .parse(&input(&["ex", "--env=prod"]))
9799            .is_err());
9800        assert_eq!(
9801            roles(&parsed, 1),
9802            ["flag env as --env", "value of env = [\"prod\"], attached"]
9803        );
9804        assert!(
9805            parsed.errors.iter().any(|e| e.to_string().contains("src")),
9806            "{:?}",
9807            parsed.errors
9808        );
9809    }
9810
9811    #[test]
9812    fn an_external_subcommand_forwards_whole_tokens() {
9813        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9814            .parse()
9815            .unwrap();
9816
9817        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9818
9819        assert_eq!(roles(&parsed, 1), ["external"]);
9820        assert_eq!(roles(&parsed, 2), ["external"]);
9821    }
9822
9823    #[test]
9824    fn a_view_keeps_the_callers_argv_positions() {
9825        let spec: Spec = r#"
9826bin "ex"
9827view "runner" root="run"
9828cmd "run" {
9829    flag "--token <token>"
9830}
9831        "#
9832        .parse()
9833        .unwrap();
9834
9835        let parsed = explain(&spec, &["runner", "--token", "secret"]);
9836
9837        // A view re-enters the parse with the same argv, so the positions still mean what
9838        // the caller wrote.
9839        assert_eq!(roles(&parsed, 0), ["program"]);
9840        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
9841    }
9842}