Skip to main content

usage/
parse.rs

1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27///
28/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
29/// command describes another program, which does not accept the mounting CLI's globals.
30/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
31/// re-parses them), but the mounted command's own flags take precedence over them, so its
32/// choices/completions are not replaced by a global's. Which flags a completion may offer
33/// there is a separate question, answered by [`ParseOutput::completion_flags`].
34fn merge_subcommand_flags(
35    available: &mut BTreeMap<String, Arc<SpecFlag>>,
36    new_flags: BTreeMap<String, Arc<SpecFlag>>,
37    crossing_mount: bool,
38) {
39    // Keep only inherited global flags from the parent.
40    available.retain(|_, f| f.global);
41
42    if crossing_mount {
43        // A mounted command owns its flags outright, including names an inherited global also
44        // uses: a word after the mounted command belongs to the mounted program. Words before
45        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
46        // mounted command does not declare (e.g. a global's short) stay inherited.
47        for (key, flag) in new_flags {
48            available.insert(key, flag);
49        }
50        return;
51    }
52
53    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
54    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
55    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56    // Maps each merged flag produced below back to the inherited global it was merged from, so
57    // the collision check can compare *origins*: a flag this loop already merged is not a
58    // different global, even though it is a different `Arc`.
59    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
61    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62        let ptr = Arc::as_ptr(flag) as usize;
63        *merged_origin.get(&ptr).unwrap_or(&ptr)
64    }
65
66    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
67    // map's existing intra-subcommand collision resolution: when two flags in the same command
68    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
69    // to its last-declared owner, and we must not change which flag owns it.
70    for (key, flag) in new_flags {
71        if flag.global {
72            // A child that re-declares (or adds) a global flag stays recognized everywhere.
73            available.insert(key, flag);
74            continue;
75        }
76
77        // A non-global re-declaration that shares a LONG name with an inherited global flag is
78        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
79        // global). Keep the global flag (global precedence, so it survives the next descent's
80        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
81        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
82        // deliberate: a re-declaration sharing only a short letter with an unrelated global
83        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
84        // handled by the `contains_key` skip below instead.
85        let inherited_global = flag.long.iter().find_map(|l| {
86            available
87                .get(&format!("--{l}"))
88                .filter(|f| f.global)
89                .cloned()
90        });
91        if let Some(global_flag) = inherited_global {
92            // Never clobber a *different* inherited global's alias. If this re-declaration's
93            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
94            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
95            // precedence dictates, instead of stealing the alias for the merged flag.
96            //
97            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
98            // earlier key of this same child already replaced some of them with the merged flag,
99            // which the lookups above may now resolve to. That is the same logical flag, so it
100            // must not read as a collision and leave this key on the pre-merge global.
101            let global_origin = origin_of(&merged_origin, &global_flag);
102            if available.get(&key).is_some_and(|existing| {
103                existing.global && origin_of(&merged_origin, existing) != global_origin
104            }) {
105                continue;
106            }
107            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108                Some(merged) => merged.clone(),
109                None => {
110                    let mut merged = (*global_flag).clone();
111                    // `exclusive` is deliberately *not* reconciled here, in either direction.
112                    // One object now answers to two alias sets that may disagree: the child
113                    // owns the spellings it declared, the ancestor keeps the ones only it
114                    // declared. A single bool cannot hold both, so the merged flag carries the
115                    // ancestor's and validation resolves the occurrence by the spelling that
116                    // was typed — the ledger it already consults to decide whether selecting
117                    // the child is company.
118                    for s in &flag.short {
119                        if !merged.short.contains(s) {
120                            merged.short.push(*s);
121                        }
122                    }
123                    for l in &flag.long {
124                        if !merged.long.contains(l) {
125                            merged.long.push(l.clone());
126                        }
127                    }
128                    // A child may deliberately promote one of the ancestor's hidden aliases.
129                    // Hidden lists are subsets of the accepted spellings, so a spelling present
130                    // on the child but absent from its hidden subset is visible at this level.
131                    merged.hidden_short_aliases.retain(|alias| {
132                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133                    });
134                    merged.hidden_aliases.retain(|alias| {
135                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136                    });
137                    for s in &flag.hidden_short_aliases {
138                        if !merged.hidden_short_aliases.contains(s) {
139                            merged.hidden_short_aliases.push(*s);
140                        }
141                    }
142                    for l in &flag.hidden_aliases {
143                        if !merged.hidden_aliases.contains(l) {
144                            merged.hidden_aliases.push(l.clone());
145                        }
146                    }
147                    let merged = Arc::new(merged);
148                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150                    // Rebind the global's *other* aliases onto the merged flag. The loop only
151                    // visits keys the child declared, so an alias the child left out (the `-y` of
152                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
153                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
154                    // logical flag must be one object under every key it answers to.
155                    for existing in available.values_mut() {
156                        if origin_of(&merged_origin, existing) == global_origin {
157                            *existing = Arc::clone(&merged);
158                        }
159                    }
160                    merged
161                }
162            };
163            available.insert(key, merged);
164            continue;
165        }
166
167        // Purely-local flag (shares nothing with an inherited global), or one that collides only
168        // on a short with an unrelated global. Insert this alias but never shadow an inherited
169        // global flag. Such non-global flags are dropped by the next descent's `retain`.
170        if available.contains_key(&key) {
171            continue;
172        }
173        available.insert(key, flag);
174    }
175}
176
177/// Build the lookup keys a flag is registered under in `available_flags`:
178/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
179fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180    let mut keys: Vec<String> = flag
181        .long
182        .iter()
183        .map(|l| format!("--{l}"))
184        .chain(flag.short.iter().map(|s| format!("-{s}")))
185        .collect();
186    if let Some(negate) = &flag.negate {
187        keys.push(negate.clone());
188    }
189    keys
190}
191
192/// The flags a command declares, keyed by each of their aliases.
193fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194    cmd.flags
195        .iter()
196        .flat_map(|f| {
197            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
198            flag_keys(&f)
199                .into_iter()
200                .map(|key| (key, Arc::clone(&f)))
201                .collect::<Vec<_>>()
202        })
203        .collect()
204}
205
206fn unique_flags<'a>(
207    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
208) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
209    let mut seen = HashSet::new();
210    flags
211        .into_iter()
212        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
213}
214
215/// Every flag a command accepts, resolved the way parsing an invocation of it
216/// resolves them.
217///
218/// `chain` runs from the root command (`spec.cmd`) down to the command in
219/// question; an empty chain yields no flags.
220///
221/// This is not "the command's flags plus its ancestors' globals". A subcommand
222/// that re-declares a global's long name is describing the *same* flag rather
223/// than a new one, so the global's help, argument and effect survive and only
224/// the re-declaration's extra aliases are added — see
225/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
226/// going through this will disagree with what the parser actually accepts.
227pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
228    let Some((root, rest)) = chain.split_first() else {
229        return vec![];
230    };
231    let mut available = gather_flags(root);
232    for cmd in rest {
233        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
234    }
235
236    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
237    // global that has both a short and a long, the merged flag is written under
238    // the long key while the short key keeps pointing at the pre-merge `Arc` —
239    // two objects for one logical flag. That is harmless for parsing, which
240    // looks flags up by key, but a caller listing flags would see it twice.
241    //
242    // Names break the tie because a long key always sorts before a short one
243    // (`--x` < `-y` at the second byte), so the merged declaration is the one
244    // reached first. Two genuinely distinct flags sharing a name is a spec bug
245    // that `usage lint` reports as a duplicate flag.
246    let mut seen_names = HashSet::new();
247    unique_flags(available.values())
248        .filter(|f| seen_names.insert(f.name.clone()))
249        .cloned()
250        .collect()
251}
252
253/// Extract the flag key from a flag word for lookup in available_flags map
254/// Handles both long flags (--flag, --flag=value) and short flags (-f)
255fn get_flag_key(word: &str) -> &str {
256    if word.starts_with("--") {
257        // Long flag: strip =value if present
258        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
259    } else if let Some((end, _)) = word.char_indices().nth(2) {
260        // Short flag: the dash and one letter, which is one character and not
261        // necessarily one byte.
262        &word[0..end]
263    } else {
264        word
265    }
266}
267
268/// Where a value came from, when it did not come from the command line.
269///
270/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
271/// token for the flag and none for the value, and that distinction is the whole question
272/// a spec author is asking when they ask why `--color` came out `always`. Values that were
273/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
274#[derive(Debug, Clone, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum ValueOrigin {
277    /// A flag that takes a value was given without one, so the declaration supplied it:
278    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
279    /// One variant for both, because from argv's side the same thing happened — the flag
280    /// was typed and the value was not.
281    DefaultMissing,
282    /// An environment variable, named.
283    ///
284    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
285    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
286    /// not say which declaration fired or which one to delete.
287    Env(String),
288    /// A declared `default`, on the flag or on the flag's argument.
289    ///
290    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
291    /// a fact about the value, and `usage lint` is the place to complain about declaring
292    /// both.
293    Default,
294    /// A `default_if` whose condition matched, with the condition that decided it. The
295    /// selector alone is ambiguous — several conditions may name it with different `when`
296    /// values.
297    DefaultIf {
298        selector: String,
299        when: Option<String>,
300    },
301}
302
303/// What one word of the command line became.
304///
305/// Several because a single token can do more than one thing: `-abc` sets three flags,
306/// `-j8` is a flag and its value.
307#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub enum TokenRole {
310    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
311    Program,
312    /// Selected a subcommand.
313    Command { name: String },
314    /// Named a flag, in this spelling. `negated` for the `negate` form.
315    Flag {
316        flag: Arc<SpecFlag>,
317        spelling: String,
318        negated: bool,
319    },
320    /// Supplied a flag's value. Several values when a `delimiter` split the word.
321    Value {
322        flag: Arc<SpecFlag>,
323        values: Vec<String>,
324        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
325        /// rather than following it as its own word.
326        attached: bool,
327    },
328    /// Filled a positional argument. Several values when a `delimiter` split the word.
329    Arg {
330        arg: Arc<SpecArg>,
331        values: Vec<String>,
332    },
333    /// An explicit `--`, consumed as a separator.
334    Separator,
335    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
336    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
337    /// without a role the word reads as having done nothing while a whole help page arrives
338    /// in the error list.
339    Builtin { spelling: String },
340    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
341    /// declaration whose run it closed — the word is not one of that run's values, which is
342    /// the whole reason it was declared.
343    ValueTerminator { ends: String },
344    /// A declared `restart_token`: the positional cursor and the values it had filled start
345    /// over here. Recorded because the words before it are still in the report, and without
346    /// this row they look like they filled arguments that then came back empty.
347    Restart,
348    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
349    /// under `unknown_flags="value"`, and `None` when the word was refused.
350    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
351    /// The word reached a declaration that would not take it, and was dropped. Without this
352    /// the token reads as having done nothing, which is the one thing it did not do.
353    Refused { reason: String },
354    /// Forwarded to an external subcommand.
355    External,
356    /// The parser stopped before this word — a help request, a refused value.
357    Unread,
358    /// Filled a sigil-classified positional after removing its declared prefix.
359    Sigil {
360        arg: Arc<SpecArg>,
361        sigil: String,
362        values: Vec<String>,
363    },
364    /// Ended one instance of a repeatable clause and began the next.
365    ClauseSeparator { name: String },
366}
367
368/// One word of the command line, and what it became.
369#[derive(Debug, Clone)]
370#[non_exhaustive]
371pub struct TokenBinding {
372    /// Position in the argv slice the parse was given, argv[0] included.
373    pub index: usize,
374    pub word: String,
375    /// Roles a word the parser made up contributed, folded onto the token it was derived
376    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
377    /// argv[0]. `word` is what the caller wrote, not what the parser read.
378    pub synthesized: bool,
379    pub roles: Vec<TokenRole>,
380}
381
382#[non_exhaustive]
383pub struct ParseOutput {
384    pub cmd: SpecCommand,
385    pub cmds: Vec<SpecCommand>,
386    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
387    /// Separator-delimited positional instances, keyed by clause name.
388    pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
389    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
390    /// What each word of the command line became, in argv order, one entry per word.
391    ///
392    /// The token half of provenance; [`ParseOutput::flag_origins`] and
393    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
394    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
395    /// a token that bound to nothing, so both exist.
396    pub tokens: Vec<TokenBinding>,
397    /// Where a flag's value came from when it did not come from argv, in the order the
398    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
399    ///
400    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
401    /// `default_missing` value, so one flag can have several.
402    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
403    /// Where an argument's value came from when it did not come from argv. Keyed as
404    /// [`ParseOutput::args`] is.
405    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
406    /// Flags a later occurrence removed, and the flag that removed them.
407    ///
408    /// The overriding name is the half a caller needs: the fallback phase silently declines
409    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
410    /// otherwise" has no answer without it.
411    pub overridden_flags: BTreeMap<String, String>,
412    /// Every flag the parser recognizes at this point, keyed by each of its aliases
413    /// (`--long`, `-s`, negations).
414    ///
415    /// This includes flags that only remain recognized because they may appear *before* a
416    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
417    /// should offer.
418    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
419    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
420    pub errors: Vec<UsageErr>,
421    /// Deprecated declarations this command line used, for the caller to render when its
422    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
423    /// not used anything yet.
424    pub warnings: Vec<Warning>,
425    /// The positional argument the next word would have filled, i.e. where the parser's
426    /// cursor stopped. `None` once every argument is satisfied.
427    ///
428    /// Completions need exactly this: the parser already accounts for `var_max`, for
429    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
430    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
431    pub next_arg: Option<Arc<SpecArg>>,
432    /// Whether an explicit `--` was consumed *as a separator*.
433    ///
434    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
435    /// of the variadic argument collecting it, not a separator, so it does not unlock a
436    /// `double_dash="required"` argument.
437    pub double_dash_seen: bool,
438    /// Remaining argv captured when an unmatched word was forwarded as an external
439    /// subcommand: the command name first, then every token after it.
440    ///
441    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
442    pub external: Option<Vec<String>>,
443}
444
445impl ParseOutput {
446    /// The flags a completion should offer for the parsed command.
447    ///
448    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
449    /// command has been reached, though, the commands above it belong to the mounting CLI and
450    /// their flags are not accepted there — mise, for example, forwards everything after a task
451    /// name to the task itself — so only the flags declared from the mount boundary down are
452    /// offered. Those globals stay in `available_flags` because they may legitimately appear
453    /// *before* the mounted command.
454    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
455        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
456            return self.available_flags.clone();
457        };
458        // A mount can also merge flags from its spec's root into the command it is mounted on
459        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
460        // replay starts one level up to inherit its globals.
461        let start = match boundary.checked_sub(1) {
462            Some(prev) if self.cmds[prev].flags_from_mount => prev,
463            _ => boundary,
464        };
465        // Re-run the descent from there, which starts with no inherited flags. Below the
466        // boundary the mounted program's commands are ordinary commands, so the descents use
467        // the same merge as the real parse.
468        let mut offered = gather_flags(&self.cmds[start]);
469        for cmd in &self.cmds[start + 1..] {
470            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
471        }
472        offered
473    }
474}
475
476#[derive(Debug, Clone)]
477pub enum ParseValue {
478    Bool(bool),
479    String(String),
480    MultiBool(Vec<bool>),
481    MultiString(Vec<String>),
482}
483
484impl ParseValue {
485    pub fn try_as_bool(self) -> Option<bool> {
486        match self {
487            Self::Bool(value) => Some(value),
488            _ => None,
489        }
490    }
491
492    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
493        match self {
494            Self::Bool(value) => Some(value),
495            _ => None,
496        }
497    }
498
499    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
500        match self {
501            Self::Bool(value) => Some(value),
502            _ => None,
503        }
504    }
505
506    pub fn try_as_string(self) -> Option<String> {
507        match self {
508            Self::String(value) => Some(value),
509            _ => None,
510        }
511    }
512
513    pub const fn try_as_string_ref(&self) -> Option<&String> {
514        match self {
515            Self::String(value) => Some(value),
516            _ => None,
517        }
518    }
519
520    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
521        match self {
522            Self::String(value) => Some(value),
523            _ => None,
524        }
525    }
526
527    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
528        match self {
529            Self::MultiBool(value) => Some(value),
530            _ => None,
531        }
532    }
533
534    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
535        match self {
536            Self::MultiBool(value) => Some(value),
537            _ => None,
538        }
539    }
540
541    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
542        match self {
543            Self::MultiBool(value) => Some(value),
544            _ => None,
545        }
546    }
547
548    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
549        match self {
550            Self::MultiString(value) => Some(value),
551            _ => None,
552        }
553    }
554
555    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
556        match self {
557            Self::MultiString(value) => Some(value),
558            _ => None,
559        }
560    }
561
562    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
563        match self {
564            Self::MultiString(value) => Some(value),
565            _ => None,
566        }
567    }
568}
569
570/// The deprecated declarations argv itself named: the commands it descended through, and the
571/// flags it bound.
572///
573/// Called before the environment and defaults have filled anything, because afterwards nothing
574/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
575/// differently, at the point where each is applied.
576///
577/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
578/// including `--help`, and the compiled parser reports selected commands rather than the one the
579/// process already is.
580fn collect_deprecations(out: &mut ParseOutput) {
581    for cmd in out.cmds.iter().skip(1) {
582        if cmd.deprecated.is_none()
583            && cmd.deprecated_warn_at.is_none()
584            && cmd.deprecated_remove_at.is_none()
585        {
586            continue;
587        }
588        out.warnings.push(Warning::command(
589            cmd.name.clone(),
590            cmd.deprecated.clone(),
591            cmd.deprecated_warn_at.clone(),
592            cmd.deprecated_remove_at.clone(),
593        ));
594    }
595    for flag in out.flags.keys() {
596        if let Some(warning) = flag_deprecation(flag) {
597            out.warnings.push(warning);
598        }
599    }
600}
601
602/// A warning for a flag that was used, if its declaration is deprecated at all.
603fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
604    if flag.deprecated.is_none()
605        && flag.deprecated_warn_at.is_none()
606        && flag.deprecated_remove_at.is_none()
607    {
608        return None;
609    }
610    Some(Warning::flag(
611        flag_spelling(flag),
612        flag.deprecated.clone(),
613        flag.deprecated_warn_at.clone(),
614        flag.deprecated_remove_at.clone(),
615    ))
616}
617
618/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
619/// about `old-flag` would be about a word nobody typed.
620fn flag_spelling(flag: &SpecFlag) -> String {
621    flag.long
622        .first()
623        .map(|long| format!("--{long}"))
624        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
625        .unwrap_or_else(|| flag.name.clone())
626}
627
628/// The name this flag reads first, which is what to use instead of a deprecated alias.
629fn flag_current_env(flag: &SpecFlag) -> Option<String> {
630    flag.env
631        .clone()
632        .or_else(|| flag.env_fallback.first().cloned())
633}
634
635fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
636    flag.deprecated_env.iter().any(|declared| declared == name)
637}
638
639/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
640fn arg_current_env(arg: &SpecArg) -> Option<String> {
641    arg.env
642        .clone()
643        .or_else(|| arg.env_fallback.first().cloned())
644}
645
646fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
647    arg.deprecated_env.iter().any(|declared| declared == name)
648}
649
650/// The first of `names` that is set, and which one it was.
651///
652/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
653/// aliases, so the winner's identity is what says whether a value arrived through an alias.
654/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
655/// disagree with it.
656fn first_set_env<'a>(
657    mut names: impl Iterator<Item = &'a str>,
658    get_env: &impl Fn(&str) -> Option<String>,
659) -> Option<(&'a str, String)> {
660    names.find_map(|name| get_env(name).map(|value| (name, value)))
661}
662
663/// Builder for parsing command-line arguments with custom options.
664///
665/// Use this when you need to customize parsing behavior, such as providing
666/// a custom environment variable map instead of using the process environment.
667///
668/// # Example
669/// ```
670/// use std::collections::HashMap;
671/// use usage::Spec;
672/// use usage::parse::Parser;
673///
674/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
675/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
676///
677/// let result = Parser::new(&spec)
678///     .with_env(env)
679///     .parse(&["cmd".into()])
680///     .unwrap();
681/// ```
682#[non_exhaustive]
683pub struct Parser<'a> {
684    spec: &'a Spec,
685    env: Option<HashMap<String, String>>,
686    mount_outputs: Option<HashMap<String, String>>,
687}
688
689impl<'a> Parser<'a> {
690    /// Create a new parser for the given spec.
691    pub fn new(spec: &'a Spec) -> Self {
692        Self {
693            spec,
694            env: None,
695            mount_outputs: None,
696        }
697    }
698
699    /// Use a custom environment variable map instead of the process environment.
700    ///
701    /// This is useful when parsing for tasks in a monorepo where the env vars
702    /// come from a child config file rather than the current process environment.
703    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
704        self.env = Some(env);
705        self
706    }
707
708    /// Inject deterministic outputs for mount commands instead of executing them.
709    ///
710    /// Keys are the exact `run` strings declared by mount nodes and values are the
711    /// usage specs those commands would print. When this is set, every encountered
712    /// mount must have an entry. Production parsing remains process-backed unless a
713    /// caller explicitly opts into injection.
714    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
715        self.mount_outputs = Some(outputs);
716        self
717    }
718
719    /// Parse the input arguments.
720    ///
721    /// Returns the parsed arguments and flags, with defaults and env vars applied.
722    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
723        let out = self.parse_collecting(input)?;
724        if let Some(err) = out
725            .errors
726            .iter()
727            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
728        {
729            bail!("{err}");
730        }
731        if !out.errors.is_empty() {
732            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
733        }
734        Ok(out)
735    }
736
737    /// Everything the parse learned, whether or not it succeeded.
738    ///
739    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
740    /// caller about to act on a command line. A caller that wants to *explain* one wants
741    /// the opposite: the bindings that worked and every complaint about the rest, since a
742    /// report saying only "missing required <src>" is the report you already had.
743    ///
744    /// Failures that stop the parse dead — a mount that will not run, a word no
745    /// declaration can take — still come back as `Err`. There is no output to describe in
746    /// those cases; see [`Parser::explain`] for what to do about it.
747    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
748        self.parse_collecting(input)
749    }
750
751    /// The binding phase's own answer for a line [`Parser::explain`] refused.
752    ///
753    /// `Ok` when the binding phase finished and the failure came after it — a flag left
754    /// waiting for a value, say. Everything argv supplied is there and only the
755    /// environment-and-defaults phase is missing.
756    ///
757    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
758    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
759    /// is more useful next to the three tokens that did bind than on its own. The word that
760    /// caused the failure carries a role saying so, and everything still queued behind it is
761    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
762    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
763    /// than in the line, such as a mount that will not run, stops the trace where it stopped
764    /// and the words past it carry no role.
765    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
766        let mut trace = Trace::new(input);
767        match parse_partial_traced(
768            self.spec,
769            input,
770            self.env.as_ref(),
771            self.mount_outputs.as_ref(),
772            MountTiming::WhenAWordIsUnknown,
773            &mut trace,
774        ) {
775            // A parse that got as far as stopping normally already moved its tokens onto the
776            // output, which is where a caller should read them from.
777            Ok((out, _)) => Ok(out),
778            Err(_) => Err(trace.tokens),
779        }
780    }
781
782    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
783        let custom_env = self.env.as_ref();
784        let (mut out, overridden_flags) = parse_partial_with_env(
785            self.spec,
786            input,
787            custom_env,
788            self.mount_outputs.as_ref(),
789            MountTiming::WhenAWordIsUnknown,
790        )?;
791        restore_current_clause(&mut out);
792        trace!("{out:?}");
793
794        // A flag still waiting for a value never got one, so the command line ended
795        // mid-flag. `parse_partial` leaves this for completions to look at — a
796        // half-typed `--jobs ` is exactly what a completion is asked about — but a
797        // full parse has nothing left to wait for, and dropping the flag silently
798        // made a forgotten value look like a working command.
799        while try_bind_default_missing(
800            &mut out.flags,
801            &mut out.flag_awaiting_value,
802            custom_env,
803            &mut out.flag_origins,
804        )? {}
805        if let Some(flag) = out.flag_awaiting_value.first() {
806            let token = flag
807                .long
808                .first()
809                .map(|l| format!("--{l}"))
810                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
811                .unwrap_or_else(|| flag.name.clone());
812            let rendered = input.join(" ");
813            let span = rendered
814                .rfind(&token)
815                .map(|at| (at, token.len()))
816                .unwrap_or((0, 0));
817            return Err(UsageErr::InvalidFlag {
818                token,
819                reason: "requires an argument".to_string(),
820                span: span.into(),
821                input: rendered,
822            }
823            .into());
824        }
825
826        // Before the environment and defaults have their turn, because both mark a field as
827        // filled and only argv can be reported as something the user typed. Env is reported
828        // where it is applied, below; a default is nobody's request and reports nothing.
829        collect_deprecations(&mut out);
830
831        let get_env = |key: &str| -> Option<String> {
832            if let Some(env_map) = custom_env {
833                env_map.get(key).cloned()
834            } else {
835                std::env::var(key).ok()
836            }
837        };
838
839        // Apply env vars and defaults for args
840        //
841        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
842        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
843        for arg in active_args(&out.cmd) {
844            // Clause instances contain argv only: defaults and environment values do not
845            // manufacture fields inside a repeated group.
846            if out.cmd.clause.is_some() {
847                break;
848            }
849            if out.args.contains_key(arg) {
850                continue;
851            }
852            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
853                if arg_env_is_deprecated(arg, env_name) {
854                    out.warnings
855                        .push(Warning::env(env_name, arg_current_env(arg)));
856                }
857                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
858                validate_choice_values(
859                    ChoiceTarget::arg(arg),
860                    &values,
861                    arg.choices.as_ref(),
862                    custom_env,
863                )?;
864                let parsed = if arg.var {
865                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
866                    ParseValue::MultiString(values)
867                } else {
868                    ParseValue::String(values.into_iter().next().unwrap_or_default())
869                };
870                out.args.insert(Arc::new(arg.clone()), parsed);
871                out.arg_origins
872                    .entry(Arc::new(arg.clone()))
873                    .or_default()
874                    .push(ValueOrigin::Env(env_name.to_string()));
875                continue;
876            }
877            if !arg.default.is_empty() {
878                // Consider var when deciding the type of default return value
879                if arg.var {
880                    let values = split_fallback_values(&arg.default, arg.delimiter);
881                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
882                    validate_choice_values(
883                        ChoiceTarget::arg(arg),
884                        &values,
885                        arg.choices.as_ref(),
886                        custom_env,
887                    )?;
888                    // For var=true, always return a vec (MultiString)
889                    out.args
890                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
891                    out.arg_origins
892                        .entry(Arc::new(arg.clone()))
893                        .or_default()
894                        .push(ValueOrigin::Default);
895                } else {
896                    validate_choice_value(
897                        ChoiceTarget::arg(arg),
898                        &arg.default[0],
899                        arg.choices.as_ref(),
900                        custom_env,
901                    )?;
902                    // For var=false, return the first default value as String
903                    out.args.insert(
904                        Arc::new(arg.clone()),
905                        ParseValue::String(arg.default[0].clone()),
906                    );
907                    out.arg_origins
908                        .entry(Arc::new(arg.clone()))
909                        .or_default()
910                        .push(ValueOrigin::Default);
911                }
912            }
913        }
914
915        // Environment first, for every flag, so a `default_if` can see a sibling
916        // that was filled from env. Applying both in one pass would make the
917        // answer depend on declaration order: `--bin-names` before `--json`
918        // would miss `EX_JSON=1`.
919        let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
920        for flag in &flags {
921            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
922                continue;
923            }
924            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
925                // The flag's own deprecation before the alias's, which is the order the
926                // compiled parser reports them in: it walks a command's flags and then its
927                // aliases. Using a deprecated flag through a variable is still using it.
928                if let Some(warning) = flag_deprecation(flag) {
929                    out.warnings.push(warning);
930                }
931                if flag_env_is_deprecated(flag, env_name) {
932                    out.warnings
933                        .push(Warning::env(env_name, flag_current_env(flag)));
934                }
935                if let Some(arg) = flag.arg.as_ref() {
936                    let values =
937                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
938                    validate_choice_values(
939                        ChoiceTarget::option(flag),
940                        &values,
941                        arg.choices.as_ref(),
942                        custom_env,
943                    )?;
944                    let parsed = if flag.var || arg.var {
945                        if flag.var {
946                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
947                        }
948                        if arg.var {
949                            validate_flag_arg_fallback_count(
950                                flag,
951                                arg,
952                                values.len(),
953                                &mut out.errors,
954                            );
955                        }
956                        ParseValue::MultiString(values)
957                    } else {
958                        ParseValue::String(values.into_iter().next().unwrap_or_default())
959                    };
960                    out.flags.insert(Arc::clone(flag), parsed);
961                } else {
962                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
963                    out.flags
964                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
965                }
966                out.flag_origins
967                    .entry(Arc::clone(flag))
968                    .or_default()
969                    .push(ValueOrigin::Env(env_name.to_string()));
970            }
971        }
972        // Decide every `default_if` against argv+env only. Binding as we go would put
973        // a default into `out.flags` and make the next flag's condition treat it as
974        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
975        // here, so an unconditional `default` on `--json` must not fire
976        // `default_if "--json"`.
977        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
978        for flag in &flags {
979            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
980                continue;
981            }
982            if let Some(condition) = flag.default_if.iter().find(|condition| {
983                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
984            }) {
985                from_default_if.push((Arc::clone(flag), condition.clone()));
986            }
987        }
988        for (flag, condition) in &from_default_if {
989            // The whole condition, not just the value: several conditions may name the same
990            // selector with different `when` values, so the selector alone does not say
991            // which one fired.
992            bind_flag_fallback(
993                flag,
994                std::slice::from_ref(&condition.value),
995                &mut out,
996                custom_env,
997                ValueOrigin::DefaultIf {
998                    selector: condition.selector.clone(),
999                    when: condition.when.clone(),
1000                },
1001            )?;
1002        }
1003        for flag in &flags {
1004            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1005                continue;
1006            }
1007            if !flag.default.is_empty() {
1008                bind_flag_fallback(
1009                    flag,
1010                    &flag.default,
1011                    &mut out,
1012                    custom_env,
1013                    ValueOrigin::Default,
1014                )?;
1015                continue;
1016            }
1017            if let Some(arg) = flag.arg.as_ref() {
1018                if !arg.default.is_empty() {
1019                    bind_flag_fallback(
1020                        flag,
1021                        &arg.default,
1022                        &mut out,
1023                        custom_env,
1024                        ValueOrigin::Default,
1025                    )?;
1026                }
1027            }
1028        }
1029        // Declarative value validation is deliberately post-binding. Defaults and
1030        // environment fallbacks have landed by here, and delimiters were already split
1031        // while binding. Like clap's value parsers, a declaration judges each resulting
1032        // raw value independently.
1033        for (arg, parsed) in &out.args {
1034            validate_expression(
1035                &arg.name,
1036                arg.validate.as_deref(),
1037                arg.validate_error.as_deref(),
1038                parsed,
1039                &mut out.errors,
1040            );
1041        }
1042        if let Some(clause) = &out.cmd.clause {
1043            let mut clause_errors = Vec::new();
1044            for (index, instance) in out
1045                .clauses
1046                .get(&clause.name)
1047                .into_iter()
1048                .flatten()
1049                .chain(std::iter::once(&out.args))
1050                .enumerate()
1051            {
1052                for arg in &clause.args {
1053                    let Some(value) = instance.get(arg) else {
1054                        if arg.required {
1055                            clause_errors.push(UsageErr::MissingClauseArg {
1056                                clause: clause.name.clone(),
1057                                instance: index + 1,
1058                                arg: arg.name.clone(),
1059                            });
1060                        }
1061                        continue;
1062                    };
1063                    if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1064                        if let Some(min) = arg.var_min {
1065                            if values.len() < min {
1066                                clause_errors.push(UsageErr::VarArgTooFew {
1067                                    name: format!(
1068                                        "{} instance {}: {}",
1069                                        clause.name,
1070                                        index + 1,
1071                                        arg.name
1072                                    ),
1073                                    min,
1074                                    got: values.len(),
1075                                });
1076                            }
1077                        }
1078                        if let Some(max) = arg.var_max {
1079                            if values.len() > max {
1080                                clause_errors.push(UsageErr::VarArgTooMany {
1081                                    name: format!(
1082                                        "{} instance {}: {}",
1083                                        clause.name,
1084                                        index + 1,
1085                                        arg.name
1086                                    ),
1087                                    max,
1088                                    got: values.len(),
1089                                });
1090                            }
1091                        }
1092                    }
1093                }
1094            }
1095            out.errors.extend(clause_errors);
1096        }
1097        for (flag, parsed) in &out.flags {
1098            if let Some(arg) = &flag.arg {
1099                validate_expression(
1100                    &flag.name,
1101                    arg.validate.as_deref(),
1102                    arg.validate_error.as_deref(),
1103                    parsed,
1104                    &mut out.errors,
1105                );
1106            }
1107        }
1108        // Applied once, here, because this is where the CLI's own version is known: a
1109        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1110        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1111        finalize_current_clause(&mut out);
1112        Ok(out)
1113    }
1114}
1115
1116/// Parse command-line arguments according to a spec.
1117///
1118/// Returns the parsed arguments and flags, with defaults and env vars applied.
1119/// Uses `std::env::var` for environment variable lookups.
1120///
1121/// For custom environment variable handling, use [`Parser`] instead.
1122#[must_use = "parsing result should be used"]
1123pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1124    Parser::new(spec).parse(input)
1125}
1126
1127/// Parse command-line arguments without applying defaults.
1128///
1129/// Use this for help text generation or when you need the raw parsed values.
1130#[must_use = "parsing result should be used"]
1131pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1132    parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
1133}
1134
1135/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1136/// `.exe` stripped so Windows and Unix agree.
1137pub fn multicall_basename(argv0: &str) -> &str {
1138    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1139    match name.get(name.len().saturating_sub(4)..) {
1140        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1141        _ => name,
1142    }
1143}
1144
1145/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1146///
1147/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1148/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1149pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1150    let base = multicall_basename(argv0);
1151    if !name.is_empty() && base == multicall_basename(name) {
1152        return None;
1153    }
1154    if let Some(bin) = bin {
1155        if !bin.is_empty() && base == multicall_basename(bin) {
1156            return None;
1157        }
1158    }
1159    Some(base)
1160}
1161
1162/// Internal version of parse_partial that accepts an optional custom env map.
1163/// When a command's own `mount` runs, for the root — which nothing descends into.
1164///
1165/// A completion has to know every command before it can offer one, even with
1166/// nothing typed yet, so it resolves up front. An execution knows the word it was
1167/// given, so it only pays for discovery when that word matches nothing declared —
1168/// and a CLI that declares its commands and mounts a few more does not spawn a
1169/// process on every invocation.
1170#[derive(Clone, Copy, PartialEq, Eq)]
1171enum MountTiming {
1172    Eager,
1173    WhenAWordIsUnknown,
1174}
1175
1176/// One word on its way through the parser, with what the parser has learned about it.
1177///
1178/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1179/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1180/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1181/// the word itself there is nothing to keep aligned. The argv position is here for the
1182/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1183/// removed from the middle, so position in the queue stops meaning position in argv on the
1184/// first descent.
1185struct Token {
1186    word: String,
1187    /// Where in the caller's argv this word came from.
1188    ///
1189    /// A word the parser made up points at the token it was derived from — the tail of a
1190    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1191    /// token a reader would point at, and there is nothing else to point at.
1192    argv: usize,
1193    /// The flag Phase 1 read this word as, and the command level it read it at.
1194    ///
1195    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1196    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1197    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1198    /// then the recognized flags have changed, because each descent drops the parent's
1199    /// non-global flags and a mounted command may declare the same name as a global seen
1200    /// here. Recording the owner keeps a word bound to the flag it was read as.
1201    ///
1202    /// The level matters to strict parsing: clap permits an inherited global once on each
1203    /// side of a subcommand boundary.
1204    binding: Option<(Arc<SpecFlag>, usize)>,
1205}
1206
1207impl Token {
1208    fn new(word: String, argv: usize) -> Self {
1209        Self {
1210            word,
1211            argv,
1212            binding: None,
1213        }
1214    }
1215}
1216
1217/// The token trace, while it is being built.
1218///
1219/// One row per word of the caller's argv, so a role can be recorded against a position
1220/// without the recorder having to know how many words came before it. Words the parser
1221/// made up have no row of their own and fold onto the row they were derived from.
1222struct Trace {
1223    tokens: Vec<TokenBinding>,
1224}
1225
1226impl Trace {
1227    fn new(input: &[String]) -> Self {
1228        Self {
1229            tokens: input
1230                .iter()
1231                .enumerate()
1232                .map(|(index, word)| TokenBinding {
1233                    index,
1234                    word: word.clone(),
1235                    synthesized: false,
1236                    roles: vec![],
1237                })
1238                .collect(),
1239        }
1240    }
1241
1242    fn record(&mut self, argv: usize, role: TokenRole) {
1243        if let Some(token) = self.tokens.get_mut(argv) {
1244            token.roles.push(role);
1245        }
1246    }
1247
1248    /// Note that what was read at this position is not what the caller wrote there.
1249    fn note_synthesized(&mut self, argv: usize) {
1250        if let Some(token) = self.tokens.get_mut(argv) {
1251            token.synthesized = true;
1252        }
1253    }
1254
1255    /// Every word the parse never reached, once it has stopped.
1256    fn close(&mut self, unread: &VecDeque<Token>) {
1257        for token in unread {
1258            self.record(token.argv, TokenRole::Unread);
1259        }
1260    }
1261}
1262
1263fn parse_partial_with_env(
1264    spec: &Spec,
1265    input: &[String],
1266    custom_env: Option<&HashMap<String, String>>,
1267    mount_outputs: Option<&HashMap<String, String>>,
1268    mount_timing: MountTiming,
1269) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1270    let mut trace = Trace::new(input);
1271    parse_partial_traced(
1272        spec,
1273        input,
1274        custom_env,
1275        mount_outputs,
1276        mount_timing,
1277        &mut trace,
1278    )
1279}
1280
1281/// The binding phase, with the trace left somewhere the caller can still read it.
1282///
1283/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1284/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1285/// words read before the failure are most of what a report wants, so the caller owns the
1286/// trace instead and keeps them. See [`Parser::explain_refused`].
1287fn parse_partial_traced(
1288    spec: &Spec,
1289    input: &[String],
1290    custom_env: Option<&HashMap<String, String>>,
1291    mount_outputs: Option<&HashMap<String, String>>,
1292    mount_timing: MountTiming,
1293    trace: &mut Trace,
1294) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1295    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1296        let viewed = spec.for_view(view)?;
1297        return parse_partial_traced(
1298            &viewed,
1299            input,
1300            custom_env,
1301            mount_outputs,
1302            mount_timing,
1303            trace,
1304        );
1305    }
1306    trace!("parse_partial: {input:?}");
1307    let mut input = input
1308        .iter()
1309        .enumerate()
1310        .map(|(argv, word)| Token::new(word.clone(), argv))
1311        .collect::<VecDeque<_>>();
1312    let argv0 = input.pop_front();
1313    if let Some(argv0) = argv0.as_ref() {
1314        trace.record(argv0.argv, TokenRole::Program);
1315    }
1316    if spec.multicall {
1317        if let Some(raw) = argv0 {
1318            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1319                // A symlink invocation reads a word the caller never typed — the basename of
1320                // the program itself — so argv[0] is both the program and, below, whatever
1321                // that word selects.
1322                trace.note_synthesized(raw.argv);
1323                input.push_front(Token::new(applet.to_string(), raw.argv));
1324            }
1325        }
1326    }
1327    // The policy observes the selected command's own argv, not values eventually filled from
1328    // env/default. Start at the root, then reset on every explicit descent. A default
1329    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1330    let mut command_has_argv = !input.is_empty();
1331
1332    let mut out = ParseOutput {
1333        cmd: spec.cmd.clone(),
1334        cmds: vec![spec.cmd.clone()],
1335        args: IndexMap::new(),
1336        clauses: IndexMap::new(),
1337        flags: IndexMap::new(),
1338        tokens: vec![],
1339        flag_origins: IndexMap::new(),
1340        arg_origins: IndexMap::new(),
1341        overridden_flags: BTreeMap::new(),
1342        available_flags: gather_flags(&spec.cmd),
1343        flag_awaiting_value: vec![],
1344        errors: vec![],
1345        warnings: vec![],
1346        next_arg: None,
1347        double_dash_seen: false,
1348        external: None,
1349    };
1350    // Keep this internal so adding relationship support remains semver-compatible. The full
1351    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1352    let mut overridden_flags = HashSet::new();
1353    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1354    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1355    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1356    // or an inherited `-c` belonged to the ancestor.
1357    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1358
1359    // Phase 1: Scan for subcommands and collect global flags
1360    //
1361    // This phase identifies subcommands early because they may have mount points
1362    // that need to be executed with the global flags that appeared before them.
1363    //
1364    // Example: "usage --verbose run task"
1365    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1366    //   -> then finds "task" as a subcommand of "run" (if it exists)
1367    //
1368    // We only collect global flags for mounts because:
1369    // - Non-global flags are specific to the current command, not subcommands
1370    // - Global flags affect all commands and should be passed to mount points
1371    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1372    // Which flag each word skipped here belongs to is recorded on the word — see
1373    // `Token::binding`.
1374    let mut command_arg_found = false;
1375    let mut variadic_flag_active = false;
1376    let mut idx = 0;
1377    // Track whether we've already applied the default_subcommand to prevent
1378    // multiple switches (e.g., if default is "run" and there's a task named "run")
1379    let mut used_default_subcommand = false;
1380    // Whether the command in scope has had its own mounts run. A mount on the root
1381    // is the case that needs this: a subcommand's mounts are run when the parser
1382    // descends into it, but nothing descends into the root.
1383    let mut mounts_resolved = false;
1384    // A completion needs the whole command list before it can offer anything, and
1385    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1386    // mean a root mount never contributed to the very thing it exists for.
1387    //
1388    // The default-subcommand gate applies here too, and has to: offering a discovered
1389    // command that a real parse would hand to the default instead would be worse than
1390    // not offering it. A root mount under a `default_subcommand` that does not say
1391    // `overrides_default` therefore contributes nothing anywhere, which is what
1392    // "the default outranks discovery" means.
1393    let default_outranks_mounts =
1394        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1395    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1396    {
1397        mounts_resolved = true;
1398        let mut mounted = out.cmd.clone();
1399        mounted.mount(&[], mount_outputs)?;
1400        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1401        if let Some(last) = out.cmds.last_mut() {
1402            *last = mounted.clone();
1403        }
1404        out.cmd = mounted;
1405    }
1406
1407    while idx < input.len() {
1408        // Only for a word that could name a command, and only when it matches
1409        // nothing already declared. A CLI that declares its commands and mounts more
1410        // does not spawn a process for every invocation, and a flag — `--help`, or
1411        // anything unrecognized — never triggers discovery at all, which it would
1412        // otherwise do simply by not being a subcommand.
1413        // A declared `default_subcommand` already says what an unmatched word means,
1414        // and it costs nothing — so discovery waits behind it unless a mount asks to
1415        // outrank it. Without this, a task runner would spawn its discovery process
1416        // once per task invocation.
1417        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1418            default_accepts_word(&out.cmd, name, &input[idx].word)
1419                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1420        });
1421        if !mounts_resolved
1422            && !out.cmd.mounts.is_empty()
1423            && !default_catches_it
1424            && is_command_word(&input[idx].word)
1425            && !is_negative_number(&input[idx].word)
1426            && out.cmd.find_subcommand(&input[idx].word).is_none()
1427        {
1428            mounts_resolved = true;
1429            let mut mounted = out.cmd.clone();
1430            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1431            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1432            if let Some(last) = out.cmds.last_mut() {
1433                *last = mounted.clone();
1434            }
1435            out.cmd = mounted;
1436        }
1437        if variadic_flag_active
1438            && out.cmd.find_subcommand(&input[idx].word).is_some()
1439            && !out.cmd.subcommand_precedence_over_arg
1440        {
1441            break;
1442        }
1443        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1444            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1445                bail!(
1446                    "subcommand '{}' cannot be used with arguments on its parent command",
1447                    input[idx].word
1448                );
1449            }
1450            let mut subcommand = subcommand.clone();
1451            // Pass prefix words (global flags before this subcommand) to mount
1452            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1453            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1454            // commands are ordinary commands relative to each other.
1455            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1456            merge_subcommand_flags(
1457                &mut out.available_flags,
1458                gather_flags(&subcommand),
1459                crossing_mount,
1460            );
1461            // Remove subcommand from input
1462            let selected = input.remove(idx);
1463            if let Some(selected) = selected {
1464                trace.record(
1465                    selected.argv,
1466                    TokenRole::Command {
1467                        name: subcommand.name.clone(),
1468                    },
1469                );
1470            }
1471            command_has_argv = idx < input.len();
1472            out.cmds.push(subcommand.clone());
1473            out.cmd = subcommand.clone();
1474            // A descent already ran the new command's mounts, above.
1475            mounts_resolved = true;
1476            prefix_flags.clear();
1477            command_arg_found = false;
1478            variadic_flag_active = false;
1479            // Continue from current position (don't reset to 0)
1480            // After remove(), idx now points to the next element
1481        } else if !is_command_word(&input[idx].word)
1482            || declared_numeric_short(&out.available_flags, &input[idx].word)
1483        {
1484            // Check if this is a known flag
1485            let word = input[idx].word.clone();
1486            let flag_key = get_flag_key(&word);
1487
1488            // A short token keys on its first letter, so `-az` would be recorded as
1489            // `-a` and its tail left over. Check the whole token here, where it is
1490            // first read: a token containing an unrecognized letter is not a bundle,
1491            // and recording it as one is what let `-a` be applied from a token that
1492            // never named it.
1493            let is_bundle = word.starts_with("--")
1494                || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1495            if let Some(f) = out
1496                .available_flags
1497                .get(flag_key)
1498                .cloned()
1499                .filter(|_| is_bundle)
1500            {
1501                command_arg_found = true;
1502                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1503                // Skip the flag and keep scanning. Both global and non-global flags may precede
1504                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1505                // stopping at one would hide the subcommand — and any mount on it — from the
1506                // parse, leaving the subcommand name to be mis-read as a positional argument.
1507                //
1508                // Only globals are forwarded to mounts: a non-global flag belongs to the
1509                // command that declared it, not to what is mounted below it.
1510                input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1511                let mut forwarded = f.global.then(|| vec![word.clone()]);
1512                idx += 1;
1513
1514                // Only consume next word if flag takes an argument AND value isn't embedded
1515                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1516                if f.arg.is_some()
1517                    && !word.contains('=')
1518                    && idx < input.len()
1519                    && accepts_detached_flag_value(&f, &input[idx].word)
1520                {
1521                    if let Some(words) = forwarded.as_mut() {
1522                        words.push(input[idx].word.clone());
1523                    }
1524                    idx += 1;
1525                }
1526                if let Some(words) = forwarded {
1527                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1528                    prefix_flags.push((f, words));
1529                }
1530            } else {
1531                // Unknown flag - stop looking for subcommands
1532                // Let the main parsing phase handle the error
1533                break;
1534            }
1535        } else {
1536            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1537                idx += 1;
1538                continue;
1539            }
1540            // Found a word that's not a flag or subcommand
1541            // Check if we should use the default_subcommand (only once, and only at the
1542            // root, which is the only place a spec can declare one — `out.cmds` holds just
1543            // the root until something descends). Without that second condition the one
1544            // declared name is looked up wherever the parser happens to be standing, so an
1545            // unrelated command acquires a default because a name matched one level down:
1546            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1547            // `config ls`.
1548            if !used_default_subcommand && out.cmds.len() == 1 {
1549                if let Some(default_name) = &spec.default_subcommand {
1550                    if let Some(subcommand) = out
1551                        .cmd
1552                        .find_subcommand(default_name)
1553                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1554                    {
1555                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1556                            bail!(
1557                                "subcommand '{}' cannot be used with arguments on its parent command",
1558                                subcommand.name
1559                            );
1560                        }
1561                        let mut subcommand = subcommand.clone();
1562                        // Pass prefix words (global flags before this) to mount
1563                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1564                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1565                        merge_subcommand_flags(
1566                            &mut out.available_flags,
1567                            gather_flags(&subcommand),
1568                            crossing_mount,
1569                        );
1570                        out.cmds.push(subcommand.clone());
1571                        out.cmd = subcommand.clone();
1572                        command_has_argv = true;
1573                        prefix_flags.clear();
1574                        command_arg_found = false;
1575                        variadic_flag_active = false;
1576                        // This descent ran the new command's mounts, so lazy
1577                        // discovery must not run them a second time.
1578                        mounts_resolved = true;
1579                        used_default_subcommand = true;
1580                        // Continue the loop to check if this word is a subcommand of the
1581                        // default subcommand (e.g., a task name added via mount).
1582                        // If it's not a subcommand, the next iteration will break and
1583                        // Phase 2 will handle it as a positional arg.
1584                        continue;
1585                    }
1586                }
1587            }
1588            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1589            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1590            // default subcommand gets first refusal so interpreted and compiled routing agree
1591            // when the root sigil and the default command can both accept this word.
1592            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1593                idx += 1;
1594                continue;
1595            }
1596            // An unmatched word that names no subcommand is forwarded as an external
1597            // command: this word, then every token after it, including flags. Known
1598            // subcommands already won above, and a default_subcommand already caught.
1599            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1600            if out.cmd.external_subcommand {
1601                let rest: Vec<Token> = input.drain(idx..).collect();
1602                for token in &rest {
1603                    trace.record(token.argv, TokenRole::External);
1604                }
1605                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1606                break;
1607            }
1608            // This could be a positional argument, so stop subcommand search
1609            break;
1610        }
1611    }
1612
1613    // Phase 2: Main argument and flag parsing
1614    //
1615    // Now that we've identified all subcommands and executed their mounts,
1616    // we can parse the remaining arguments, flags, and their values.
1617
1618    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
1619    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
1620    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1621    // argument filled?" has to consult `out.args` by key instead of counting.
1622    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1623    let mut enable_flags = true;
1624    let mut grouped_flag = false;
1625    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1626    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1627    // words that come after it — see `report_double_dash_violation`.
1628    let mut seen_double_dash = false;
1629    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1630    // reopen sigil classification for this invocation.
1631    let mut restart_seen = false;
1632    // Args already reported as having been offered a word before the `--` they require, so a
1633    // variadic one does not report the same violation for every word it is offered.
1634    let mut double_dash_violations: HashSet<String> = HashSet::new();
1635    // Scalar occurrences are scoped to the command level where they were written. Inherited
1636    // globals may therefore appear once before and once after a subcommand under clap's strict
1637    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1638    // opposite forms may override one another, while repeating either spelling is an error.
1639    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1640
1641    while !input.is_empty() {
1642        let token = input.pop_front().unwrap();
1643        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1644        let binding = token.binding;
1645        let argv = token.argv;
1646        let mut w = token.word;
1647        // A short's attached value is re-queued with `grouped_flag` set, and that
1648        // continuation is not a following word. `require_equals` refuses only the
1649        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1650        // only when the value is actually missing, so `-cnever` is still `never`.
1651        let attached_continuation = grouped_flag;
1652
1653        // A clause boundary is syntax even after an automatic trailing argument disabled
1654        // flags. Only an explicit `--` protects a literal separator.
1655        if !seen_double_dash {
1656            if let Some(clause) = out.cmd.clause.as_ref() {
1657                if w == clause.separator {
1658                    let name = clause.name.clone();
1659                    out.clauses
1660                        .entry(name.clone())
1661                        .or_default()
1662                        .push(std::mem::take(&mut out.args));
1663                    out.arg_origins.clear();
1664                    trace.record(argv, TokenRole::ClauseSeparator { name });
1665                    next_arg_idx = 0;
1666                    out.flag_awaiting_value.clear();
1667                    enable_flags = true;
1668                    seen_double_dash = false;
1669                    continue;
1670                }
1671            }
1672        }
1673
1674        // Check for restart_token - resets argument parsing for multiple command invocations
1675        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1676        if let Some(ref restart_token) = out.cmd.restart_token {
1677            if w == *restart_token {
1678                // Reset argument parsing state for a fresh command invocation, keeping the
1679                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1680                // is not cleared here either, so clearing it would let one arg report the same
1681                // violation once per invocation.
1682                out.args.clear();
1683                // With the values gone, so is where they came from — otherwise the second
1684                // invocation of `run lint ::: test` reports the first one's provenance. The
1685                // token trace is *not* cleared: those words were read, and a report that
1686                // dropped them would show a command line with a hole in it.
1687                out.arg_origins.clear();
1688                trace.record(argv, TokenRole::Restart);
1689                next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1690                restart_seen = true;
1691                out.flag_awaiting_value.clear(); // Clear any pending flag values
1692                enable_flags = true; // Reset -- separator effect
1693                seen_double_dash = false; // The next invocation needs its own `--`
1694                continue;
1695            }
1696        }
1697
1698        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1699        // like, and that has to be asked before the separator arm below rather than
1700        // after it. Asked after, a `--` was consumed as a separator while the flag
1701        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1702        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1703        // itself, which is what clap does with the same declaration — and no flag can
1704        // still be waiting once the separator has done its job, so the starvation rule
1705        // below has no path around it.
1706        if enable_flags
1707            && !attached_continuation
1708            && w.starts_with('-')
1709            && out
1710                .flag_awaiting_value
1711                .last()
1712                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1713        {
1714            // A variadic argument collects here too: which token supplied its first
1715            // value says nothing about how many it takes.
1716            let should_return = bind_pending_flag_value(
1717                spec,
1718                &out.cmd,
1719                &mut out.errors,
1720                &mut out.flags,
1721                &mut out.flag_awaiting_value,
1722                &mut w,
1723                &mut input,
1724                custom_env,
1725                trace,
1726                argv,
1727                // The token a hyphen-valued flag takes is the following word, never attached.
1728                false,
1729            )?;
1730            if should_return {
1731                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1732                return Ok((out, overridden_flags));
1733            }
1734            continue;
1735        }
1736
1737        // A flag whose value may be omitted that cannot take this token as a detached
1738        // value finishes bare and leaves the token for whatever comes next:
1739        // `--color --verbose` colours with the missing value and still sets verbose,
1740        // and `--inspect 9229` with `require_equals` binds the missing value rather
1741        // than treating 9229 as the port.
1742        if enable_flags
1743            && !attached_continuation
1744            && !out.flag_awaiting_value.is_empty()
1745            && out.flag_awaiting_value.last().is_some_and(|flag| {
1746                (flag.default_missing.is_some() || flag.value_optional)
1747                    && !accepts_detached_flag_value(flag, &w)
1748            })
1749        {
1750            try_bind_default_missing(
1751                &mut out.flags,
1752                &mut out.flag_awaiting_value,
1753                custom_env,
1754                &mut out.flag_origins,
1755            )?;
1756        }
1757
1758        // The first explicit `--` is still a separator after an `automatic` argument has
1759        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1760        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1761        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1762        // behavior as the bug it is.
1763        if w == "--" && !seen_double_dash {
1764            enable_flags = false;
1765
1766            // Only preserve the double dash token if we're collecting values for a variadic arg
1767            // in double_dash == `preserve` mode
1768            let should_preserve = active_args(&out.cmd)
1769                .get(next_arg_idx)
1770                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1771                .unwrap_or(false);
1772
1773            if should_preserve {
1774                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1775                // neither counts as one nor unlocks a `double_dash="required"` arg.
1776            } else {
1777                seen_double_dash = true;
1778                trace.record(argv, TokenRole::Separator);
1779
1780                // Everything after an explicit `--` belongs to the arg that requires one, so
1781                // jump the cursor there — past any earlier arg, including a greedy variadic
1782                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1783                // which is what `double_dash="required"` is generated from. Specs without such
1784                // an arg find nothing and keep the cursor where it was.
1785                let target = active_args(&out.cmd).iter().position(|arg| {
1786                    arg.double_dash == SpecDoubleDashChoices::Required
1787                        && !out.args.contains_key(arg)
1788                });
1789                if let Some(target) = target {
1790                    // Forward only. An unfilled required arg declared *before* the cursor is
1791                    // left where it is rather than rewound to — words already assigned to
1792                    // later args would have to be taken back for that to mean anything, and
1793                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1794                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1795                    // one ahead of others is already outside what this models.
1796                    if target > next_arg_idx {
1797                        next_arg_idx = target;
1798                    }
1799                }
1800                continue;
1801            }
1802        }
1803
1804        // long flags
1805        if enable_flags && w.starts_with("--") {
1806            grouped_flag = false;
1807            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1808            // an empty value while `--jobs` supplies none. Collapsing the two lost
1809            // the flag entirely.
1810            let split = w.split_once('=');
1811            let word = split.map(|(word, _)| word).unwrap_or(&w);
1812            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1813            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1814                let command_level = binding
1815                    .as_ref()
1816                    .map(|(_, level)| *level)
1817                    .unwrap_or(out.cmds.len() - 1);
1818                parsed_flag_spellings
1819                    .entry(Arc::as_ptr(f) as usize)
1820                    .or_default()
1821                    .insert(word.to_string());
1822                // Recorded before the action check below: a token that named a flag named it
1823                // whether or not the parse can carry on afterwards.
1824                trace.record(
1825                    argv,
1826                    TokenRole::Flag {
1827                        flag: Arc::clone(f),
1828                        spelling: word.to_string(),
1829                        negated: f.negate.as_deref() == Some(word),
1830                    },
1831                );
1832                if f.action != crate::SpecFlagAction::Set {
1833                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1834                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1835                    return Ok((out, overridden_flags));
1836                }
1837                apply_flag_overrides(
1838                    f,
1839                    &out.available_flags,
1840                    &mut out.flags,
1841                    &mut out.flag_awaiting_value,
1842                    &mut overridden_flags,
1843                    &mut out.overridden_flags,
1844                );
1845                if let Some(pending) = out.flag_awaiting_value.first() {
1846                    out.errors.push(render_missing_flag_value(pending, &w));
1847                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1848                    return Ok((out, overridden_flags));
1849                }
1850                // An attached value only means something to a flag that takes one:
1851                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1852                // give a flag that holds no value. Handing that leftover to the
1853                // positionals would re-split one token into two, so `ex --force=yes`
1854                // would fill an argument the caller never typed a word for.
1855                if f.arg.is_some() {
1856                    record_scalar_flag_occurrence(
1857                        &out.cmds,
1858                        f,
1859                        command_level,
1860                        None,
1861                        &mut scalar_occurrences,
1862                        &mut out.errors,
1863                    );
1864                    let f = Arc::clone(f);
1865                    out.flag_awaiting_value.push(Arc::clone(&f));
1866                    // The `=` has already settled that this text is the value, so it
1867                    // binds here rather than going back on the queue to be read as a
1868                    // token again — where `--jobs=--force` looked like a flag of its
1869                    // own and bound `force`, leaving `jobs` unset.
1870                    if let Some((_, val)) = split {
1871                        // The `=` settles where the *first* value came from and nothing
1872                        // more, so a variadic argument goes on collecting from the words
1873                        // after it exactly as the detached form does.
1874                        let mut val = val.to_string();
1875                        let should_return = bind_pending_flag_value(
1876                            spec,
1877                            &out.cmd,
1878                            &mut out.errors,
1879                            &mut out.flags,
1880                            &mut out.flag_awaiting_value,
1881                            &mut val,
1882                            &mut input,
1883                            custom_env,
1884                            trace,
1885                            argv,
1886                            // The `=` settled that this text is the value, so it rode in on
1887                            // the flag's own token.
1888                            true,
1889                        )?;
1890                        if should_return {
1891                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1892                            return Ok((out, overridden_flags));
1893                        }
1894                    }
1895                } else if f.count {
1896                    let arr = out
1897                        .flags
1898                        .entry(Arc::clone(f))
1899                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1900                        .try_as_multi_bool_mut()
1901                        .unwrap();
1902                    arr.push(true);
1903                } else {
1904                    let negate = f.negate.clone().unwrap_or_default();
1905                    let negated_form = word == negate;
1906                    let value = if f.bool_value {
1907                        match split.map(|(_, value)| value) {
1908                            Some("true") => !negated_form,
1909                            Some("false") => negated_form,
1910                            Some(value) => {
1911                                out.errors.push(UsageErr::InvalidValue {
1912                                    name: f.name.clone(),
1913                                    value: value.to_string(),
1914                                    reason: "expected `true` or `false`".to_string(),
1915                                });
1916                                continue;
1917                            }
1918                            None => !negated_form,
1919                        }
1920                    } else {
1921                        !negated_form
1922                    };
1923                    // Which form was typed is a question about the name, so it is
1924                    // asked of `word` rather than the whole token: the attached value
1925                    // is dropped just above, and comparing `--no-color=yes` against
1926                    // `--no-color` would take the negation down with it.
1927                    record_scalar_flag_occurrence(
1928                        &out.cmds,
1929                        f,
1930                        command_level,
1931                        Some(!negated_form),
1932                        &mut scalar_occurrences,
1933                        &mut out.errors,
1934                    );
1935                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1936                }
1937                continue;
1938            }
1939            if is_help_arg(spec, &out.cmd, &w) {
1940                out.errors
1941                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1942                trace.record(
1943                    argv,
1944                    TokenRole::Builtin {
1945                        spelling: w.clone(),
1946                    },
1947                );
1948                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1949                return Ok((out, overridden_flags));
1950            }
1951            if is_version_arg(spec, &out.cmds, &w) {
1952                out.errors.push(render_version_err(spec, w.len() > 2));
1953                trace.record(
1954                    argv,
1955                    TokenRole::Builtin {
1956                        spelling: w.clone(),
1957                    },
1958                );
1959                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1960                return Ok((out, overridden_flags));
1961            }
1962            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1963                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1964                trace.close(&input);
1965                return Err(refused.into());
1966            }
1967        }
1968
1969        // short flags
1970        //
1971        // A fresh token is checked whole before any of it is applied: `-az` with only
1972        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
1973        // discovering that `z` names nothing. A grouped continuation is exempt — its
1974        // token was already checked when it arrived.
1975        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1976        let positional_negative_number = !declared_numeric_short
1977            && is_negative_number(&w)
1978            && active_args(&out.cmd)
1979                .get(next_arg_idx)
1980                .is_some_and(|arg| arg.allow_negative_numbers);
1981        if enable_flags
1982            && !grouped_flag
1983            // A word phase 1 already resolved to a flag needs no re-checking, and
1984            // the flags in scope have changed since, so re-checking would be wrong.
1985            && binding.is_none()
1986            && w.starts_with('-')
1987            && w.len() > 1
1988            && is_flag_like(&w)
1989            && !positional_negative_number
1990            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1991        {
1992            // Refused if this command asked for that; otherwise it carries on below
1993            // as one word, with none of its letters applied.
1994            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1995                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1996                trace.close(&input);
1997                return Err(refused.into());
1998            }
1999        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2000            let short = w.chars().nth(1).unwrap();
2001            if let Some(f) = binding
2002                .as_ref()
2003                .map(|(flag, _)| flag)
2004                .or_else(|| out.available_flags.get(&format!("-{short}")))
2005            {
2006                let command_level = binding
2007                    .as_ref()
2008                    .map(|(_, level)| *level)
2009                    .unwrap_or(out.cmds.len() - 1);
2010                if f.action != crate::SpecFlagAction::Set {
2011                    out.errors
2012                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2013                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2014                    return Ok((out, overridden_flags));
2015                }
2016                parsed_flag_spellings
2017                    .entry(Arc::as_ptr(f) as usize)
2018                    .or_default()
2019                    .insert(format!("-{short}"));
2020                trace.record(
2021                    argv,
2022                    TokenRole::Flag {
2023                        flag: Arc::clone(f),
2024                        spelling: format!("-{short}"),
2025                        // A short spelling is never the negated form: `negate` is a long.
2026                        negated: false,
2027                    },
2028                );
2029                apply_flag_overrides(
2030                    f,
2031                    &out.available_flags,
2032                    &mut out.flags,
2033                    &mut out.flag_awaiting_value,
2034                    &mut overridden_flags,
2035                    &mut out.overridden_flags,
2036                );
2037                if !attached_continuation {
2038                    if let Some(pending) = out.flag_awaiting_value.first() {
2039                        out.errors.push(render_missing_flag_value(pending, &w));
2040                        record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2041                        return Ok((out, overridden_flags));
2042                    }
2043                }
2044                let rest = &w[1 + short.len_utf8()..];
2045                if !rest.is_empty() {
2046                    // `-abc` is one token that names three flags, so the tail is read at the
2047                    // bundle's own position rather than at one of its own.
2048                    input.push_front(Token::new(format!("-{rest}"), argv));
2049                }
2050                // A fully consumed short is no longer a grouped continuation.
2051                // Leaving this set after `-ai` made `-i` skip `require_equals`
2052                // and bind the following word.
2053                grouped_flag = !rest.is_empty();
2054                if f.arg.is_some() {
2055                    record_scalar_flag_occurrence(
2056                        &out.cmds,
2057                        f,
2058                        command_level,
2059                        None,
2060                        &mut scalar_occurrences,
2061                        &mut out.errors,
2062                    );
2063                    out.flag_awaiting_value.push(Arc::clone(f));
2064                } else if f.count {
2065                    let arr = out
2066                        .flags
2067                        .entry(Arc::clone(f))
2068                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2069                        .try_as_multi_bool_mut()
2070                        .unwrap();
2071                    arr.push(true);
2072                } else {
2073                    let negate = f.negate.clone().unwrap_or_default();
2074                    let value = w != negate;
2075                    record_scalar_flag_occurrence(
2076                        &out.cmds,
2077                        f,
2078                        command_level,
2079                        Some(value),
2080                        &mut scalar_occurrences,
2081                        &mut out.errors,
2082                    );
2083                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2084                }
2085                continue;
2086            }
2087            // The letter nothing declared may still be one the parser supplies, and it may
2088            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2089            // neither reaches the whole-token spellings below.
2090            if let Some(err) = supplied_short(spec, &out.cmds, short) {
2091                out.errors.push(err);
2092                trace.record(
2093                    argv,
2094                    TokenRole::Builtin {
2095                        spelling: format!("-{short}"),
2096                    },
2097                );
2098                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2099                return Ok((out, overridden_flags));
2100            }
2101            if is_help_arg(spec, &out.cmd, &w) {
2102                out.errors
2103                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2104                trace.record(
2105                    argv,
2106                    TokenRole::Builtin {
2107                        spelling: w.clone(),
2108                    },
2109                );
2110                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2111                return Ok((out, overridden_flags));
2112            }
2113            if is_version_arg(spec, &out.cmds, &w) {
2114                out.errors.push(render_version_err(spec, w.len() > 2));
2115                trace.record(
2116                    argv,
2117                    TokenRole::Builtin {
2118                        spelling: w.clone(),
2119                    },
2120                );
2121                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2122                return Ok((out, overridden_flags));
2123            }
2124            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2125                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2126                trace.close(&input);
2127                return Err(refused.into());
2128            }
2129            if grouped_flag {
2130                grouped_flag = false;
2131                w.remove(0);
2132                // What is left is a short flag's attached value, and one `=` between
2133                // the letter and the value is a separator: `-j=8` means 8. Only one,
2134                // so `-j==8` still means `=8`.
2135                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2136                    w.remove(0);
2137                }
2138            }
2139        }
2140
2141        // Only while flags are still being read. A flag still waiting when the separator
2142        // was consumed is starved: its value would have to come from after the `--`,
2143        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2144        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2145        // was gone. Left waiting, it is reported as the missing value it is.
2146        // `require_equals` refuses a detached value: `--flag value` is a missing
2147        // value, not a flag of `"value"`. The attached form is still bound above.
2148        // Reported here rather than left waiting until the end of the line: falling
2149        // through would offer `value` to the positionals and call it an unexpected
2150        // word, which is the wrong error and a different one from usage-argv.
2151        if enable_flags
2152            && !attached_continuation
2153            && !out.flag_awaiting_value.is_empty()
2154            && out
2155                .flag_awaiting_value
2156                .last()
2157                .is_some_and(|flag| flag.require_equals)
2158        {
2159            let flag = out.flag_awaiting_value.last().unwrap();
2160            let token = flag
2161                .long
2162                .first()
2163                .map(|l| format!("--{l}"))
2164                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2165                .unwrap_or_else(|| flag.name.clone());
2166            out.errors.push(UsageErr::InvalidFlag {
2167                token: token.clone(),
2168                reason: "requires an argument".to_string(),
2169                span: (0, 0).into(),
2170                input: format!("{token} {w}"),
2171            });
2172            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2173            return Ok((out, overridden_flags));
2174        }
2175        if enable_flags
2176            && !out.flag_awaiting_value.is_empty()
2177            && (attached_continuation
2178                || out
2179                    .flag_awaiting_value
2180                    .last()
2181                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2182        {
2183            // Held before the drain pops it: a flag whose argument is variadic keeps
2184            // taking values after this first one.
2185            let should_return = bind_pending_flag_value(
2186                spec,
2187                &out.cmd,
2188                &mut out.errors,
2189                &mut out.flags,
2190                &mut out.flag_awaiting_value,
2191                &mut w,
2192                &mut input,
2193                custom_env,
2194                trace,
2195                argv,
2196                attached_continuation,
2197            )?;
2198            if should_return {
2199                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2200                return Ok((out, overridden_flags));
2201            }
2202            continue;
2203        }
2204
2205        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2206            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2207            .flatten()
2208        {
2209            if value.is_empty() {
2210                out.errors.push(UsageErr::InvalidValue {
2211                    name: arg.name.clone(),
2212                    value: w.clone(),
2213                    reason: format!("expected a value after sigil {sigil:?}"),
2214                });
2215                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2216                return Ok((out, overridden_flags));
2217            }
2218            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2219            let suppress_trailing_delimiter =
2220                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2221            let delimiter = if suppress_trailing_delimiter && trailing_value {
2222                None
2223            } else {
2224                arg.delimiter
2225            };
2226            let parts = match delimiter {
2227                Some(delimiter) => value
2228                    .split(delimiter)
2229                    .map(str::to_string)
2230                    .collect::<Vec<_>>(),
2231                None => vec![value.to_string()],
2232            };
2233            let mut refused = false;
2234            for part in &parts {
2235                if validate_choices(
2236                    spec,
2237                    &out.cmd,
2238                    &mut out.errors,
2239                    ChoiceTarget::arg(arg),
2240                    part,
2241                    arg.choices.as_ref(),
2242                    custom_env,
2243                )? {
2244                    refused = true;
2245                    break;
2246                }
2247            }
2248            if refused {
2249                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2250                return Ok((out, overridden_flags));
2251            }
2252            trace.record(
2253                argv,
2254                TokenRole::Sigil {
2255                    arg: Arc::new(arg.clone()),
2256                    sigil: sigil.to_string(),
2257                    values: parts.clone(),
2258                },
2259            );
2260            let key = Arc::new(arg.clone());
2261            if arg.var {
2262                let arr = out
2263                    .args
2264                    .entry(key)
2265                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2266                    .try_as_multi_string_mut()
2267                    .unwrap();
2268                arr.extend(parts);
2269            } else {
2270                out.args.insert(key, ParseValue::String(value.to_string()));
2271            }
2272            continue;
2273        }
2274
2275        if out.cmd.allow_missing_positional {
2276            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2277            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2278                if current.required || out.args.contains_key(current) {
2279                    break;
2280                }
2281                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2282                    .iter()
2283                    .filter(|arg| arg.required && arg.sigil.is_none())
2284                    .count();
2285                if required_after == 0 {
2286                    break;
2287                }
2288                let remaining_values = 1 + input
2289                    .iter()
2290                    .filter(|token| {
2291                        (!enable_flags || !is_flag_like(&token.word))
2292                            && (!enable_flags
2293                                || restart_seen
2294                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2295                    })
2296                    .count();
2297                if remaining_values > required_after {
2298                    break;
2299                }
2300                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2301            }
2302        }
2303
2304        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2305            if arg.var
2306                && out.args.contains_key(arg)
2307                && arg.value_terminator.as_deref() == Some(w.as_str())
2308            {
2309                trace.record(
2310                    argv,
2311                    TokenRole::ValueTerminator {
2312                        ends: arg.name.clone(),
2313                    },
2314                );
2315                next_arg_idx += 1;
2316                continue;
2317            }
2318            // Before anything else: an arg that requires `--` accepts nothing until one has been
2319            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2320            // reported as an invalid choice, and from reaching that function's help escape.
2321            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2322                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2323                trace.record(
2324                    argv,
2325                    TokenRole::Refused {
2326                        reason: format!("{} only accepts words after `--`", arg.name),
2327                    },
2328                );
2329                // Drop the word without filling the arg or advancing the cursor: every later
2330                // word hits the same arg and is rejected the same way, so the parse still ends
2331                // in an error rather than in `unexpected word`.
2332                continue;
2333            }
2334            // Split before judging, as the flag path does: after the split the word is
2335            // no longer one value, and `choices` has to be asked about each. Judging
2336            // first rejects `src:docs` against a list that both halves are on, and
2337            // names the whole word rather than the half that was wrong.
2338            let trailing_value =
2339                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2340            let suppress_trailing_delimiter =
2341                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2342            let delimiter = if suppress_trailing_delimiter && trailing_value {
2343                None
2344            } else {
2345                arg.delimiter
2346            };
2347            let parts: Vec<String> = match delimiter {
2348                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2349                None => vec![w.clone()],
2350            };
2351            let mut refused = false;
2352            for part in &parts {
2353                if validate_choices(
2354                    spec,
2355                    &out.cmd,
2356                    &mut out.errors,
2357                    ChoiceTarget::arg(arg),
2358                    part,
2359                    arg.choices.as_ref(),
2360                    custom_env,
2361                )? {
2362                    refused = true;
2363                    break;
2364                }
2365            }
2366            if refused {
2367                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2368                return Ok((out, overridden_flags));
2369            }
2370            // `double_dash="automatic"` means the first value this arg takes is the last
2371            // token read as anything but data: a wrapper declaring it can forward flags
2372            // without its caller typing a `--`. Set before the value is stored, so the
2373            // rest of the command line is already past flag parsing.
2374            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2375                enable_flags = false;
2376            }
2377            // A flag-like word reaching a positional while flags are still being read was
2378            // offered to every declaration and matched none: under the default
2379            // `unknown_flags="value"` it becomes data, and saying so is the difference
2380            // between "you have a typo" and "this argument took your typo".
2381            let unknown_flag = enable_flags
2382                && !positional_negative_number
2383                && is_flag_like(&w)
2384                && binding.is_none();
2385            trace.record(
2386                argv,
2387                if unknown_flag {
2388                    TokenRole::UnknownFlag {
2389                        bound_as: Some(Arc::new(arg.clone())),
2390                    }
2391                } else {
2392                    TokenRole::Arg {
2393                        arg: Arc::new(arg.clone()),
2394                        values: parts.clone(),
2395                    }
2396                },
2397            );
2398            if arg.var {
2399                let arr = out
2400                    .args
2401                    .entry(Arc::new(arg.clone()))
2402                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2403                    .try_as_multi_string_mut()
2404                    .unwrap();
2405                // The values this word carried, split above so that everything
2406                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2407                // counts the values the user meant rather than the words they typed.
2408                arr.extend(parts.iter().cloned());
2409                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2410                    next_arg_idx += 1;
2411                }
2412            } else {
2413                out.args
2414                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2415                next_arg_idx += 1;
2416            }
2417            continue;
2418        }
2419        if is_help_arg(spec, &out.cmd, &w) {
2420            out.errors
2421                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2422            trace.record(
2423                argv,
2424                TokenRole::Builtin {
2425                    spelling: w.clone(),
2426                },
2427            );
2428            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2429            return Ok((out, overridden_flags));
2430        }
2431        if is_version_arg(spec, &out.cmds, &w) {
2432            out.errors.push(render_version_err(spec, w.len() > 2));
2433            trace.record(
2434                argv,
2435                TokenRole::Builtin {
2436                    spelling: w.clone(),
2437                },
2438            );
2439            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2440            return Ok((out, overridden_flags));
2441        }
2442        trace.record(
2443            argv,
2444            TokenRole::Refused {
2445                reason: "no declaration takes this word".to_string(),
2446            },
2447        );
2448        trace.close(&input);
2449        bail!("unexpected word: {w}");
2450    }
2451
2452    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2453    validate_clause_relationships(&mut out, &overridden_flags, custom_env);
2454
2455    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2456    // declarations with the same canonical name therefore share one public value entry even
2457    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2458    // both, which is what exclusivity needs.
2459    let flag_was_parsed =
2460        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2461
2462    // The spellings the selected command's own declaration speaks for, on this object.
2463    //
2464    // Empty unless that declaration really is this object's: a parent and child may each
2465    // declare `--clean` without merging, leaving two flags that share a name, and the
2466    // ancestor's must not be read as the child's. The test is whether every spelling the child
2467    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2468    // an ancestor whose long form the child took over.
2469    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2470        let declared: HashSet<String> = out
2471            .cmd
2472            .flags
2473            .iter()
2474            .filter(|declared| declared.name == flag.name)
2475            .flat_map(flag_keys)
2476            .collect();
2477        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2478        // some other inherited global already owns — `-c --clean` beside an inherited
2479        // `-c --config` — and that collision is resolved in the other global's favor, so the
2480        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2481        // unrelated collision disown the child from the `--clean` it plainly does own.
2482        //
2483        // Still enough to tell the two-object case apart, which is what this guards: when a
2484        // child re-declares a global as global, the child's own spellings resolve to the
2485        // child's separate flag, so none of them lands on the ancestor's.
2486        let speaks_for_this_flag = declared.iter().any(|spelling| {
2487            out.available_flags
2488                .get(spelling)
2489                .is_some_and(|available| Arc::ptr_eq(available, flag))
2490        });
2491        if speaks_for_this_flag {
2492            declared
2493        } else {
2494            HashSet::new()
2495        }
2496    };
2497
2498    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2499    //
2500    // A child that re-declares an inherited global merges into one object answering to two
2501    // alias sets whose declarations may disagree, so there is no single owner to name: the
2502    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2503    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2504    // child's in one invocation — and each carries its own declaration's answer.
2505    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2506        let child = child_spellings(flag);
2507        let child_exclusive = !child.is_empty()
2508            && out
2509                .cmd
2510                .flags
2511                .iter()
2512                .any(|declared| declared.name == flag.name && declared.exclusive);
2513        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2514            Some(spellings) => (
2515                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2516                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2517            ),
2518            // An environment value has no spelling to attribute it by. The declaration the
2519            // selected command has in scope is the one that answers — which is the child's
2520            // when it re-declared the flag, and the ancestor's when it did not.
2521            None => (child_exclusive, flag.exclusive && child.is_empty()),
2522        }
2523    };
2524
2525    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2526        let (child, ancestor) = exclusivity_in_play(flag);
2527        child || ancestor
2528    };
2529
2530    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2531    // command that otherwise needs an input. Companions are still diagnosed below, but an
2532    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2533    // whether it was alone or not.
2534    let exclusive_present =
2535        unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2536            exclusive_occurrence(flag)
2537                && !overridden_flags.contains(&flag.name)
2538                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2539        });
2540    let requirements_apply = |command_index: usize| {
2541        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2542    };
2543
2544    if out.cmd.arg_required_else_help && !command_has_argv {
2545        out.errors.push(render_help_err(spec, &out.cmd, false));
2546    }
2547
2548    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2549    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2550    // the child would be here instead. The spec has carried `subcommand_required` since it was
2551    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2552    // complete invocation while usage-argv and clap both refused it.
2553    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2554        let mut names: Vec<&str> = out
2555            .cmd
2556            .subcommands
2557            .iter()
2558            // Aliases share a map entry with the name they point at; listing both would offer
2559            // the same command twice under two spellings.
2560            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2561            .map(|(name, _)| name.as_str())
2562            .collect();
2563        names.sort_unstable();
2564        out.errors.push(UsageErr::MissingSubcommand(
2565            out.cmd.name.clone(),
2566            names.join(", "),
2567        ));
2568    }
2569
2570    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2571    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2572    if !exclusive_present {
2573        for arg in out
2574            .cmds
2575            .iter()
2576            .enumerate()
2577            .filter(|(index, _)| requirements_apply(*index))
2578            .flat_map(|(_, cmd)| &cmd.args)
2579        {
2580            if out.args.contains_key(arg) {
2581                continue;
2582            }
2583            // Already reported as needing a `--`; one mistake should not yield two messages.
2584            if double_dash_violations.contains(&arg.name) {
2585                continue;
2586            }
2587            let required_if = arg.required_if.iter().any(|selector| {
2588                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2589            });
2590            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2591                selector_explicit_has_value(
2592                    &condition.selector,
2593                    &condition.value,
2594                    &out,
2595                    &overridden_flags,
2596                    custom_env,
2597                )
2598            });
2599            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2600                && arg.required_if_eq_all.iter().all(|condition| {
2601                    selector_explicit_has_value(
2602                        &condition.selector,
2603                        &condition.value,
2604                        &out,
2605                        &overridden_flags,
2606                        custom_env,
2607                    )
2608                });
2609            let unless_any = arg.required_unless.iter().any(|selector| {
2610                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2611            });
2612            let unless_all = !arg.required_unless_all.is_empty()
2613                && arg.required_unless_all.iter().all(|selector| {
2614                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2615                });
2616            let required_unless = !(unless_any
2617                || unless_all
2618                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2619            if (arg.required
2620                || required_if
2621                || required_if_eq
2622                || required_if_eq_all
2623                || required_unless)
2624                && arg.default.is_empty()
2625            {
2626                // Check if there's an env var available (custom env map takes precedence)
2627                let has_env = arg
2628                    .env
2629                    .as_ref()
2630                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2631                if !has_env {
2632                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2633                }
2634            }
2635        }
2636    }
2637
2638    // Conflicts are a question about the invocation as a whole rather than about any one
2639    // token, so they are checked here beside the requirement checks rather than at the
2640    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2641    // Its own loop: the requirement loop below skips the flags that *were* given, which
2642    // is exactly the set this needs.
2643    //
2644    // A value from the environment counts on both sides, matching what
2645    // `selector_is_explicit` says about the other flag: the question is whether a flag
2646    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2647    // would make the same pair of flags a conflict or not depending on which one
2648    // happened to be typed.
2649    for flag in unique_flags(out.available_flags.values()) {
2650        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2651        if !given || overridden_flags.contains(&flag.name) {
2652            continue;
2653        }
2654        for other in &flag.conflicts {
2655            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2656                out.errors.push(UsageErr::InvalidFlag {
2657                    token: format!("--{}", flag.name),
2658                    reason: format!("conflicts with {other}"),
2659                    span: (0, 0).into(),
2660                    input: format!("--{} {other}", flag.name),
2661                });
2662            }
2663        }
2664        // The positive form, checked in the same pass and under the same rule: a value
2665        // from the environment satisfies a requirement, because the question is whether
2666        // the other flag has a value rather than how it got one. A flag that was
2667        // overridden away has not been given, so it cannot satisfy anything either —
2668        // which is what `selector_is_explicit` already accounts for.
2669        //
2670        // Reported as the missing flag rather than as something wrong with the flag that
2671        // named it, which is what clap says too: an unmet `requires` is a required
2672        // argument that was not provided. Named by its own name, resolved through the
2673        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2674        let owner = out
2675            .cmds
2676            .iter()
2677            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2678            .unwrap_or(out.cmds.len() - 1);
2679        if !exclusive_present && requirements_apply(owner) {
2680            for other in &flag.requires {
2681                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2682                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2683                    if other.starts_with('-') {
2684                        out.errors.push(UsageErr::MissingFlag(name));
2685                    } else {
2686                        out.errors.push(UsageErr::MissingArg(name));
2687                    }
2688                }
2689            }
2690            for condition in &flag.requires_if {
2691                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2692                    && !selector_is_satisfied(
2693                        &condition.requires,
2694                        &out,
2695                        &overridden_flags,
2696                        custom_env,
2697                    )
2698                {
2699                    let name = selector_flag_name(&condition.requires, &out)
2700                        .unwrap_or_else(|| condition.requires.clone());
2701                    out.errors.push(UsageErr::MissingFlag(name));
2702                }
2703            }
2704        }
2705    }
2706
2707    // Positionals can declare the same pairwise conflict as flags. Their selector is
2708    // the bare argument name, while a flag keeps its dashed spelling.
2709    for (command_index, arg) in out
2710        .cmds
2711        .iter()
2712        .enumerate()
2713        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2714    {
2715        let given = arg_is_explicit(arg, &out, custom_env);
2716        if !given {
2717            continue;
2718        }
2719        for other in &arg.conflicts {
2720            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2721                out.errors.push(UsageErr::InvalidFlag {
2722                    token: arg.name.clone(),
2723                    reason: format!("conflicts with {other}"),
2724                    span: (0, 0).into(),
2725                    input: format!("{} {other}", arg.name),
2726                });
2727            }
2728        }
2729        if !exclusive_present && requirements_apply(command_index) {
2730            for other in &arg.requires {
2731                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2732                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2733                    if other.starts_with('-') {
2734                        out.errors.push(UsageErr::MissingFlag(name));
2735                    } else {
2736                        out.errors.push(UsageErr::MissingArg(name));
2737                    }
2738                }
2739            }
2740        }
2741    }
2742
2743    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2744    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2745    // positionals included, which is what distinguishes it from being in a group with
2746    // every other flag.
2747    //
2748    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2749    // exclusive one is nobody saying anything, and counting it would make the exclusive
2750    // flag unusable on any command that has a default. Environment values do count, also as
2751    // `conflicts` reads them, so the spec parser and the derive agree.
2752    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2753        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2754        // but re-declared aliases can leave distinct declarations with that same name in
2755        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2756        // compare the parser's `Arc`s by identity here.
2757        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2758        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2759            continue;
2760        }
2761        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2762            .find(|other| {
2763                !Arc::ptr_eq(other, flag)
2764                    && !overridden_flags.contains(&other.name)
2765                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2766            })
2767            .map(|other| format!("--{}", other.name));
2768        let other_arg = active_args(&out.cmd).iter().find(|arg| {
2769            out.args.keys().any(|given| given.name == arg.name)
2770                || out
2771                    .clauses
2772                    .values()
2773                    .flatten()
2774                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
2775                || arg
2776                    .env
2777                    .as_ref()
2778                    .is_some_and(|env| env_contains(custom_env, env))
2779        });
2780        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2781        // exclusive flag belonging to the child itself does not conflict with the command word
2782        // needed to reach that child — so the question is not who owns the flag but whose
2783        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2784        // separated.
2785        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2786        let selected_subcommand =
2787            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2788        let other = other_flag
2789            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2790            .or(selected_subcommand);
2791        if let Some(other) = other {
2792            out.errors.push(UsageErr::InvalidFlag {
2793                token: format!("--{}", flag.name),
2794                reason: format!("must be given on its own, and {other} was given too"),
2795                span: (0, 0).into(),
2796                input: format!("--{} {other}", flag.name),
2797            });
2798        }
2799    }
2800
2801    // Groups, checked once per group rather than per flag: both questions a group asks —
2802    // how many members were given, and whether that is enough — are about the set, which
2803    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2804    //
2805    // The same "given" rule as everything else here, so a member filled from the
2806    // environment or a default counts.
2807    // Every command in the chain, not only the selected one: a group may name global
2808    // flags, which belong to an ancestor and are declared there.
2809    let mut group_errors: Vec<UsageErr> = Vec::new();
2810    for (command_index, group) in out
2811        .cmds
2812        .iter()
2813        .enumerate()
2814        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2815    {
2816        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2817        // `--file` are two spellings of one flag, and a group naming both — or naming one
2818        // flag twice — would otherwise report that flag as conflicting with itself the
2819        // moment it was given. Deduplicated rather than refused where the group is
2820        // written, because listing both spellings is redundant, not wrong.
2821        let mut given: Vec<&str> = Vec::new();
2822        let mut seen: Vec<String> = Vec::new();
2823        for selector in &group.members {
2824            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2825                continue;
2826            }
2827            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2828            if seen.contains(&name) {
2829                continue;
2830            }
2831            seen.push(name);
2832            given.push(selector.as_str());
2833        }
2834        if !group.multiple && given.len() > 1 {
2835            group_errors.push(UsageErr::InvalidFlag {
2836                token: given[1].to_string(),
2837                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2838                span: (0, 0).into(),
2839                input: format!("{} {}", given[0], given[1]),
2840            });
2841        }
2842        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2843        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2844        // exclusivity must count only what was supplied, or a defaulted member would
2845        // collide with the sibling the user actually typed.
2846        let satisfied = group
2847            .members
2848            .iter()
2849            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2850        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2851            // The members are what a user has to type, so they are in the message; the
2852            // group's name is there too, since a command with several groups would
2853            // otherwise report the same sentence twice with nothing to tell them apart.
2854            group_errors.push(UsageErr::MissingGroup {
2855                group: group.name.clone(),
2856                members: group.members.join(", "),
2857            });
2858        }
2859    }
2860    out.errors.extend(group_errors);
2861
2862    if !exclusive_present {
2863        for flag in unique_flags(out.available_flags.values()) {
2864            let owner = out
2865                .cmds
2866                .iter()
2867                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2868                .unwrap_or(out.cmds.len() - 1);
2869            if !requirements_apply(owner) {
2870                continue;
2871            }
2872            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2873                continue;
2874            }
2875            let has_default =
2876                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2877            let has_env = flag_has_env(flag, custom_env);
2878            let required_if = flag.required_if.iter().any(|selector| {
2879                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2880            });
2881            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2882                selector_explicit_has_value(
2883                    &condition.selector,
2884                    &condition.value,
2885                    &out,
2886                    &overridden_flags,
2887                    custom_env,
2888                )
2889            });
2890            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2891                && flag.required_if_eq_all.iter().all(|condition| {
2892                    selector_explicit_has_value(
2893                        &condition.selector,
2894                        &condition.value,
2895                        &out,
2896                        &overridden_flags,
2897                        custom_env,
2898                    )
2899                });
2900            let unless_any = flag.required_unless.iter().any(|selector| {
2901                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2902            });
2903            let unless_all = !flag.required_unless_all.is_empty()
2904                && flag.required_unless_all.iter().all(|selector| {
2905                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2906                });
2907            let required_unless = !(unless_any
2908                || unless_all
2909                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
2910            if (flag.required
2911                || required_if
2912                || required_if_eq
2913                || required_if_eq_all
2914                || required_unless)
2915                && !has_default
2916                && !has_env
2917            {
2918                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2919            }
2920        }
2921    }
2922
2923    // Validate var_min/var_max constraints for variadic args
2924    for (arg, value) in &out.args {
2925        if arg.var {
2926            if let ParseValue::MultiString(values) = value {
2927                if let Some(min) = arg.var_min {
2928                    if values.len() < min {
2929                        out.errors.push(UsageErr::VarArgTooFew {
2930                            name: arg.name.clone(),
2931                            min,
2932                            got: values.len(),
2933                        });
2934                    }
2935                }
2936                if let Some(max) = arg.var_max {
2937                    if values.len() > max {
2938                        out.errors.push(UsageErr::VarArgTooMany {
2939                            name: arg.name.clone(),
2940                            max,
2941                            got: values.len(),
2942                        });
2943                    }
2944                }
2945            }
2946        }
2947    }
2948
2949    // Validate var_min/var_max constraints for variadic flags. These are bounds on
2950    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
2951    // by binding once per occurrence, where the per-occurrence count is still available.
2952    for flag in unique_flags(out.available_flags.values()) {
2953        if flag.var {
2954            let bound = match out.flags.get(flag) {
2955                Some(ParseValue::MultiString(values)) => values.len(),
2956                Some(ParseValue::MultiBool(values)) => values.len(),
2957                Some(_) => 1,
2958                None => 0,
2959            };
2960            // A partial parse deliberately leaves the final value-optional flag pending so
2961            // completion can still answer for it. It is nevertheless a real occurrence for
2962            // the repeated flag's bounds; the full parser closes it just after this phase.
2963            let pending = out
2964                .flag_awaiting_value
2965                .iter()
2966                .filter(|pending| {
2967                    Arc::ptr_eq(pending, flag)
2968                        && (pending.value_optional || pending.default_missing.is_some())
2969                })
2970                .count();
2971            let count = bound + pending;
2972            if count == 0 {
2973                continue;
2974            }
2975            if let Some(min) = flag.var_min {
2976                if count < min {
2977                    out.errors.push(UsageErr::VarFlagTooFew {
2978                        name: flag.name.clone(),
2979                        min,
2980                        got: count,
2981                    });
2982                }
2983            }
2984            if let Some(max) = flag.var_max {
2985                if count > max {
2986                    out.errors.push(UsageErr::VarFlagTooMany {
2987                        name: flag.name.clone(),
2988                        max,
2989                        got: count,
2990                    });
2991                }
2992            }
2993        }
2994    }
2995
2996    Ok((out, overridden_flags))
2997}
2998
2999fn validate_expression(
3000    name: &str,
3001    expression: Option<&str>,
3002    message: Option<&str>,
3003    parsed: &ParseValue,
3004    errors: &mut Vec<UsageErr>,
3005) {
3006    let Some(expression) = expression else {
3007        return;
3008    };
3009    #[cfg(not(feature = "validation"))]
3010    let _ = expression;
3011    let values: &[String] = match parsed {
3012        ParseValue::String(value) => std::slice::from_ref(value),
3013        ParseValue::MultiString(values) => values,
3014        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3015    };
3016    #[cfg(feature = "validation")]
3017    for value in values {
3018        let reason = match usage_validation::validate(expression, value) {
3019            Ok(true) => continue,
3020            Ok(false) => message
3021                .unwrap_or("does not satisfy the validation expression")
3022                .to_string(),
3023            Err(error) => format!("validation expression failed: {error}"),
3024        };
3025        errors.push(UsageErr::InvalidValue {
3026            name: name.to_string(),
3027            value: value.clone(),
3028            reason,
3029        });
3030        break;
3031    }
3032    #[cfg(not(feature = "validation"))]
3033    if let Some(value) = values.first() {
3034        let _ = message;
3035        errors.push(UsageErr::InvalidValue {
3036            name: name.to_string(),
3037            value: value.clone(),
3038            reason: "expression validation requires the `validation` feature".to_string(),
3039        });
3040    }
3041}
3042
3043#[cfg(all(test, not(feature = "validation")))]
3044mod optional_validation_tests {
3045    use crate::{parse, Spec};
3046
3047    #[test]
3048    fn validation_declarations_require_the_opt_in_runtime_feature() {
3049        let spec: Spec = r#"
3050name "ex"
3051bin "ex"
3052arg "<port>" validate="int(value) > 0"
3053        "#
3054        .parse()
3055        .unwrap();
3056        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3057        assert!(
3058            error
3059                .to_string()
3060                .contains("requires the `validation` feature"),
3061            "{error:?}"
3062        );
3063    }
3064}
3065
3066fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3067    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3068}
3069
3070fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3071    overrider
3072        .overrides
3073        .iter()
3074        .any(|selector| flag_matches_selector(overridden, selector))
3075}
3076
3077fn apply_prefix_flag_overrides(
3078    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3079    flag: Arc<SpecFlag>,
3080) {
3081    prefix_flags
3082        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3083}
3084
3085fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3086    prefix_flags
3087        .iter()
3088        .flat_map(|(_, words)| words.iter().cloned())
3089        .collect()
3090}
3091
3092fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3093    match custom_env {
3094        Some(env) => env.contains_key(env_var),
3095        None => std::env::var(env_var).is_ok(),
3096    }
3097}
3098
3099fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3100    flag.env_names()
3101        .any(|env_var| env_contains(custom_env, env_var))
3102}
3103
3104fn fallback_is_true(value: &str) -> bool {
3105    matches!(value, "1" | "true" | "True" | "TRUE")
3106}
3107
3108fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3109    match delimiter {
3110        Some(delimiter) => values
3111            .iter()
3112            .flat_map(|value| value.split(delimiter).map(str::to_string))
3113            .collect(),
3114        None => values.to_vec(),
3115    }
3116}
3117
3118fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3119    if let Some(min) = arg.var_min {
3120        if count < min {
3121            errors.push(UsageErr::VarArgTooFew {
3122                name: arg.name.clone(),
3123                min,
3124                got: count,
3125            });
3126        }
3127    }
3128    if let Some(max) = arg.var_max {
3129        if count > max {
3130            errors.push(UsageErr::VarArgTooMany {
3131                name: arg.name.clone(),
3132                max,
3133                got: count,
3134            });
3135        }
3136    }
3137}
3138
3139fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3140    if let Some(min) = flag.var_min {
3141        if count < min {
3142            errors.push(UsageErr::VarFlagTooFew {
3143                name: flag.name.clone(),
3144                min,
3145                got: count,
3146            });
3147        }
3148    }
3149    if let Some(max) = flag.var_max {
3150        if count > max {
3151            errors.push(UsageErr::VarFlagTooMany {
3152                name: flag.name.clone(),
3153                max,
3154                got: count,
3155            });
3156        }
3157    }
3158}
3159
3160fn validate_flag_arg_fallback_count(
3161    flag: &SpecFlag,
3162    arg: &SpecArg,
3163    count: usize,
3164    errors: &mut Vec<UsageErr>,
3165) {
3166    if let Some(min) = arg.var_min {
3167        if count < min {
3168            errors.push(UsageErr::VarFlagTooFew {
3169                name: flag.name.clone(),
3170                min,
3171                got: count,
3172            });
3173        }
3174    }
3175    if let Some(max) = arg.var_max {
3176        if count > max {
3177            errors.push(UsageErr::VarFlagTooMany {
3178                name: flag.name.clone(),
3179                max,
3180                got: count,
3181            });
3182        }
3183    }
3184}
3185
3186/// Bind a fallback the way an unconditional `default` does: one value, or several
3187/// for `var`, and choices checked the same way.
3188fn bind_flag_fallback(
3189    flag: &Arc<SpecFlag>,
3190    values: &[String],
3191    out: &mut ParseOutput,
3192    custom_env: Option<&HashMap<String, String>>,
3193    origin: ValueOrigin,
3194) -> Result<(), miette::Error> {
3195    if values.is_empty() {
3196        return Ok(());
3197    }
3198    if let Some(arg) = flag.arg.as_ref() {
3199        let values = split_fallback_values(values, arg.delimiter);
3200        if flag.var || arg.var {
3201            if flag.var {
3202                validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3203            }
3204            if arg.var {
3205                validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3206            }
3207            validate_choice_values(
3208                ChoiceTarget::option(flag),
3209                &values,
3210                arg.choices.as_ref(),
3211                custom_env,
3212            )?;
3213            out.flags
3214                .insert(Arc::clone(flag), ParseValue::MultiString(values));
3215        } else {
3216            let value = values.into_iter().next().unwrap_or_default();
3217            validate_choice_value(
3218                ChoiceTarget::option(flag),
3219                &value,
3220                arg.choices.as_ref(),
3221                custom_env,
3222            )?;
3223            out.flags
3224                .insert(Arc::clone(flag), ParseValue::String(value));
3225        }
3226    } else if flag.var {
3227        validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3228        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3229        out.flags
3230            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3231    } else {
3232        out.flags.insert(
3233            Arc::clone(flag),
3234            ParseValue::Bool(fallback_is_true(&values[0])),
3235        );
3236    }
3237    out.flag_origins
3238        .entry(Arc::clone(flag))
3239        .or_default()
3240        .push(origin);
3241    Ok(())
3242}
3243
3244fn default_if_condition_matches(
3245    condition: &crate::SpecDefaultIf,
3246    out: &ParseOutput,
3247    overridden_flags: &HashSet<String>,
3248    custom_env: Option<&HashMap<String, String>>,
3249) -> bool {
3250    match &condition.when {
3251        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3252        Some(when) => {
3253            let Some(flag) = out
3254                .available_flags
3255                .values()
3256                .chain(out.flags.keys())
3257                .find(|flag| flag_matches_selector(flag, &condition.selector))
3258            else {
3259                return false;
3260            };
3261            if overridden_flags.contains(&flag.name) {
3262                return false;
3263            }
3264            explicit_flag_has_value(flag, when, out, custom_env)
3265        }
3266    }
3267}
3268
3269/// Whether an explicitly supplied value of `flag` equals `expected`.
3270///
3271/// clap treats command-line and environment values as explicit for `requires_if`, but
3272/// not defaults. Keep that source distinction here instead of consulting the flag's
3273/// defaults through `selector_is_satisfied`.
3274fn explicit_flag_has_value(
3275    flag: &SpecFlag,
3276    expected: &str,
3277    out: &ParseOutput,
3278    custom_env: Option<&HashMap<String, String>>,
3279) -> bool {
3280    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3281        ParseValue::Bool(value) => value.to_string() == expected,
3282        ParseValue::String(value) => value == expected,
3283        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3284        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3285    });
3286    if out.flags.contains_key(flag) {
3287        return parsed_matches;
3288    }
3289
3290    let value = flag.env_names().find_map(|env| match custom_env {
3291        Some(values) => values.get(env).cloned(),
3292        None => std::env::var(env).ok(),
3293    });
3294    value.is_some_and(
3295        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3296            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3297            None if flag.arg.is_none() => {
3298                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3299            }
3300            None => value == expected,
3301        },
3302    )
3303}
3304
3305fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3306    match value {
3307        ParseValue::Bool(value) => value.to_string() == expected,
3308        ParseValue::String(value) => value == expected,
3309        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3310        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3311    }
3312}
3313
3314fn validate_clause_relationships(
3315    out: &mut ParseOutput,
3316    overridden_flags: &HashSet<String>,
3317    custom_env: Option<&HashMap<String, String>>,
3318) {
3319    let Some(clause) = out.cmd.clause.as_ref() else {
3320        return;
3321    };
3322    let Some(instances) = out.clauses.get(&clause.name) else {
3323        return;
3324    };
3325    let flag_is_explicit = |selector: &str| {
3326        out.available_flags
3327            .values()
3328            .chain(out.flags.keys())
3329            .any(|flag| {
3330                flag_matches_selector(flag, selector)
3331                    && !overridden_flags.contains(&flag.name)
3332                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3333            })
3334    };
3335    let flag_matches_value = |selector: &str, expected: &str| {
3336        out.available_flags
3337            .values()
3338            .chain(out.flags.keys())
3339            .find(|flag| flag_matches_selector(flag, selector))
3340            .is_some_and(|flag| {
3341                !overridden_flags.contains(&flag.name)
3342                    && explicit_flag_has_value(flag, expected, out, custom_env)
3343            })
3344    };
3345    let flag_is_satisfied = |selector: &str| {
3346        out.available_flags
3347            .values()
3348            .chain(out.flags.keys())
3349            .any(|flag| flag_matches_selector(flag, selector))
3350            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3351    };
3352    let mut errors = Vec::new();
3353    for (instance_index, instance) in instances.iter().enumerate() {
3354        let arg_is_explicit = |selector: &str| {
3355            instance
3356                .keys()
3357                .any(|arg| !selector.starts_with('-') && arg.name == selector)
3358        };
3359        let selector_is_explicit =
3360            |selector: &str| flag_is_explicit(selector) || arg_is_explicit(selector);
3361        let selector_has_value = |selector: &str, expected: &str| {
3362            flag_matches_value(selector, expected)
3363                || instance.iter().any(|(arg, value)| {
3364                    !selector.starts_with('-')
3365                        && arg.name == selector
3366                        && parse_value_has(value, expected)
3367                })
3368        };
3369        let selector_is_satisfied =
3370            |selector: &str| selector_is_explicit(selector) || flag_is_satisfied(selector);
3371        for arg in &clause.args {
3372            let given = instance.keys().any(|present| present.name == arg.name);
3373            if !given {
3374                let required_if = arg
3375                    .required_if
3376                    .iter()
3377                    .any(|selector| selector_is_explicit(selector));
3378                let required_if_eq = arg
3379                    .required_if_eq
3380                    .iter()
3381                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
3382                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3383                    && arg
3384                        .required_if_eq_all
3385                        .iter()
3386                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
3387                let unless_any = arg
3388                    .required_unless
3389                    .iter()
3390                    .any(|selector| selector_is_explicit(selector));
3391                let unless_all = !arg.required_unless_all.is_empty()
3392                    && arg
3393                        .required_unless_all
3394                        .iter()
3395                        .all(|selector| selector_is_explicit(selector));
3396                let required_unless = !(unless_any
3397                    || unless_all
3398                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3399                if required_if || required_if_eq || required_if_eq_all || required_unless {
3400                    errors.push(UsageErr::MissingClauseArg {
3401                        clause: clause.name.clone(),
3402                        instance: instance_index + 1,
3403                        arg: arg.name.clone(),
3404                    });
3405                }
3406                continue;
3407            }
3408            for other in &arg.conflicts {
3409                if selector_is_explicit(other) {
3410                    errors.push(UsageErr::InvalidFlag {
3411                        token: arg.name.clone(),
3412                        reason: format!("conflicts with {other}"),
3413                        span: (0, 0).into(),
3414                        input: format!("{} {other}", arg.name),
3415                    });
3416                }
3417            }
3418            for other in &arg.requires {
3419                if selector_is_satisfied(other) {
3420                    continue;
3421                }
3422                if other.starts_with('-') {
3423                    let name = selector_flag_name(other, out).unwrap_or_else(|| other.clone());
3424                    errors.push(UsageErr::MissingFlag(name));
3425                } else {
3426                    errors.push(UsageErr::MissingClauseArg {
3427                        clause: clause.name.clone(),
3428                        instance: instance_index + 1,
3429                        arg: other.clone(),
3430                    });
3431                }
3432            }
3433        }
3434    }
3435    out.errors.extend(errors);
3436}
3437
3438fn selector_explicit_has_value(
3439    selector: &str,
3440    expected: &str,
3441    out: &ParseOutput,
3442    overridden_flags: &HashSet<String>,
3443    custom_env: Option<&HashMap<String, String>>,
3444) -> bool {
3445    if let Some(flag) = out
3446        .available_flags
3447        .values()
3448        .chain(out.flags.keys())
3449        .find(|flag| flag_matches_selector(flag, selector))
3450    {
3451        return !overridden_flags.contains(&flag.name)
3452            && explicit_flag_has_value(flag, expected, out, custom_env);
3453    }
3454    let Some(arg) = selector_arg(selector, out) else {
3455        return false;
3456    };
3457    let parsed = out
3458        .args
3459        .iter()
3460        .find(|(given, _)| given.name == arg.name)
3461        .map(|(_, value)| value)
3462        .or_else(|| {
3463            out.clauses.values().flatten().find_map(|instance| {
3464                instance
3465                    .iter()
3466                    .find(|(given, _)| given.name == arg.name)
3467                    .map(|(_, value)| value)
3468            })
3469        });
3470    if let Some(value) = parsed {
3471        return match value {
3472            ParseValue::String(value) => value == expected,
3473            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3474            ParseValue::Bool(value) => value.to_string() == expected,
3475            ParseValue::MultiBool(values) => {
3476                values.iter().any(|value| value.to_string() == expected)
3477            }
3478        };
3479    }
3480    let value = arg.env_names().find_map(|env| match custom_env {
3481        Some(values) => values.get(env).cloned(),
3482        None => std::env::var(env).ok(),
3483    });
3484    value.is_some_and(|value| match arg.delimiter {
3485        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3486        None => value == expected,
3487    })
3488}
3489
3490fn selector_is_explicit(
3491    selector: &str,
3492    out: &ParseOutput,
3493    overridden_flags: &HashSet<String>,
3494    custom_env: Option<&HashMap<String, String>>,
3495) -> bool {
3496    let flag_is_explicit = out
3497        .available_flags
3498        .values()
3499        .chain(out.flags.keys())
3500        .any(|flag| {
3501            flag_matches_selector(flag, selector)
3502                && !overridden_flags.contains(&flag.name)
3503                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3504        });
3505    flag_is_explicit
3506        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3507}
3508
3509/// The name of the flag a selector points at, for an error that has to name it.
3510///
3511/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3512/// about a flag that is *missing* has to say which one, and the selector may be a short
3513/// form or an alias rather than the name.
3514fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3515    out.available_flags
3516        .values()
3517        .chain(out.flags.keys())
3518        .find(|flag| flag_matches_selector(flag, selector))
3519        .map(|flag| flag.name.clone())
3520        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3521}
3522
3523/// Whether a selector's flag ended up with a value, however it got one.
3524///
3525/// The rule for a *positive* relationship, and the difference from
3526/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
3527/// exclusivity — has to count only what was given, or a flag with a default would
3528/// conflict with everything and no command line would parse. A positive one asks whether
3529/// the flag it names has a value, and a default is a value: that is already how plain
3530/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
3531/// otherwise would have made the same flag missing here and present ten lines below.
3532fn selector_is_satisfied(
3533    selector: &str,
3534    out: &ParseOutput,
3535    overridden_flags: &HashSet<String>,
3536    custom_env: Option<&HashMap<String, String>>,
3537) -> bool {
3538    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3539        return true;
3540    }
3541    let flag_is_satisfied = out
3542        .available_flags
3543        .values()
3544        .chain(out.flags.keys())
3545        .filter(|flag| flag_matches_selector(flag, selector))
3546        .any(|flag| {
3547            !overridden_flags.contains(&flag.name)
3548                && (!flag.default.is_empty()
3549                    || flag.arg.iter().any(|a| !a.default.is_empty())
3550                    || flag.default_if.iter().any(|condition| {
3551                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
3552                    }))
3553        });
3554    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3555}
3556
3557fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3558    // Bare words are positional selectors. Keep accepting a flag's internal name above
3559    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
3560    if selector.starts_with('-') {
3561        return None;
3562    }
3563    out.cmds
3564        .iter()
3565        .flat_map(active_args)
3566        .find(|arg| arg.name == selector)
3567}
3568
3569fn arg_is_explicit(
3570    arg: &SpecArg,
3571    out: &ParseOutput,
3572    custom_env: Option<&HashMap<String, String>>,
3573) -> bool {
3574    out.args.keys().any(|given| given.name == arg.name)
3575        || out
3576            .clauses
3577            .values()
3578            .flatten()
3579            .any(|instance| instance.keys().any(|given| given.name == arg.name))
3580        || arg
3581            .env
3582            .as_ref()
3583            .is_some_and(|env| env_contains(custom_env, env))
3584}
3585
3586fn apply_flag_overrides(
3587    flag: &Arc<SpecFlag>,
3588    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3589    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3590    pending_flags: &mut Vec<Arc<SpecFlag>>,
3591    overridden_flags: &mut HashSet<String>,
3592    // The reportable half of the same fact: which flag did the overriding. The set above
3593    // only stops a default or an environment value restoring what was overridden, and
3594    // "`--quiet` is unset despite its default" has no answer without the name.
3595    attributed: &mut BTreeMap<String, String>,
3596) {
3597    let overridden_names: HashSet<String> = available_flags
3598        .values()
3599        .chain(parsed_flags.keys())
3600        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3601        .map(|other| other.name.clone())
3602        .collect();
3603
3604    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3605    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3606    for name in &overridden_names {
3607        attributed.insert(name.clone(), flag.name.clone());
3608    }
3609    overridden_flags.extend(overridden_names);
3610    // An explicit occurrence always restores this flag, including self-overrides.
3611    overridden_flags.remove(&flag.name);
3612    attributed.remove(&flag.name);
3613}
3614
3615#[cfg(feature = "cli-help")]
3616fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3617    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3618}
3619
3620#[cfg(feature = "cli-help")]
3621fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3622    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3623        if !out.is_empty() {
3624            out.push('\n');
3625        }
3626        out.push_str(&docs::cli::render_help(spec, cmd, true));
3627        let mut children: Vec<_> = cmd
3628            .subcommands
3629            .values()
3630            .filter(|child| !child.hide)
3631            .collect();
3632        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3633        for child in children {
3634            append(out, spec, child);
3635        }
3636    }
3637
3638    let mut out = String::new();
3639    append(&mut out, spec, cmd);
3640    UsageErr::Help(out)
3641}
3642
3643#[cfg(not(feature = "cli-help"))]
3644fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3645    UsageErr::Help("help".to_string())
3646}
3647
3648#[cfg(not(feature = "cli-help"))]
3649fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3650    UsageErr::Help("help".to_string())
3651}
3652
3653/// The version to answer with. `--version` prefers the long text and `-V` the concise
3654/// one, each falling back to the other when only one is declared.
3655fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3656    let value = if long {
3657        spec.long_version.as_ref().or(spec.version.as_ref())
3658    } else {
3659        spec.version.as_ref().or(spec.long_version.as_ref())
3660    };
3661    UsageErr::Version(value.cloned().unwrap_or_default())
3662}
3663
3664fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3665    use crate::SpecFlagAction;
3666    match flag.action {
3667        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3668        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3669        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3670        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3671        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3672        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3673    }
3674}
3675
3676/// Report a required flag value that was displaced by a later option.
3677fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
3678    let token = flag
3679        .long
3680        .first()
3681        .map(|long| format!("--{long}"))
3682        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
3683        .unwrap_or_else(|| flag.name.clone());
3684    UsageErr::InvalidFlag {
3685        token: token.clone(),
3686        reason: "requires an argument".to_string(),
3687        span: (0, 0).into(),
3688        input: format!("{token} {following}"),
3689    }
3690}
3691
3692#[derive(Copy, Clone)]
3693struct ChoiceTarget<'a> {
3694    kind: &'a str,
3695    name: &'a str,
3696}
3697
3698impl<'a> ChoiceTarget<'a> {
3699    fn arg(arg: &'a SpecArg) -> Self {
3700        Self {
3701            kind: "arg",
3702            name: &arg.name,
3703        }
3704    }
3705
3706    fn option(flag: &'a SpecFlag) -> Self {
3707        Self {
3708            kind: "option",
3709            name: &flag.name,
3710        }
3711    }
3712}
3713
3714/// Whether every letter of a short token names a flag in scope.
3715///
3716/// Scanning stops at the first letter whose flag takes a value, because everything
3717/// after it is that value rather than more letters.
3718fn short_bundle_is_known(
3719    spec: &Spec,
3720    cmds: &[SpecCommand],
3721    available: &BTreeMap<String, Arc<SpecFlag>>,
3722    token: &str,
3723) -> bool {
3724    for c in token.chars().skip(1) {
3725        match available.get(&format!("-{c}")) {
3726            // `-h` and `-V` are recognized letters even though no spec declares them, so a
3727            // bundle containing one is a bundle. Without this `-vh` was not read as one at
3728            // all and fell through to `unexpected word`, while usage-argv, usage-go and
3729            // clap all answer it with help.
3730            None if supplied_short(spec, cmds, c).is_some() => {}
3731            None => return false,
3732            Some(f) if f.arg.is_some() => return true,
3733            Some(_) => {}
3734        }
3735    }
3736    true
3737}
3738
3739/// The response `-h` or `-V` produces where nothing declares that letter.
3740///
3741/// The letter form of the flags the parser supplies rather than a spec declaring them,
3742/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
3743/// about here one letter at a time, because a bundle is read one letter at a time.
3744///
3745/// Always the short response: `-h` is short help however many letters share its token,
3746/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
3747/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
3748fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3749    let cmd = cmds.last()?;
3750    match letter {
3751        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3752        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3753        _ => None,
3754    }
3755}
3756
3757/// Refuse a flag-like token that named nothing, if this command asked for that.
3758///
3759/// Called from the flag branches, where the lookup has just failed and nothing from
3760/// the token has been applied yet — so a bundle like `-az` is refused whole rather
3761/// than after setting `-a`.
3762fn reject_unknown_flag_if_asked(
3763    spec: &Spec,
3764    path: &[SpecCommand],
3765    token: &str,
3766) -> Result<(), UsageErr> {
3767    // A lone `-` is a value by convention. A negative number reaches this only
3768    // when no pending value opted into the narrower exception.
3769    if !is_flag_like(token) {
3770        return Ok(());
3771    }
3772    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3773        return Ok(());
3774    }
3775    Err(UsageErr::InvalidFlag {
3776        token: token.to_string(),
3777        reason: "no such flag".to_string(),
3778        span: (0, 0).into(),
3779        input: token.to_string(),
3780    })
3781}
3782
3783/// Whether a flag-like token that matches nothing is a value or an error, here.
3784///
3785/// The nearest enclosing command that stated a preference wins, then the spec,
3786/// then the default. Inherited, unlike `effect`: it describes how a command line
3787/// is read, and a CLI that forwards options tends to forward them at every level.
3788fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3789    path.iter()
3790        .rev()
3791        .find_map(|cmd| cmd.unknown_flags)
3792        .or(spec.unknown_flags)
3793        .unwrap_or_default()
3794}
3795
3796/// Whether a token would be read as a flag, for the purpose of rejecting unknown
3797/// ones.
3798///
3799/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
3800/// a field may make the narrower negative-number exception.
3801fn is_flag_like(token: &str) -> bool {
3802    match token.strip_prefix('-') {
3803        None | Some("") => false,
3804        Some(_) => true,
3805    }
3806}
3807
3808fn is_negative_number(token: &str) -> bool {
3809    token.strip_prefix('-').is_some_and(is_number)
3810}
3811
3812/// Whether a flag may claim its following token as a detached value.
3813///
3814/// A required value keeps the historical negative-number exception. A value
3815/// that may be omitted needs an explicit opt-in to distinguish a negative value
3816/// from the flag's bare form.
3817fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
3818    !flag.require_equals
3819        && (!is_flag_like(token)
3820            || flag.allow_hyphen_values()
3821            || (is_negative_number(token)
3822                && (flag
3823                    .arg
3824                    .as_ref()
3825                    .is_some_and(|arg| arg.allow_negative_numbers)
3826                    || (flag.default_missing.is_none() && !flag.value_optional))))
3827}
3828
3829fn record_scalar_flag_occurrence(
3830    cmds: &[SpecCommand],
3831    flag: &Arc<SpecFlag>,
3832    command_level: usize,
3833    bool_value: Option<bool>,
3834    occurrences: &mut HashMap<(usize, usize), u8>,
3835    errors: &mut Vec<UsageErr>,
3836) {
3837    let strict = cmds
3838        .get(command_level)
3839        .is_some_and(|cmd| !cmd.args_override_self);
3840    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3841    if !strict || flag.count || collects_values {
3842        return;
3843    }
3844
3845    let bit = match bool_value {
3846        Some(false) if flag.negate.is_some() => 0b10,
3847        _ => 0b01,
3848    };
3849    let key = (Arc::as_ptr(flag) as usize, command_level);
3850    let seen = occurrences.entry(key).or_default();
3851    if *seen & bit != 0 {
3852        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3853    }
3854    *seen |= bit;
3855}
3856
3857/// A token that can select a subcommand, trigger a mount, or be forwarded as an
3858/// external command.
3859///
3860/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
3861/// so it was never a candidate to *select* anything either. usage-argv uses the
3862/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
3863/// treated every token that `starts_with('-')` as a flag.
3864fn is_command_word(token: &str) -> bool {
3865    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3866}
3867
3868/// Whether an otherwise numeric-looking token is an exact declared short flag.
3869///
3870/// This check belongs in both parse phases: phase 1 must skip the flag while it
3871/// searches for a later subcommand, and phase 2 must bind it instead of offering
3872/// it to an `allow_negative_numbers` positional.
3873fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3874    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3875}
3876
3877/// Whether an unmatched word belongs to the root's default command.
3878///
3879/// Ordinary words always do. A negative number only does when the default command's
3880/// first positional explicitly accepts one; otherwise it stays at the root, matching
3881/// usage-argv and generated Go.
3882fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3883    !is_negative_number(token)
3884        || cmd
3885            .find_subcommand(default_name)
3886            .and_then(|default| default.args.first())
3887            .is_some_and(|arg| arg.allow_negative_numbers)
3888}
3889
3890/// Digits, at most one `.`, and an optional exponent.
3891///
3892/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
3893/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
3894/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
3895/// two disagreed about `-1e5` when one used a float parse and the other did not.
3896fn is_number(rest: &str) -> bool {
3897    let (mantissa, exponent) = match rest.find(['e', 'E']) {
3898        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3899        None => (rest, None),
3900    };
3901
3902    let mut seen_digit = false;
3903    let mut seen_dot = false;
3904    for c in mantissa.chars() {
3905        match c {
3906            '0'..='9' => seen_digit = true,
3907            '.' if !seen_dot => seen_dot = true,
3908            _ => return false,
3909        }
3910    }
3911    if !seen_digit {
3912        return false;
3913    }
3914
3915    match exponent {
3916        None => true,
3917        Some(exp) => {
3918            let digits = exp
3919                .strip_prefix('+')
3920                .or_else(|| exp.strip_prefix('-'))
3921                .unwrap_or(exp);
3922            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3923        }
3924    }
3925}
3926
3927/// Bind one value to the flag waiting for it, and let a variadic argument go on
3928/// collecting from the words that follow.
3929///
3930/// Every route to a flag's value comes through here — the following word, the text
3931/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
3932/// like — so that all three agree on how many values the flag ends up with.
3933#[allow(clippy::too_many_arguments)]
3934fn bind_pending_flag_value(
3935    spec: &Spec,
3936    cmd: &SpecCommand,
3937    errors: &mut Vec<UsageErr>,
3938    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3939    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3940    word: &mut String,
3941    input: &mut VecDeque<Token>,
3942    custom_env: Option<&HashMap<String, String>>,
3943    trace: &mut Trace,
3944    // Which token supplied `word`, and whether it rode along on the flag's own token
3945    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
3946    // their own positions and are recorded where they are read.
3947    argv: usize,
3948    attached: bool,
3949) -> miette::Result<bool> {
3950    // Held before the drain pops it, along with what the flag is already carrying: a
3951    // `var_max` bounds the values this occurrence takes, not the list they are appended
3952    // to, so a second `--include` starts counting again.
3953    let collecting = flag_awaiting_value
3954        .last()
3955        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3956        .cloned()
3957        .map(|flag| {
3958            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3959            (flag, carried)
3960        });
3961    let mut bound = vec![];
3962    let refused = drain_pending_flag_values(
3963        spec,
3964        cmd,
3965        errors,
3966        flags,
3967        flag_awaiting_value,
3968        word,
3969        custom_env,
3970        &mut bound,
3971    )?;
3972    for (flag, values) in bound {
3973        trace.record(
3974            argv,
3975            TokenRole::Value {
3976                flag,
3977                values,
3978                attached,
3979            },
3980        );
3981    }
3982    if refused {
3983        return Ok(true);
3984    }
3985    let Some((flag, carried)) = collecting else {
3986        return Ok(false);
3987    };
3988    collect_variadic_flag_values(
3989        spec,
3990        cmd,
3991        errors,
3992        flags,
3993        flag_awaiting_value,
3994        &flag,
3995        carried,
3996        input,
3997        custom_env,
3998        trace,
3999    )
4000}
4001
4002/// Keep feeding a flag whose argument is variadic from the words that follow it.
4003///
4004/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4005/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4006/// This is greedy by design — a command declaring both such a flag and positionals will
4007/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4008///
4009/// `carried` is what the flag already held when this occurrence began, so the bound
4010/// counts this run rather than everything the flag has collected across the command
4011/// line. Each value goes through the same drain as the first, so choices are checked
4012/// and the value lands in the same list rather than by a second route that could
4013/// disagree.
4014#[allow(clippy::too_many_arguments)]
4015fn collect_variadic_flag_values(
4016    spec: &Spec,
4017    cmd: &SpecCommand,
4018    errors: &mut Vec<UsageErr>,
4019    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4020    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4021    flag: &Arc<SpecFlag>,
4022    carried: usize,
4023    input: &mut VecDeque<Token>,
4024    custom_env: Option<&HashMap<String, String>>,
4025    trace: &mut Trace,
4026) -> miette::Result<bool> {
4027    let max = flag
4028        .arg
4029        .as_ref()
4030        .and_then(|arg| arg.var_max)
4031        .unwrap_or(usize::MAX);
4032    while flags
4033        .get(flag)
4034        .map(value_count)
4035        .unwrap_or(0)
4036        .saturating_sub(carried)
4037        < max
4038    {
4039        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4040            break;
4041        };
4042        if flag
4043            .arg
4044            .as_ref()
4045            .and_then(|arg| arg.value_terminator.as_deref())
4046            == Some(next)
4047        {
4048            let terminator = input.pop_front().unwrap();
4049            trace.record(
4050                terminator.argv,
4051                TokenRole::ValueTerminator {
4052                    ends: flag.name.clone(),
4053                },
4054            );
4055            break;
4056        }
4057        // The separator is left where it is: stopping here hands it to the arm that
4058        // knows what it means, rather than reading it as one more value.
4059        if next == "--"
4060            || (is_flag_like(next)
4061                && !(flag
4062                    .arg
4063                    .as_ref()
4064                    .is_some_and(|arg| arg.allow_negative_numbers)
4065                    && is_negative_number(next)))
4066        {
4067            break;
4068        }
4069        let taken = input.pop_front().unwrap();
4070        let argv = taken.argv;
4071        let mut word = taken.word;
4072        flag_awaiting_value.push(Arc::clone(flag));
4073        let mut bound = vec![];
4074        let refused = drain_pending_flag_values(
4075            spec,
4076            cmd,
4077            errors,
4078            flags,
4079            flag_awaiting_value,
4080            &mut word,
4081            custom_env,
4082            &mut bound,
4083        )?;
4084        for (flag, values) in bound {
4085            // A later word of the same occurrence is its own token, and never attached:
4086            // only the first value can ride along on the flag.
4087            trace.record(
4088                argv,
4089                TokenRole::Value {
4090                    flag,
4091                    values,
4092                    attached: false,
4093                },
4094            );
4095        }
4096        if refused {
4097            return Ok(true);
4098        }
4099    }
4100    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4101    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4102    // several values, so the run can end up *past* the bound rather than on it, and stopping
4103    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4104    // case — three values out of the one word the loop was entitled to take.
4105    //
4106    // Counted against `carried` like the loop itself, so this stays a statement about the
4107    // occurrence rather than about the list the occurrences build up.
4108    let taken = flags
4109        .get(flag)
4110        .map(value_count)
4111        .unwrap_or(0)
4112        .saturating_sub(carried);
4113    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4114        if taken < min {
4115            errors.push(UsageErr::VarFlagTooFew {
4116                name: flag.name.clone(),
4117                min,
4118                got: taken,
4119            });
4120        }
4121    }
4122    if taken > max {
4123        errors.push(UsageErr::VarFlagTooMany {
4124            name: flag.name.clone(),
4125            max,
4126            got: taken,
4127        });
4128    }
4129    Ok(false)
4130}
4131
4132/// How many values a flag is holding, for a bound that counts them.
4133fn value_count(value: &ParseValue) -> usize {
4134    match value {
4135        ParseValue::MultiString(values) => values.len(),
4136        ParseValue::MultiBool(values) => values.len(),
4137        _ => 1,
4138    }
4139}
4140
4141/// Finish a value-optional flag that was given with no value.
4142///
4143/// Returns whether anything was bound. Completions keep the flag waiting — a
4144/// half-typed `--color ` is a question about the value — so this is asked only
4145/// once a full parse has decided the value is not coming, or once the next token
4146/// has made that decision.
4147///
4148/// The missing string is a real value: if the flag names `choices`, it has to
4149/// be one of them, the same way an env var or a `default` is checked. Binding
4150/// first and failing later would leave the flag set to a value the spec forbids.
4151fn try_bind_default_missing(
4152    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4153    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4154    custom_env: Option<&HashMap<String, String>>,
4155    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4156) -> miette::Result<bool> {
4157    let Some(flag) = flag_awaiting_value.last() else {
4158        return Ok(false);
4159    };
4160    let value = match flag.default_missing.clone() {
4161        Some(value) => value,
4162        None if flag.value_optional => {
4163            let flag = flag_awaiting_value.pop().unwrap();
4164            // Presence in the map distinguishes this from an absent flag; an
4165            // empty collection distinguishes it from an explicitly empty
4166            // `--flag=` string without inventing a sentinel value.
4167            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4168            origins
4169                .entry(Arc::clone(&flag))
4170                .or_default()
4171                .push(ValueOrigin::DefaultMissing);
4172            if flag.var {
4173                // A repeated bare occurrence is still an occurrence. The string collection
4174                // uses an empty value for it, just as the concrete `default_missing` path
4175                // pushes one value per occurrence; otherwise bounds and consumers silently
4176                // lose every bare repeat after the first.
4177                flags
4178                    .entry(flag)
4179                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4180                    .try_as_multi_string_mut()
4181                    .unwrap()
4182                    .push(String::new());
4183            } else if variadic_value {
4184                // A variadic occurrence stays pending after each value. Reaching the next
4185                // flag (or EOF) closes that same occurrence; it must not erase what it took.
4186                flags
4187                    .entry(flag)
4188                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4189            } else {
4190                // A scalar pending here is a new bare occurrence. The normal permissive
4191                // repeat policy makes the later occurrence a correction, including a
4192                // correction from an explicit value back to the bare tri-state.
4193                flags.insert(flag, ParseValue::MultiString(Vec::new()));
4194            }
4195            return Ok(true);
4196        }
4197        None => return Ok(false),
4198    };
4199    if let Some(arg) = flag.arg.as_ref() {
4200        validate_choice_value(
4201            ChoiceTarget::option(flag),
4202            &value,
4203            arg.choices.as_ref(),
4204            custom_env,
4205        )?;
4206    }
4207    let flag = flag_awaiting_value.pop().unwrap();
4208    origins
4209        .entry(Arc::clone(&flag))
4210        .or_default()
4211        .push(ValueOrigin::DefaultMissing);
4212    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4213    if collecting {
4214        let arr = flags
4215            .entry(flag)
4216            .or_insert_with(|| ParseValue::MultiString(vec![]))
4217            .try_as_multi_string_mut()
4218            .unwrap();
4219        arr.push(value);
4220    } else {
4221        flags.insert(flag, ParseValue::String(value));
4222    }
4223    Ok(true)
4224}
4225
4226/// `bound` collects what each drained flag took, in the order it took it. The values are
4227/// the word after any `delimiter` split, which is the only place that split is known: by the
4228/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
4229/// second occurrence has appended to the same list.
4230#[allow(clippy::too_many_arguments)]
4231fn drain_pending_flag_values(
4232    spec: &Spec,
4233    cmd: &SpecCommand,
4234    errors: &mut Vec<UsageErr>,
4235    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4236    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4237    word: &mut String,
4238    custom_env: Option<&HashMap<String, String>>,
4239    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
4240) -> miette::Result<bool> {
4241    while let Some(flag) = flag_awaiting_value.pop() {
4242        let arg = flag.arg.as_ref().unwrap();
4243        // Split before anything judges the word, because after the split it is no longer
4244        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
4245        // Judging first would reject the whole word against a list neither half is on.
4246        let parts: Vec<String> = match arg.delimiter {
4247            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
4248            None => vec![std::mem::take(word)],
4249        };
4250        for part in &parts {
4251            if validate_choices(
4252                spec,
4253                cmd,
4254                errors,
4255                ChoiceTarget::option(&flag),
4256                part,
4257                arg.choices.as_ref(),
4258                custom_env,
4259            )? {
4260                return Ok(true);
4261            }
4262        }
4263        word.clear();
4264        bound.push((Arc::clone(&flag), parts.clone()));
4265        // Two ways to hold several values, and both record a list: a `var` flag
4266        // collects one per occurrence, a variadic argument collects several from one.
4267        if flag.var || arg.var {
4268            let arr = flags
4269                .entry(flag)
4270                .or_insert_with(|| ParseValue::MultiString(vec![]))
4271                .try_as_multi_string_mut()
4272                .unwrap();
4273            arr.extend(parts);
4274        } else {
4275            // Nowhere for a second value to go, so the word stands as it was typed. A
4276            // delimiter on a flag that takes one value is refused where it is written.
4277            flags.insert(
4278                flag,
4279                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
4280            );
4281        }
4282    }
4283    Ok(false)
4284}
4285
4286fn choice_error(
4287    target: ChoiceTarget<'_>,
4288    value: &str,
4289    choices: Option<&SpecChoices>,
4290    custom_env: Option<&HashMap<String, String>>,
4291) -> Option<String> {
4292    let choices = choices?;
4293    if !choices.strict {
4294        return None;
4295    }
4296    let values = choices.values_with_env(custom_env);
4297    if choices.matches_with_env(value, custom_env) {
4298        return None;
4299    }
4300    if let Some(env) = choices.env() {
4301        if values.is_empty() {
4302            return Some(format!(
4303                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
4304                target.kind, target.name,
4305            ));
4306        }
4307    }
4308    Some(format!(
4309        "Invalid choice for {} {}: {value}, expected one of {}",
4310        target.kind,
4311        target.name,
4312        values.join(", ")
4313    ))
4314}
4315
4316fn validate_choices(
4317    spec: &Spec,
4318    cmd: &SpecCommand,
4319    errors: &mut Vec<UsageErr>,
4320    target: ChoiceTarget<'_>,
4321    value: &str,
4322    choices: Option<&SpecChoices>,
4323    custom_env: Option<&HashMap<String, String>>,
4324) -> miette::Result<bool> {
4325    if is_help_arg(spec, cmd, value)
4326        && choices
4327            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
4328    {
4329        errors.push(render_help_err(spec, cmd, value.len() > 2));
4330        return Ok(true);
4331    }
4332
4333    if let Some(err) = choice_error(target, value, choices, custom_env) {
4334        bail!("{err}");
4335    }
4336    Ok(false)
4337}
4338
4339fn validate_choice_value(
4340    target: ChoiceTarget<'_>,
4341    value: &str,
4342    choices: Option<&SpecChoices>,
4343    custom_env: Option<&HashMap<String, String>>,
4344) -> miette::Result<()> {
4345    if let Some(err) = choice_error(target, value, choices, custom_env) {
4346        bail!("{err}");
4347    }
4348    Ok(())
4349}
4350
4351fn validate_choice_values(
4352    target: ChoiceTarget<'_>,
4353    values: &[String],
4354    choices: Option<&SpecChoices>,
4355    custom_env: Option<&HashMap<String, String>>,
4356) -> miette::Result<()> {
4357    for value in values {
4358        validate_choice_value(target, value, choices, custom_env)?;
4359    }
4360    Ok(())
4361}
4362
4363/// Everything a parse records about where it stopped: the positional cursor, so callers that
4364/// do not re-run the parse — completions, above all — agree with it, and the token trace.
4365///
4366/// Every exit from the binding phase comes through here, which is what makes it the right
4367/// place to close the trace: whatever is still queued was never read, and saying so is more
4368/// useful than leaving those words out of the report entirely.
4369fn record_stop(
4370    out: &mut ParseOutput,
4371    next_arg_idx: usize,
4372    seen_double_dash: bool,
4373    trace: &mut Trace,
4374    unread: &VecDeque<Token>,
4375) {
4376    out.next_arg = out
4377        .cmd
4378        .args
4379        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
4380        .cloned()
4381        .map(Arc::new);
4382    out.double_dash_seen = seen_double_dash;
4383    finalize_current_clause(out);
4384    trace.close(unread);
4385    out.tokens = std::mem::take(&mut trace.tokens);
4386}
4387
4388fn finalize_current_clause(out: &mut ParseOutput) {
4389    let Some(clause) = out.cmd.clause.as_ref() else {
4390        return;
4391    };
4392    out.clauses
4393        .entry(clause.name.clone())
4394        .or_default()
4395        .push(std::mem::take(&mut out.args));
4396}
4397
4398fn restore_current_clause(out: &mut ParseOutput) {
4399    let Some(clause) = out.cmd.clause.as_ref() else {
4400        return;
4401    };
4402    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
4403        out.args = current;
4404    }
4405}
4406
4407fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
4408    while active_args(cmd)
4409        .get(idx)
4410        .is_some_and(|arg| arg.sigil.is_some())
4411    {
4412        idx += 1;
4413    }
4414    idx
4415}
4416
4417fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
4418    cmd.clause
4419        .as_ref()
4420        .map(|clause| clause.args.as_slice())
4421        .unwrap_or(cmd.args.as_slice())
4422}
4423
4424fn match_sigil_arg<'a>(
4425    cmd: &'a SpecCommand,
4426    word: &'a str,
4427) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4428    cmd.args
4429        .iter()
4430        .filter_map(|arg| {
4431            let sigil = arg.sigil.as_deref()?;
4432            let value = word.strip_prefix(sigil)?;
4433            Some((arg, sigil, value))
4434        })
4435        .max_by_key(|(_, sigil, _)| sigil.len())
4436}
4437
4438fn match_sigil_arg_chain<'a>(
4439    cmds: &'a [SpecCommand],
4440    word: &'a str,
4441) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4442    cmds.iter()
4443        .filter_map(|cmd| match_sigil_arg(cmd, word))
4444        .max_by_key(|(_, sigil, _)| sigil.len())
4445}
4446
4447/// Record that `arg` was handed a word before the `--` it requires.
4448///
4449/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
4450/// message is emitted only the first time each arg is seen. The set is also what suppresses the
4451/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
4452/// end of the parse.
4453fn report_double_dash_violation(
4454    arg: &SpecArg,
4455    errors: &mut Vec<UsageErr>,
4456    violations: &mut HashSet<String>,
4457) {
4458    if violations.insert(arg.name.clone()) {
4459        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4460    }
4461}
4462
4463/// `--version` and `-V`, which the parser supplies where the spec declares a version.
4464///
4465/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
4466/// command tables — both of which accepted these spellings while this parser called them
4467/// unknown words. The help page has always listed `-V, --version` under the same
4468/// condition, and said in as many words that it did so "only where a version is
4469/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
4470/// page advertising a flag the parse refused.
4471///
4472/// The root only, because that is where the page lists it: `version` is a property of
4473/// the program, and a subcommand answering with the program's version is a claim no spec
4474/// made. A declared flag wins by arriving first — every scan consults this only after
4475/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
4476/// keeps `--version` supplied beside it.
4477fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4478    (spec.version.is_some() || spec.long_version.is_some())
4479        && cmds.len() == 1
4480        && !spec.cmd.disable_version_flag
4481        && (w == "--version" || w == "-V")
4482}
4483
4484fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4485    spec.disable_help != Some(true)
4486        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4487            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4488}
4489
4490impl ParseOutput {
4491    pub fn as_env(&self) -> BTreeMap<String, String> {
4492        let mut env = BTreeMap::new();
4493        for (flag, val) in &self.flags {
4494            let key = format!("usage_{}", crate::case::snake(&flag.name));
4495            let val = match val {
4496                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4497                ParseValue::String(s) => s.clone(),
4498                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4499                ParseValue::MultiString(s) => crate::shell_words::join(s),
4500            };
4501            env.insert(key, val);
4502        }
4503        for (arg, val) in &self.args {
4504            let key = format!("usage_{}", crate::case::snake(&arg.name));
4505            env.insert(key, val.to_string());
4506        }
4507        env
4508    }
4509}
4510
4511impl Display for ParseValue {
4512    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4513        match self {
4514            ParseValue::Bool(b) => write!(f, "{b}"),
4515            ParseValue::String(s) => write!(f, "{s}"),
4516            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4517            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4518        }
4519    }
4520}
4521
4522/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
4523fn render_token(token: &TokenBinding) -> String {
4524    let roles = token.roles.iter().map(render_role).join(", ");
4525    let synthesized = if token.synthesized { " (read as)" } else { "" };
4526    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4527}
4528
4529fn render_role(role: &TokenRole) -> String {
4530    match role {
4531        TokenRole::Program => "program".to_string(),
4532        TokenRole::Command { name } => format!("subcommand {name}"),
4533        TokenRole::Flag {
4534            flag,
4535            spelling,
4536            negated,
4537        } => {
4538            let negated = if *negated { ", negated" } else { "" };
4539            format!("flag {} as {spelling}{negated}", flag.name)
4540        }
4541        TokenRole::Value {
4542            flag,
4543            values,
4544            attached,
4545        } => {
4546            let attached = if *attached { ", attached" } else { "" };
4547            format!("value of {} = {values:?}{attached}", flag.name)
4548        }
4549        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4550        TokenRole::Sigil { arg, sigil, values } => {
4551            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
4552        }
4553        TokenRole::Separator => "separator".to_string(),
4554        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4555        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4556        TokenRole::Restart => "restart".to_string(),
4557        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
4558        TokenRole::UnknownFlag { bound_as } => match bound_as {
4559            Some(arg) => format!("unknown flag, bound as {}", arg.name),
4560            None => "unknown flag".to_string(),
4561        },
4562        TokenRole::Refused { reason } => format!("refused: {reason}"),
4563        TokenRole::External => "external".to_string(),
4564        TokenRole::Unread => "unread".to_string(),
4565    }
4566}
4567
4568impl Debug for ParseOutput {
4569    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4570        f.debug_struct("ParseOutput")
4571            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4572            .field(
4573                "args",
4574                &self
4575                    .args
4576                    .iter()
4577                    .map(|(a, w)| format!("{}: {w}", a.name))
4578                    .collect_vec(),
4579            )
4580            .field("clauses", &self.clauses)
4581            .field(
4582                "available_flags",
4583                &self
4584                    .available_flags
4585                    .iter()
4586                    .map(|(f, w)| format!("{f}: {w}"))
4587                    .collect_vec(),
4588            )
4589            .field(
4590                "flags",
4591                &self
4592                    .flags
4593                    .iter()
4594                    .map(|(f, w)| format!("{}: {w}", f.name))
4595                    .collect_vec(),
4596            )
4597            .field("flag_awaiting_value", &self.flag_awaiting_value)
4598            .field("errors", &self.errors)
4599            .field("external", &self.external)
4600            // Provenance, one line per token and one per fallback. This is the parser's
4601            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
4602            // first — `usage explain` renders the same facts for a reader.
4603            .field(
4604                "tokens",
4605                &self.tokens.iter().map(render_token).collect_vec(),
4606            )
4607            .field(
4608                "origins",
4609                &self
4610                    .flag_origins
4611                    .iter()
4612                    .map(|(f, o)| format!("{}: {o:?}", f.name))
4613                    .chain(
4614                        self.arg_origins
4615                            .iter()
4616                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
4617                    )
4618                    .collect_vec(),
4619            )
4620            .field("overridden_flags", &self.overridden_flags)
4621            .finish()
4622    }
4623}
4624
4625#[cfg(test)]
4626mod tests {
4627    use super::*;
4628    use crate::SpecFlagAction;
4629
4630    fn input(words: &[&str]) -> Vec<String> {
4631        words.iter().map(|word| (*word).to_string()).collect()
4632    }
4633
4634    #[test]
4635    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4636        // The page has always listed `-V, --version` wherever a `version` is declared,
4637        // and usage-argv and usage-go have always accepted both. This parser called them
4638        // unknown words, so the one implementation the corpus measures the others against
4639        // was the one that disagreed.
4640        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4641            .parse()
4642            .unwrap();
4643
4644        for spelling in ["--version", "-V"] {
4645            let err = parse(&spec, &input(&["ex", spelling]))
4646                .expect_err("answering with a version ends the parse");
4647            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4648        }
4649    }
4650
4651    #[test]
4652    fn the_supplied_version_flag_is_the_roots_alone() {
4653        // `version` describes the program, and the page lists the entry on the program's
4654        // own page only. A subcommand answering with it would be a claim no spec made.
4655        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4656            .parse()
4657            .unwrap();
4658
4659        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4660        assert_eq!(err.to_string(), "unexpected word: --version");
4661    }
4662
4663    #[test]
4664    fn no_declared_version_supplies_nothing() {
4665        // A `--version` answering with nothing is worse than one that is not there, which
4666        // is why the entry is conditional on the page and the spelling on the parse.
4667        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4668
4669        for spelling in ["--version", "-V"] {
4670            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4671            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4672        }
4673    }
4674
4675    #[test]
4676    fn disable_version_flag_removes_the_supplied_spellings() {
4677        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4678            .parse()
4679            .unwrap();
4680
4681        for spelling in ["--version", "-V"] {
4682            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4683            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4684        }
4685    }
4686
4687    #[test]
4688    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4689        // The page drops each supplied spelling the CLI claimed and keeps the other; the
4690        // parse agrees without being told, because a declared flag is matched first.
4691        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4692            .parse()
4693            .unwrap();
4694
4695        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4696        assert_eq!(out.flags.len(), 1);
4697
4698        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4699        assert_eq!(err.to_string(), "1.2.3");
4700    }
4701
4702    #[test]
4703    fn the_supplied_spellings_split_the_two_version_texts() {
4704        // The same split `render_action_err` gives a declared version flag: the long
4705        // spelling prefers `long_version`, the short prefers the concise one.
4706        let spec: Spec =
4707            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4708                .parse()
4709                .unwrap();
4710
4711        assert_eq!(
4712            parse(&spec, &input(&["ex", "--version"]))
4713                .unwrap_err()
4714                .to_string(),
4715            "1.2.3 (abcdef)"
4716        );
4717        assert_eq!(
4718            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4719            "1.2.3"
4720        );
4721    }
4722
4723    #[test]
4724    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4725        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
4726        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
4727        // naming nothing: usage-argv and usage-go both resolve the letter through the
4728        // same lookup that finds a declared short, and clap prints help for it too.
4729        let spec: Spec =
4730            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4731                .parse()
4732                .unwrap();
4733
4734        for token in ["-vh", "-hv"] {
4735            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4736            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4737        }
4738        for token in ["-vV", "-Vv"] {
4739            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4740            assert_eq!(err.to_string(), "1.2.3", "{token}");
4741        }
4742    }
4743
4744    #[test]
4745    fn a_bundled_help_letter_asks_for_the_short_page() {
4746        // Whatever else shares the token: `-h` is the short spelling, and the letters
4747        // beside it say nothing about which page was asked for.
4748        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"
4749            .parse()
4750            .unwrap();
4751
4752        let short = parse(&spec, &input(&["ex", "-vh"]))
4753            .unwrap_err()
4754            .to_string();
4755        let long = parse(&spec, &input(&["ex", "--help"]))
4756            .unwrap_err()
4757            .to_string();
4758        assert!(short.contains("Be loud"), "{short}");
4759        assert!(!short.contains("at length"), "{short}");
4760        assert!(long.contains("at length"), "{long}");
4761    }
4762
4763    #[test]
4764    fn the_bundled_version_letter_is_the_roots_alone() {
4765        // The same rule the whole-token spelling follows, asked one letter at a time.
4766        let spec: Spec =
4767            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4768                .parse()
4769                .unwrap();
4770
4771        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4772        assert_eq!(err.to_string(), "unexpected word: -vV");
4773    }
4774
4775    #[test]
4776    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4777        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
4778        // this spec's own `-h`, taking its value from the rest of the token.
4779        let spec: Spec =
4780            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4781                .parse()
4782                .unwrap();
4783
4784        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4785        assert_eq!(out.flags.len(), 2);
4786        assert!(out
4787            .flags
4788            .iter()
4789            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4790    }
4791
4792    #[test]
4793    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4794        let spec: Spec =
4795            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4796                .parse()
4797                .unwrap();
4798
4799        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4800        assert_eq!(err.to_string(), "unexpected word: -vh");
4801    }
4802
4803    #[test]
4804    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4805        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
4806        // on the way to discovering that `z` names nothing.
4807        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4808            .parse()
4809            .unwrap();
4810
4811        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4812        assert!(out.flags.is_empty(), "{:?}", out.flags);
4813        assert_eq!(out.args.len(), 1);
4814    }
4815
4816    fn spec_with_arg(arg: SpecArg) -> Spec {
4817        let cmd = SpecCommand::builder().name("test").arg(arg).build();
4818        Spec {
4819            name: "test".to_string(),
4820            bin: "test".to_string(),
4821            cmd,
4822            ..Default::default()
4823        }
4824    }
4825
4826    fn spec_with_flag(flag: SpecFlag) -> Spec {
4827        let cmd = SpecCommand::builder().name("test").flag(flag).build();
4828        Spec {
4829            name: "test".to_string(),
4830            bin: "test".to_string(),
4831            cmd,
4832            ..Default::default()
4833        }
4834    }
4835
4836    fn parse_with_env(
4837        spec: &Spec,
4838        words: &[&str],
4839        env: &[(&str, &str)],
4840    ) -> Result<ParseOutput, miette::Error> {
4841        let env = env
4842            .iter()
4843            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4844            .collect();
4845        Parser::new(spec).with_env(env).parse(&input(words))
4846    }
4847
4848    fn first_string_value(parsed: &ParseOutput) -> &str {
4849        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4850            return value;
4851        }
4852        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4853            return value;
4854        }
4855        panic!("expected first parsed value to be ParseValue::String");
4856    }
4857
4858    #[test]
4859    fn custom_environment_parser_dispatches_executable_views() {
4860        let spec: Spec = r#"
4861bin "ex"
4862view "runner" root="run"
4863cmd "run" {
4864    flag "--token <token>" env="TOKEN"
4865}
4866        "#
4867        .parse()
4868        .unwrap();
4869        let parsed = Parser::new(&spec)
4870            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4871            .parse(&input(&["runner"]))
4872            .unwrap();
4873
4874        assert_eq!(parsed.cmd.name, "runner");
4875        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4876            && matches!(value, ParseValue::String(value) if value == "secret")));
4877    }
4878
4879    #[test]
4880    fn an_executable_view_keeps_the_hosts_version_action() {
4881        let spec: Spec = r#"
4882bin "ex"
4883version "1.2.3"
4884flag "-V --version" action="version"
4885flag "--verbose" global=#true
4886view "runner" root="run" globals=#true
4887cmd "run"
4888        "#
4889        .parse()
4890        .unwrap();
4891
4892        let error = Parser::new(&spec)
4893            .parse(&input(&["runner", "--version"]))
4894            .expect_err("the host version action should answer before view projection");
4895        assert_eq!(error.to_string(), "1.2.3");
4896
4897        let error = Parser::new(&spec)
4898            .parse(&input(&["runner", "--verbose", "--version"]))
4899            .expect_err("the host version action should remain after a carried global");
4900        assert_eq!(error.to_string(), "1.2.3");
4901    }
4902
4903    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4904        let flag = parsed
4905            .flags
4906            .keys()
4907            .find(|flag| flag.name == name)
4908            .unwrap_or_else(|| panic!("expected flag {name}"));
4909        let value = parsed
4910            .flags
4911            .get(flag)
4912            .unwrap_or_else(|| panic!("expected value for flag {name}"));
4913        match value {
4914            ParseValue::String(value) => value,
4915            _ => panic!("expected flag {name} to be ParseValue::String"),
4916        }
4917    }
4918
4919    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4920        let err = result.expect_err("expected parser error");
4921        assert_eq!(format!("{err}"), expected);
4922    }
4923
4924    #[test]
4925    fn a_short_version_action_falls_back_to_the_long_version() {
4926        let flag = SpecFlag::builder()
4927            .short('R')
4928            .action(SpecFlagAction::Version)
4929            .build();
4930        let spec = Spec {
4931            name: "test".to_string(),
4932            bin: "test".to_string(),
4933            long_version: Some("1.2.3\ncommit abc123".to_string()),
4934            ..Default::default()
4935        };
4936        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4937            panic!("expected version action")
4938        };
4939        assert_eq!(version, "1.2.3\ncommit abc123");
4940    }
4941
4942    #[cfg(feature = "unstable_choices_env")]
4943    fn spec_arg_choices_env(key: &str) -> Spec {
4944        spec_with_arg(
4945            SpecArg::builder()
4946                .name("env")
4947                .choices_env(key)
4948                .required(false)
4949                .build(),
4950        )
4951    }
4952
4953    #[cfg(feature = "unstable_choices_env")]
4954    fn spec_flag_choices_env(key: &str) -> Spec {
4955        spec_with_flag(
4956            SpecFlag::builder()
4957                .long("env")
4958                .arg(SpecArg::builder().name("env").choices_env(key).build())
4959                .build(),
4960        )
4961    }
4962
4963    #[test]
4964    fn test_parse() {
4965        let cmd = SpecCommand::builder()
4966            .name("test")
4967            .arg(SpecArg::builder().name("arg").build())
4968            .flag(SpecFlag::builder().long("flag").build())
4969            .build();
4970        let spec = Spec {
4971            name: "test".to_string(),
4972            bin: "test".to_string(),
4973            cmd,
4974            ..Default::default()
4975        };
4976        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4977        let parsed = parse(&spec, &input).unwrap();
4978        assert_eq!(parsed.cmds.len(), 1);
4979        assert_eq!(parsed.cmds[0].name, "test");
4980        assert_eq!(parsed.args.len(), 1);
4981        assert_eq!(parsed.flags.len(), 1);
4982        assert_eq!(parsed.available_flags.len(), 1);
4983    }
4984
4985    #[test]
4986    fn test_flag_overrides_last_occurrence_wins() {
4987        let spec: Spec = r#"
4988flag "--stdin" default=#true
4989flag "--file <file>" overrides="--stdin"
4990        "#
4991        .parse()
4992        .unwrap();
4993
4994        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4995        assert_eq!(file_wins.flags.len(), 1);
4996        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4997        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4998
4999        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5000        assert_eq!(stdin_wins.flags.len(), 1);
5001        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5002        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5003    }
5004
5005    #[test]
5006    fn test_flag_override_clears_pending_value() {
5007        let spec: Spec = r#"
5008flag "--file <file>" overrides="--stdin"
5009flag "--stdin"
5010arg "[input]"
5011        "#
5012        .parse()
5013        .unwrap();
5014
5015        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5016        assert_eq!(parsed.flags.len(), 1);
5017        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5018        assert_eq!(first_string_value(&parsed), "input.txt");
5019    }
5020
5021    #[cfg(unix)]
5022    #[test]
5023    fn a_mount_on_the_root_discovers_subcommands() {
5024        // The root is a command like any other, so it can find its own subcommands
5025        // by running something. Uses `echo` rather than a fixture because resolving
5026        // a mount is what is being tested.
5027        let spec: Spec = r#"
5028name "ex"
5029bin "ex"
5030cmd "declared"
5031mount run="echo 'cmd \"discovered\"'"
5032"#
5033        .parse()
5034        .unwrap();
5035
5036        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5037        assert_eq!(out.cmd.name, "discovered");
5038    }
5039
5040    #[test]
5041    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5042        let spec: Spec = r#"
5043name "ex"
5044bin "ex"
5045mount run="this command must never run"
5046cmd "declared"
5047"#
5048        .parse()
5049        .unwrap();
5050
5051        Parser::new(&spec)
5052            .with_mount_outputs(HashMap::new())
5053            .parse(&input(&["ex", "declared"]))
5054            .expect("a declared command does not resolve the mount");
5055
5056        let error = Parser::new(&spec)
5057            .with_mount_outputs(HashMap::new())
5058            .parse(&input(&["ex", "discovered"]))
5059            .unwrap_err();
5060        assert!(
5061            error
5062                .to_string()
5063                .contains("No injected output was provided for mount command"),
5064            "{error}"
5065        );
5066    }
5067
5068    #[cfg(unix)]
5069    #[test]
5070    fn completion_sees_root_mounted_commands_with_nothing_typed() {
5071        // The case a root mount exists for. `mycli <tab>` has no word to trigger
5072        // discovery with, so a completion has to resolve up front or the mounted
5073        // commands are never offered.
5074        let spec: Spec = r#"
5075name "ex"
5076bin "ex"
5077cmd "declared"
5078mount run="echo 'cmd \"discovered\"'"
5079"#
5080        .parse()
5081        .unwrap();
5082
5083        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5084        assert!(
5085            out.cmd.subcommands.contains_key("discovered"),
5086            "a completion should see mounted commands; got {:?}",
5087            out.cmd.subcommands.keys().collect::<Vec<_>>()
5088        );
5089    }
5090
5091    #[cfg(unix)]
5092    #[test]
5093    fn completion_and_execution_agree_about_discovery() {
5094        // Offering a command that a real parse would hand to the default instead is
5095        // worse than not offering it, so the gate applies to both paths. The mount
5096        // fails if it runs, which is how both halves are checked at once.
5097        let spec: Spec = r#"
5098name "ex"
5099bin "ex"
5100default_subcommand "run"
5101cmd "run" {
5102  arg "<task>"
5103}
5104mount run="exit 1"
5105"#
5106        .parse()
5107        .unwrap();
5108
5109        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5110        assert!(
5111            !out.cmd.subcommands.contains_key("discovered"),
5112            "a completion must not offer what execution will not route"
5113        );
5114
5115        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5116        assert_eq!(out.cmd.name, "run");
5117    }
5118
5119    #[cfg(unix)]
5120    #[test]
5121    fn a_default_subcommand_outranks_discovery() {
5122        // The default already says what an unmatched word means, and says it for
5123        // free. The mount fails if it runs, so parsing proves discovery was skipped.
5124        let spec: Spec = r#"
5125name "ex"
5126bin "ex"
5127default_subcommand "run"
5128cmd "run" {
5129  arg "<task>"
5130}
5131mount run="exit 1"
5132"#
5133        .parse()
5134        .unwrap();
5135
5136        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5137        assert_eq!(out.cmd.name, "run");
5138    }
5139
5140    #[cfg(unix)]
5141    #[test]
5142    fn a_mount_may_ask_to_outrank_the_default() {
5143        // Opting in, and paying for it: discovery runs first, so a discovered
5144        // command wins over the fallback.
5145        let spec: Spec = r#"
5146name "ex"
5147bin "ex"
5148default_subcommand "run"
5149cmd "run" {
5150  arg "<task>"
5151}
5152mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5153"#
5154        .parse()
5155        .unwrap();
5156
5157        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5158        assert_eq!(out.cmd.name, "discovered");
5159
5160        // A word it does not know still reaches the default.
5161        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5162        assert_eq!(out.cmd.name, "run");
5163    }
5164
5165    #[cfg(unix)]
5166    #[test]
5167    fn a_flag_does_not_run_the_mount() {
5168        // A flag matches no subcommand, which would have been enough to trigger
5169        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
5170        // so parsing at all is the proof that it did not.
5171        let spec: Spec = r#"
5172name "ex"
5173bin "ex"
5174flag "--verbose"
5175cmd "declared"
5176mount run="exit 1"
5177"#
5178        .parse()
5179        .unwrap();
5180
5181        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
5182        assert_eq!(out.cmd.name, "ex");
5183    }
5184
5185    #[cfg(unix)]
5186    #[test]
5187    fn a_declared_subcommand_does_not_run_the_mount() {
5188        // The mount would fail if it ran, so this parsing at all is the proof that
5189        // discovery is skipped when the word is already known. Worth pinning: a root
5190        // mount that resolved eagerly would spawn a process on every invocation.
5191        let spec: Spec = r#"
5192name "ex"
5193bin "ex"
5194cmd "declared"
5195mount run="exit 1"
5196"#
5197        .parse()
5198        .unwrap();
5199
5200        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
5201        assert_eq!(out.cmd.name, "declared");
5202    }
5203
5204    #[test]
5205    fn a_root_mount_survives_being_written_out() {
5206        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
5207            .parse()
5208            .unwrap();
5209        assert_eq!(spec.cmd.mounts.len(), 1);
5210
5211        let reparsed: Spec = spec.to_string().parse().unwrap();
5212        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
5213        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
5214    }
5215
5216    #[test]
5217    fn test_mount_prefix_applies_flag_overrides() {
5218        let stdin = Arc::new(
5219            SpecFlag::builder()
5220                .name("stdin")
5221                .long("stdin")
5222                .global(true)
5223                .build(),
5224        );
5225        let file = Arc::new(
5226            SpecFlag::builder()
5227                .name("file")
5228                .long("file")
5229                .arg(SpecArg::builder().name("file").build())
5230                .global(true)
5231                .overrides_with(vec!["--stdin".to_string()])
5232                .build(),
5233        );
5234        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
5235
5236        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
5237        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
5238
5239        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
5240    }
5241
5242    #[test]
5243    fn test_flag_override_suppresses_env_value() {
5244        let spec: Spec = r#"
5245flag "--stdin" env="USE_STDIN"
5246flag "--file <file>" overrides="--stdin"
5247        "#
5248        .parse()
5249        .unwrap();
5250
5251        let parsed = parse_with_env(
5252            &spec,
5253            &["test", "--file", "input.txt"],
5254            &[("USE_STDIN", "true")],
5255        )
5256        .unwrap();
5257        assert_eq!(parsed.flags.len(), 1);
5258        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5259    }
5260
5261    #[test]
5262    fn test_flag_override_suppresses_required_check() {
5263        let spec: Spec = r#"
5264flag "--stdin" required=#true
5265flag "--file <file>" overrides="--stdin"
5266        "#
5267        .parse()
5268        .unwrap();
5269
5270        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5271        assert_eq!(parsed.flags.len(), 1);
5272        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5273    }
5274
5275    #[test]
5276    fn test_flag_required_if() {
5277        let spec: Spec = r#"
5278flag "--dir <dir>"
5279flag "--file <file>" required_if="--dir"
5280        "#
5281        .parse()
5282        .unwrap();
5283
5284        parse(&spec, &input(&["test"])).unwrap();
5285        assert_parse_err(
5286            parse(&spec, &input(&["test", "--dir", "src"])),
5287            "Missing required flag: --file <file>",
5288        );
5289        parse(
5290            &spec,
5291            &input(&["test", "--dir", "src", "--file", "input.txt"]),
5292        )
5293        .unwrap();
5294    }
5295
5296    #[test]
5297    fn test_flag_required_unless() {
5298        let spec: Spec = r#"
5299flag "--stdin"
5300flag "--file <file>" required_unless="--stdin"
5301        "#
5302        .parse()
5303        .unwrap();
5304
5305        assert_parse_err(
5306            parse(&spec, &input(&["test"])),
5307            "Missing required flag: --file <file>",
5308        );
5309        parse(&spec, &input(&["test", "--stdin"])).unwrap();
5310        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5311    }
5312
5313    #[test]
5314    fn complete_required_relationship_truth_tables() {
5315        let spec: Spec = r#"
5316name "ex"
5317bin "ex"
5318flag "--mode <mode>"
5319flag "--scope <scope>"
5320flag "--token <token>" {
5321    required_if_eq "--mode" "remote"
5322}
5323flag "--approval <approval>" {
5324    required_if_eq_all "--mode" "remote" "--scope" "global"
5325}
5326flag "--input <input>" {
5327    required_unless "--stdin" "--file"
5328}
5329flag "--checksum <checksum>" {
5330    required_unless_all "--stdin" "--file"
5331}
5332flag "--stdin"
5333flag "--file <file>"
5334arg "[request]" {
5335    requires "--mode" "--scope"
5336}
5337"#
5338        .parse()
5339        .unwrap();
5340        let parse_args = |args: &[&str]| {
5341            parse(
5342                &spec,
5343                &args
5344                    .iter()
5345                    .map(|arg| (*arg).to_string())
5346                    .collect::<Vec<_>>(),
5347            )
5348        };
5349
5350        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
5351        assert!(parse_args(&[
5352            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
5353        ])
5354        .is_err());
5355        parse_args(&[
5356            "ex",
5357            "--mode",
5358            "remote",
5359            "--token",
5360            "secret",
5361            "--scope",
5362            "global",
5363            "--approval",
5364            "yes",
5365            "--stdin",
5366            "--file",
5367            "in",
5368        ])
5369        .unwrap();
5370        parse_args(&[
5371            "ex",
5372            "--mode",
5373            "local",
5374            "--scope",
5375            "project",
5376            "--stdin",
5377            "--checksum",
5378            "sum",
5379            "request.json",
5380        ])
5381        .unwrap();
5382
5383        let reparsed: Spec = spec.to_string().parse().unwrap();
5384        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
5385        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
5386        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
5387        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
5388    }
5389
5390    #[test]
5391    fn test_conditional_requirements_treat_env_as_explicit() {
5392        let spec: Spec = r#"
5393flag "--dir <dir>" env="INPUT_DIR"
5394flag "--stdin" env="USE_STDIN"
5395flag "--file <file>" required_if="--dir" required_unless="--stdin"
5396        "#
5397        .parse()
5398        .unwrap();
5399
5400        assert_parse_err(
5401            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5402            "Missing required flag: --file <file>",
5403        );
5404        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5405    }
5406
5407    #[test]
5408    fn test_custom_env_does_not_fall_back_to_process_env() {
5409        assert!(std::env::var("PATH").is_ok());
5410        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5411
5412        assert_parse_err(
5413            parse_with_env(&spec, &["test"], &[]),
5414            "Missing required flag: --file <file>",
5415        );
5416    }
5417
5418    #[test]
5419    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5420        let spec: Spec = r#"
5421flag "--dir <dir>" default="src"
5422flag "--file <file>" required_if="--dir"
5423        "#
5424        .parse()
5425        .unwrap();
5426
5427        parse(&spec, &input(&["test"])).unwrap();
5428    }
5429
5430    #[test]
5431    fn test_conditional_requirements_see_overridden_flags_as_absent() {
5432        let spec: Spec = r#"
5433flag "--stdin"
5434flag "--dir <dir>" overrides="--stdin"
5435flag "--file <file>" required_unless="--stdin"
5436        "#
5437        .parse()
5438        .unwrap();
5439
5440        assert_parse_err(
5441            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5442            "Missing required flag: --file <file>",
5443        );
5444    }
5445
5446    #[test]
5447    fn short_flag_is_one_character_not_one_byte() {
5448        // A short is declared and read by character. Counting bytes instead either
5449        // refuses the declaration or slices the token inside the character, and clap
5450        // — which many specs are generated from — accepts shorts like this one.
5451        let spec = spec_with_flag(
5452            SpecFlag::builder()
5453                .short('磨')
5454                .long("polish")
5455                .arg(SpecArg::builder().name("opt").build())
5456                .build(),
5457        );
5458        let attached = Parser::new(&spec)
5459            .parse(&input(&["test", "-磨VALUE"]))
5460            .unwrap();
5461        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5462        let detached = Parser::new(&spec)
5463            .parse(&input(&["test", "-磨", "V"]))
5464            .unwrap();
5465        assert_eq!(flag_string_value(&detached, "polish"), "V");
5466    }
5467
5468    #[test]
5469    fn test_as_env() {
5470        let cmd = SpecCommand::builder()
5471            .name("test")
5472            .arg(SpecArg::builder().name("arg").build())
5473            .flag(SpecFlag::builder().long("flag").build())
5474            .flag(
5475                SpecFlag::builder()
5476                    .long("force")
5477                    .negate("--no-force")
5478                    .build(),
5479            )
5480            .build();
5481        let spec = Spec {
5482            name: "test".to_string(),
5483            bin: "test".to_string(),
5484            cmd,
5485            ..Default::default()
5486        };
5487        let input = vec![
5488            "test".to_string(),
5489            "--flag".to_string(),
5490            "--no-force".to_string(),
5491        ];
5492        let parsed = parse(&spec, &input).unwrap();
5493        let env = parsed.as_env();
5494        assert_eq!(env.len(), 2);
5495        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5496        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5497    }
5498
5499    #[test]
5500    fn test_arg_env_var() {
5501        let cmd = SpecCommand::builder()
5502            .name("test")
5503            .arg(
5504                SpecArg::builder()
5505                    .name("input")
5506                    .env("TEST_ARG_INPUT")
5507                    .required(true)
5508                    .build(),
5509            )
5510            .build();
5511        let spec = Spec {
5512            name: "test".to_string(),
5513            bin: "test".to_string(),
5514            cmd,
5515            ..Default::default()
5516        };
5517
5518        // Set env var
5519        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5520
5521        let input = vec!["test".to_string()];
5522        let parsed = parse(&spec, &input).unwrap();
5523
5524        assert_eq!(parsed.args.len(), 1);
5525        let arg = parsed.args.keys().next().unwrap();
5526        assert_eq!(arg.name, "input");
5527        let value = parsed.args.values().next().unwrap();
5528        assert_eq!(value.to_string(), "test_file.txt");
5529
5530        // Clean up
5531        std::env::remove_var("TEST_ARG_INPUT");
5532    }
5533
5534    #[test]
5535    fn test_flag_env_var_with_arg() {
5536        let cmd = SpecCommand::builder()
5537            .name("test")
5538            .flag(
5539                SpecFlag::builder()
5540                    .long("output")
5541                    .env("TEST_FLAG_OUTPUT")
5542                    .arg(SpecArg::builder().name("file").build())
5543                    .build(),
5544            )
5545            .build();
5546        let spec = Spec {
5547            name: "test".to_string(),
5548            bin: "test".to_string(),
5549            cmd,
5550            ..Default::default()
5551        };
5552
5553        // Set env var
5554        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5555
5556        let input = vec!["test".to_string()];
5557        let parsed = parse(&spec, &input).unwrap();
5558
5559        assert_eq!(parsed.flags.len(), 1);
5560        let flag = parsed.flags.keys().next().unwrap();
5561        assert_eq!(flag.name, "output");
5562        let value = parsed.flags.values().next().unwrap();
5563        assert_eq!(value.to_string(), "output.txt");
5564
5565        // Clean up
5566        std::env::remove_var("TEST_FLAG_OUTPUT");
5567    }
5568
5569    #[test]
5570    fn test_flag_env_var_boolean() {
5571        let cmd = SpecCommand::builder()
5572            .name("test")
5573            .flag(
5574                SpecFlag::builder()
5575                    .long("verbose")
5576                    .env("TEST_FLAG_VERBOSE")
5577                    .build(),
5578            )
5579            .build();
5580        let spec = Spec {
5581            name: "test".to_string(),
5582            bin: "test".to_string(),
5583            cmd,
5584            ..Default::default()
5585        };
5586
5587        // Set env var to true
5588        std::env::set_var("TEST_FLAG_VERBOSE", "true");
5589
5590        let input = vec!["test".to_string()];
5591        let parsed = parse(&spec, &input).unwrap();
5592
5593        assert_eq!(parsed.flags.len(), 1);
5594        let flag = parsed.flags.keys().next().unwrap();
5595        assert_eq!(flag.name, "verbose");
5596        let value = parsed.flags.values().next().unwrap();
5597        assert_eq!(value.to_string(), "true");
5598
5599        // Clean up
5600        std::env::remove_var("TEST_FLAG_VERBOSE");
5601    }
5602
5603    #[test]
5604    fn test_env_var_precedence() {
5605        // CLI args should take precedence over env vars
5606        let cmd = SpecCommand::builder()
5607            .name("test")
5608            .arg(
5609                SpecArg::builder()
5610                    .name("input")
5611                    .env("TEST_PRECEDENCE_INPUT")
5612                    .required(true)
5613                    .build(),
5614            )
5615            .build();
5616        let spec = Spec {
5617            name: "test".to_string(),
5618            bin: "test".to_string(),
5619            cmd,
5620            ..Default::default()
5621        };
5622
5623        // Set env var
5624        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5625
5626        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5627        let parsed = parse(&spec, &input).unwrap();
5628
5629        assert_eq!(parsed.args.len(), 1);
5630        let value = parsed.args.values().next().unwrap();
5631        // CLI arg should take precedence
5632        assert_eq!(value.to_string(), "cli_file.txt");
5633
5634        // Clean up
5635        std::env::remove_var("TEST_PRECEDENCE_INPUT");
5636    }
5637
5638    #[test]
5639    fn test_flag_var_true_with_single_default() {
5640        // When var=true and default="bar", the default should be MultiString(["bar"])
5641        let cmd = SpecCommand::builder()
5642            .name("test")
5643            .flag(
5644                SpecFlag::builder()
5645                    .long("foo")
5646                    .var(true)
5647                    .arg(SpecArg::builder().name("foo").build())
5648                    .default_value("bar")
5649                    .build(),
5650            )
5651            .build();
5652        let spec = Spec {
5653            name: "test".to_string(),
5654            bin: "test".to_string(),
5655            cmd,
5656            ..Default::default()
5657        };
5658
5659        // User doesn't provide the flag
5660        let input = vec!["test".to_string()];
5661        let parsed = parse(&spec, &input).unwrap();
5662
5663        assert_eq!(parsed.flags.len(), 1);
5664        let flag = parsed.flags.keys().next().unwrap();
5665        assert_eq!(flag.name, "foo");
5666        let value = parsed.flags.values().next().unwrap();
5667        // Should be MultiString, not String
5668        match value {
5669            ParseValue::MultiString(v) => {
5670                assert_eq!(v.len(), 1);
5671                assert_eq!(v[0], "bar");
5672            }
5673            _ => panic!("Expected MultiString, got {:?}", value),
5674        }
5675    }
5676
5677    #[test]
5678    fn test_flag_var_true_with_multiple_defaults() {
5679        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
5680        let cmd = SpecCommand::builder()
5681            .name("test")
5682            .flag(
5683                SpecFlag::builder()
5684                    .long("foo")
5685                    .var(true)
5686                    .arg(SpecArg::builder().name("foo").build())
5687                    .default_values(["xyz", "bar"])
5688                    .build(),
5689            )
5690            .build();
5691        let spec = Spec {
5692            name: "test".to_string(),
5693            bin: "test".to_string(),
5694            cmd,
5695            ..Default::default()
5696        };
5697
5698        // User doesn't provide the flag
5699        let input = vec!["test".to_string()];
5700        let parsed = parse(&spec, &input).unwrap();
5701
5702        assert_eq!(parsed.flags.len(), 1);
5703        let value = parsed.flags.values().next().unwrap();
5704        // Should be MultiString with both values
5705        match value {
5706            ParseValue::MultiString(v) => {
5707                assert_eq!(v.len(), 2);
5708                assert_eq!(v[0], "xyz");
5709                assert_eq!(v[1], "bar");
5710            }
5711            _ => panic!("Expected MultiString, got {:?}", value),
5712        }
5713    }
5714
5715    #[test]
5716    fn test_flag_var_false_with_default_remains_string() {
5717        // When var=false (default), the default should still be String("bar")
5718        let cmd = SpecCommand::builder()
5719            .name("test")
5720            .flag(
5721                SpecFlag::builder()
5722                    .long("foo")
5723                    .var(false) // Default behavior
5724                    .arg(SpecArg::builder().name("foo").build())
5725                    .default_value("bar")
5726                    .build(),
5727            )
5728            .build();
5729        let spec = Spec {
5730            name: "test".to_string(),
5731            bin: "test".to_string(),
5732            cmd,
5733            ..Default::default()
5734        };
5735
5736        // User doesn't provide the flag
5737        let input = vec!["test".to_string()];
5738        let parsed = parse(&spec, &input).unwrap();
5739
5740        assert_eq!(parsed.flags.len(), 1);
5741        let value = parsed.flags.values().next().unwrap();
5742        // Should be String, not MultiString
5743        match value {
5744            ParseValue::String(s) => {
5745                assert_eq!(s, "bar");
5746            }
5747            _ => panic!("Expected String, got {:?}", value),
5748        }
5749    }
5750
5751    #[test]
5752    fn test_arg_var_true_with_single_default() {
5753        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
5754        let cmd = SpecCommand::builder()
5755            .name("test")
5756            .arg(
5757                SpecArg::builder()
5758                    .name("files")
5759                    .var(true)
5760                    .default_value("default.txt")
5761                    .required(false)
5762                    .build(),
5763            )
5764            .build();
5765        let spec = Spec {
5766            name: "test".to_string(),
5767            bin: "test".to_string(),
5768            cmd,
5769            ..Default::default()
5770        };
5771
5772        // User doesn't provide the arg
5773        let input = vec!["test".to_string()];
5774        let parsed = parse(&spec, &input).unwrap();
5775
5776        assert_eq!(parsed.args.len(), 1);
5777        let value = parsed.args.values().next().unwrap();
5778        // Should be MultiString, not String
5779        match value {
5780            ParseValue::MultiString(v) => {
5781                assert_eq!(v.len(), 1);
5782                assert_eq!(v[0], "default.txt");
5783            }
5784            _ => panic!("Expected MultiString, got {:?}", value),
5785        }
5786    }
5787
5788    #[test]
5789    fn test_arg_var_true_with_multiple_defaults() {
5790        // When arg has var=true and multiple defaults
5791        let cmd = SpecCommand::builder()
5792            .name("test")
5793            .arg(
5794                SpecArg::builder()
5795                    .name("files")
5796                    .var(true)
5797                    .default_values(["file1.txt", "file2.txt"])
5798                    .required(false)
5799                    .build(),
5800            )
5801            .build();
5802        let spec = Spec {
5803            name: "test".to_string(),
5804            bin: "test".to_string(),
5805            cmd,
5806            ..Default::default()
5807        };
5808
5809        // User doesn't provide the arg
5810        let input = vec!["test".to_string()];
5811        let parsed = parse(&spec, &input).unwrap();
5812
5813        assert_eq!(parsed.args.len(), 1);
5814        let value = parsed.args.values().next().unwrap();
5815        // Should be MultiString with both values
5816        match value {
5817            ParseValue::MultiString(v) => {
5818                assert_eq!(v.len(), 2);
5819                assert_eq!(v[0], "file1.txt");
5820                assert_eq!(v[1], "file2.txt");
5821            }
5822            _ => panic!("Expected MultiString, got {:?}", value),
5823        }
5824    }
5825
5826    #[test]
5827    fn test_arg_var_false_with_default_remains_string() {
5828        // When arg has var=false (default), the default should still be String
5829        let cmd = SpecCommand::builder()
5830            .name("test")
5831            .arg(
5832                SpecArg::builder()
5833                    .name("file")
5834                    .var(false)
5835                    .default_value("default.txt")
5836                    .required(false)
5837                    .build(),
5838            )
5839            .build();
5840        let spec = Spec {
5841            name: "test".to_string(),
5842            bin: "test".to_string(),
5843            cmd,
5844            ..Default::default()
5845        };
5846
5847        // User doesn't provide the arg
5848        let input = vec!["test".to_string()];
5849        let parsed = parse(&spec, &input).unwrap();
5850
5851        assert_eq!(parsed.args.len(), 1);
5852        let value = parsed.args.values().next().unwrap();
5853        // Should be String, not MultiString
5854        match value {
5855            ParseValue::String(s) => {
5856                assert_eq!(s, "default.txt");
5857            }
5858            _ => panic!("Expected String, got {:?}", value),
5859        }
5860    }
5861
5862    #[test]
5863    fn test_scalar_defaults_validate_only_first_default_choice() {
5864        let specs = [
5865            spec_with_arg(
5866                SpecArg::builder()
5867                    .name("env")
5868                    .var(false)
5869                    .default_values(["dev", "prod"])
5870                    .choices(["dev"])
5871                    .required(false)
5872                    .build(),
5873            ),
5874            spec_with_flag(
5875                SpecFlag::builder()
5876                    .long("env")
5877                    .arg(
5878                        SpecArg::builder()
5879                            .name("env")
5880                            .default_values(["dev", "prod"])
5881                            .choices(["dev"])
5882                            .build(),
5883                    )
5884                    .build(),
5885            ),
5886        ];
5887
5888        for spec in specs {
5889            let parsed = parse(&spec, &input(&["test"])).unwrap();
5890            assert_eq!(first_string_value(&parsed), "dev");
5891        }
5892    }
5893
5894    #[test]
5895    fn a_delimiter_turns_one_word_into_several_values() {
5896        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5897            .parse()
5898            .unwrap();
5899
5900        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5901        let multi = |value: &ParseValue| match value {
5902            ParseValue::MultiString(values) => values.clone(),
5903            other => panic!("expected several values, got {other:?}"),
5904        };
5905        let tags = parsed
5906            .flags
5907            .iter()
5908            .find(|(f, _)| f.name == "tags")
5909            .map(|(_, v)| v)
5910            .unwrap();
5911        assert_eq!(multi(tags), vec!["a", "b", "c"]);
5912        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5913    }
5914
5915    #[test]
5916    fn a_positional_splits_before_its_choices_are_asked() {
5917        // The flag path did this and the positional path did not, so a word whose parts
5918        // were all choices was rejected as one value, and a bad half was reported as the
5919        // whole word.
5920        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
5921            .parse()
5922            .unwrap();
5923
5924        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5925
5926        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5927        let message = err.to_string();
5928        assert!(message.contains("nowhere"), "{message}");
5929        assert!(
5930            !message.contains("src:nowhere"),
5931            "the bad half should be named, not the whole word: {message}"
5932        );
5933    }
5934
5935    #[test]
5936    fn a_split_value_is_counted_and_judged_as_values() {
5937        // Split during the parse rather than after it, so everything downstream sees the
5938        // values the user meant rather than the words they typed: `choices` judges each
5939        // one, and the bounds count them.
5940        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
5941            .parse()
5942            .unwrap();
5943
5944        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5945        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5946        assert!(err.to_string().contains("staging"), "{err}");
5947        assert!(
5948            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5949            "three values should breach var_max=2"
5950        );
5951    }
5952
5953    #[test]
5954    fn a_split_bound_counts_one_occurrence_at_a_time() {
5955        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
5956        // delimiter the collection simply stops at it, so it could never be exceeded; a word
5957        // carrying several values can carry an occurrence past it in one step, and that is
5958        // the only way this bound is ever breached.
5959        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
5960            .parse()
5961            .unwrap();
5962
5963        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5964        assert!(
5965            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5966            "three values out of one word is still three values"
5967        );
5968        // The rule the corpus documents for plain words, on split ones: a second occurrence
5969        // starts counting again rather than adding to the first.
5970        parse(
5971            &spec,
5972            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5973        )
5974        .expect("two per occurrence, twice, is within the bound");
5975    }
5976
5977    #[test]
5978    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5979        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5980            .parse()
5981            .unwrap();
5982
5983        parse(
5984            &spec,
5985            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5986        )
5987        .expect("each occurrence satisfies the bound independently");
5988
5989        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5990        assert!(
5991            error
5992                .to_string()
5993                .contains("requires at least 2 value(s), got 1"),
5994            "{error:?}"
5995        );
5996    }
5997
5998    #[test]
5999    fn an_exclusive_flag_has_to_be_alone() {
6000        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6001            .parse()
6002            .unwrap();
6003
6004        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6005
6006        // Any other flag.
6007        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6008        assert!(err.to_string().contains("on its own"), "{err}");
6009
6010        // And a positional, which is what makes this more than a conflict with every
6011        // other flag.
6012        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6013        assert!(err.to_string().contains("on its own"), "{err}");
6014
6015        // Not given, so it imposes nothing.
6016        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6017    }
6018
6019    #[test]
6020    fn an_exclusive_flag_conflicts_with_clause_arguments() {
6021        let spec: Spec = r#"name "ex"
6022bin "ex"
6023flag "--dump" exclusive=#true
6024clause "tasks" separator=":::" {
6025  arg "<task>"
6026}
6027"#
6028        .parse()
6029        .unwrap();
6030
6031        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6032        assert!(err.to_string().contains("on its own"), "{err}");
6033    }
6034
6035    #[test]
6036    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6037        // Only what was supplied counts, as `conflicts` reads it. A default counting as
6038        // company would make an exclusive flag unusable on any command that has one.
6039        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6040            .parse()
6041            .unwrap();
6042
6043        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6044        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6045    }
6046
6047    #[test]
6048    fn an_exclusive_flag_bypasses_required_siblings() {
6049        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6050            .parse()
6051            .unwrap();
6052
6053        parse(&spec, &input(&["ex", "--dump"]))
6054            .expect("exclusive is the command's requiredness escape");
6055        assert!(parse(
6056            &spec,
6057            &input(&["ex", "--dump", "--out", "somewhere", "target"])
6058        )
6059        .is_err());
6060    }
6061
6062    #[test]
6063    fn an_environment_value_counts_for_an_exclusive_flag() {
6064        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6065            .parse()
6066            .unwrap();
6067
6068        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6069        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6070    }
6071
6072    #[test]
6073    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6074        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6075            .parse()
6076            .unwrap();
6077
6078        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6079        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6080    }
6081
6082    #[test]
6083    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6084        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6085            .parse()
6086            .unwrap();
6087
6088        parse(&spec, &input(&["ex", "run", "--clean"]))
6089            .expect("the child flag is alone within the child command");
6090        assert!(
6091            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6092            "the parent flag still conflicts with selecting the child"
6093        );
6094    }
6095
6096    #[test]
6097    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6098        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6099            .parse()
6100            .unwrap();
6101
6102        parse(&spec, &input(&["ex", "run", "--clean"]))
6103            .expect("the child-local exclusive flag is alone inside the child command");
6104        assert!(
6105            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6106            "the ancestor spelling still conflicts with selecting the child"
6107        );
6108    }
6109
6110    #[test]
6111    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6112        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
6113            .parse()
6114            .unwrap();
6115
6116        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6117        assert!(
6118            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6119            "the distinct parent declaration is still company despite sharing a name"
6120        );
6121    }
6122
6123    #[test]
6124    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6125        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6126            .parse()
6127            .unwrap();
6128
6129        parse(&spec, &input(&["ex", "run", "--clean"]))
6130            .expect("the child exclusive flag is valid alone");
6131        assert!(
6132            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6133            "merging with the inherited global must not discard child exclusivity"
6134        );
6135    }
6136
6137    #[test]
6138    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6139        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6140            .parse()
6141            .unwrap();
6142
6143        parse(&spec, &input(&["ex", "run", "--clean"]))
6144            .expect("the typed long form belongs to the child declaration");
6145        assert!(
6146            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6147            "the inherited short form still belongs to the ancestor"
6148        );
6149        assert!(
6150            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6151            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6152        );
6153    }
6154
6155    #[test]
6156    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
6157        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
6158            .parse()
6159            .unwrap();
6160
6161        parse(&spec, &input(&["ex", "run", "--clean"]))
6162            .expect("the child's spelling does not activate the orphan ancestor alias");
6163        assert!(
6164            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6165            "the inherited short alias still belongs to the ancestor exclusive flag"
6166        );
6167    }
6168
6169    #[test]
6170    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
6171        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"
6172            .parse()
6173            .unwrap();
6174
6175        assert!(
6176            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
6177            "the inherited negated alias still belongs to the ancestor exclusive flag"
6178        );
6179    }
6180
6181    #[test]
6182    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
6183        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
6184        // an unrelated inherited global already owns. That collision is resolved in the other
6185        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
6186        // the `--clean` it declared, and its exclusivity holds.
6187        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"
6188            .parse()
6189            .unwrap();
6190
6191        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
6192        assert!(
6193            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6194            "one unrelated alias collision cannot disown the child from its own flag"
6195        );
6196    }
6197
6198    #[test]
6199    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
6200        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
6201        // subcommand word the flag can only be the ancestor's, because that is the only one in
6202        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
6203        // is set. The pair below differ in nothing else, which is what makes this one rule
6204        // rather than two behaviors.
6205        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6206            .parse()
6207            .unwrap();
6208        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
6209            .expect("the ancestor owns this occurrence, and it is not exclusive");
6210        assert!(
6211            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6212            "after the subcommand word the child's declaration is in scope, and it is exclusive"
6213        );
6214
6215        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6216            .parse()
6217            .unwrap();
6218        assert!(
6219            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
6220            "the same rule, with an exclusive ancestor: selecting the child is company for it"
6221        );
6222    }
6223
6224    #[test]
6225    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
6226        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
6227        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
6228        // the ancestor ever declared, so the ancestor's answer still governs it.
6229        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
6230            .parse()
6231            .unwrap();
6232
6233        assert!(
6234            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6235            "the orphan ancestor alias is still the ancestor's exclusive flag"
6236        );
6237        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
6238            .expect("the child's own spelling drops the exclusivity the child did not restate");
6239    }
6240
6241    #[test]
6242    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
6243        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
6244        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
6245        // occurrence to the ancestor because `-c` appeared in it lost that.
6246        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6247            .parse()
6248            .unwrap();
6249
6250        assert!(
6251            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
6252            "the child spelling is exclusive whatever it was typed beside"
6253        );
6254        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
6255            .expect("the ancestor's own spelling was never exclusive");
6256    }
6257
6258    #[test]
6259    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
6260        // An environment value has no spelling to attribute, so the declaration the selected
6261        // command has in scope answers — in both directions. Comparing whole alias sets asked
6262        // the ancestor instead, because the merged flag also carries its orphan `-c`.
6263        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"
6264            .parse()
6265            .unwrap();
6266
6267        assert!(
6268            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
6269            "the child added exclusivity the environment value has to honor"
6270        );
6271
6272        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"
6273            .parse()
6274            .unwrap();
6275
6276        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
6277            .expect("the child dropped the exclusivity, and the environment value follows it");
6278    }
6279
6280    #[test]
6281    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
6282        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
6283        // the same reason the companion check does.
6284        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"
6285            .parse()
6286            .unwrap();
6287
6288        parse(&spec, &input(&["ex", "run", "--clean"]))
6289            .expect("a merged child exclusive flag is still the command's requiredness escape");
6290    }
6291
6292    #[test]
6293    fn a_group_allows_one_member_and_refuses_two() {
6294        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
6295            .parse()
6296            .unwrap();
6297
6298        // One is fine, and so is none: a plain group says "at most one".
6299        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
6300        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
6301
6302        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
6303        assert!(err.to_string().contains("group input"), "{err}");
6304    }
6305
6306    #[test]
6307    fn positional_selectors_work_in_conflicts_and_groups() {
6308        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
6309            .parse()
6310            .unwrap();
6311        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
6312            .expect("the flag alone is valid");
6313        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
6314        assert!(parse(
6315            &conflicts,
6316            &input(&["ex", "--from-file", "vars.env", "literal"])
6317        )
6318        .is_err());
6319
6320        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
6321            .parse()
6322            .unwrap();
6323        assert!(parse(
6324            &positional_source,
6325            &input(&["ex", "--from-file", "vars.env", "literal"])
6326        )
6327        .is_err());
6328
6329        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
6330            .parse()
6331            .unwrap();
6332        assert!(parse(&group, &input(&["ex"])).is_err());
6333        parse(&group, &input(&["ex", "target-name"]))
6334            .expect("a positional satisfies a required group");
6335        assert!(parse(
6336            &group,
6337            &input(&["ex", "--file", "input.txt", "target-name"])
6338        )
6339        .is_err());
6340    }
6341
6342    #[test]
6343    fn a_required_group_needs_one_of_its_members() {
6344        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6345            .parse()
6346            .unwrap();
6347
6348        let err = parse(&spec, &input(&["ex"])).unwrap_err();
6349        // The members, because that is what a user has to type; the name, because a
6350        // command with several groups would otherwise report the same sentence twice.
6351        assert!(err.to_string().contains("--file, --url"), "{err}");
6352        assert!(err.to_string().contains("input"), "{err}");
6353
6354        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
6355    }
6356
6357    #[test]
6358    fn a_multiple_group_only_polices_requiredness() {
6359        // `multiple` with `required` is "at least one of these", so two is fine and
6360        // none is not.
6361        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
6362            .parse()
6363            .unwrap();
6364
6365        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
6366        assert!(parse(&spec, &input(&["ex"])).is_err());
6367    }
6368
6369    #[test]
6370    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
6371        // The two halves of a group are two kinds of rule, and they read a default
6372        // differently on purpose. Requiredness asks whether a member has a value, and a
6373        // default is a value — the rule `requires` follows. Exclusivity asks what the
6374        // user supplied, because a defaulted member counted as supplied would collide
6375        // with the sibling they actually typed and refuse a correct command line.
6376        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6377            .parse()
6378            .unwrap();
6379
6380        parse(&spec, &input(&["ex"])).expect("the default fills the group");
6381        parse(&spec, &input(&["ex", "--url", "u"]))
6382            .expect("the default must not conflict with the flag the user typed");
6383    }
6384
6385    #[test]
6386    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
6387        // `-f` and `--file` are one flag. Counted by selector, giving it once would
6388        // report it as conflicting with itself.
6389        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
6390            .parse()
6391            .unwrap();
6392
6393        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
6394        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
6395
6396        // A genuine collision is still one.
6397        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
6398        assert!(err.to_string().contains("group input"), "{err}");
6399    }
6400
6401    #[test]
6402    fn a_group_reads_the_environment_as_given() {
6403        // The environment does count, which is the same asymmetry `conflicts` has: an
6404        // env var is somebody saying something, a default is nobody saying anything.
6405        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6406            .parse()
6407            .unwrap();
6408
6409        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
6410    }
6411
6412    #[test]
6413    fn a_requirement_names_the_flag_that_is_missing() {
6414        // Reported as the missing flag rather than as something wrong with `--out`,
6415        // which is what clap says for an unmet `requires` and what a user can act on.
6416        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6417            .parse()
6418            .unwrap();
6419
6420        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6421        assert!(
6422            err.to_string().contains("format"),
6423            "the missing flag should be named: {err}"
6424        );
6425
6426        // Satisfied, in either order.
6427        for words in [
6428            &["ex", "--out", "a.txt", "--format", "json"][..],
6429            &["ex", "--format", "json", "--out", "a.txt"][..],
6430        ] {
6431            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6432        }
6433
6434        // Nothing happens when the flag that imposes the rule is absent: a requirement
6435        // is a consequence of using the flag, not a rule about the command line.
6436        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6437    }
6438
6439    #[test]
6440    fn a_value_activates_only_its_conditional_requirement() {
6441        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"
6442            .parse()
6443            .unwrap();
6444
6445        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6446            .expect("an unrelated value requires nothing");
6447
6448        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6449        assert!(key.to_string().contains("key"), "{key}");
6450        parse(
6451            &spec,
6452            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6453        )
6454        .expect("the matching requirement is satisfied");
6455
6456        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6457        assert!(token.to_string().contains("token"), "{token}");
6458    }
6459
6460    #[test]
6461    fn conditional_requirements_read_explicit_env_but_not_defaults() {
6462        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"
6463            .parse()
6464            .unwrap();
6465        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6466        assert!(err.to_string().contains("key"), "{err}");
6467
6468        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"
6469            .parse()
6470            .unwrap();
6471        parse(&from_default, &input(&["ex"]))
6472            .expect("a default is not an explicit conditional value");
6473    }
6474
6475    #[test]
6476    fn command_line_values_override_env_for_conditional_requirements() {
6477        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6478            .parse()
6479            .unwrap();
6480
6481        parse_with_env(
6482            &spec,
6483            &["ex", "--config", "ordinary.toml"],
6484            &[("EX_CONFIG", "special.toml")],
6485        )
6486        .expect("the command-line value takes precedence over the environment");
6487    }
6488
6489    #[test]
6490    fn conditional_requirements_normalize_boolean_env_values() {
6491        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6492            .parse()
6493            .unwrap();
6494
6495        for value in ["1", "true", "True", "TRUE"] {
6496            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6497            assert!(err.to_string().contains("key"), "{value}: {err}");
6498        }
6499        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6500            .expect("a false environment value does not activate a true condition");
6501    }
6502
6503    #[test]
6504    fn a_default_satisfies_a_requirement() {
6505        // The flag it names has a value, which is the question a requirement asks. Read
6506        // any other way, `--format` would be missing here and present ten lines further
6507        // down, where plain required-ness reads the same default as filling it.
6508        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6509            .parse()
6510            .unwrap();
6511
6512        parse(&spec, &input(&["ex", "--out", "a.txt"]))
6513            .expect("a defaulted flag is not a missing one");
6514    }
6515
6516    #[test]
6517    fn a_present_flag_binds_a_conditional_default() {
6518        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6519            .parse()
6520            .unwrap();
6521
6522        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6523        assert_eq!(
6524            with.as_env().get("usage_bin_names").map(String::as_str),
6525            Some("true")
6526        );
6527
6528        let without = parse(&spec, &input(&["ex"])).unwrap();
6529        assert!(
6530            !without.as_env().contains_key("usage_bin_names"),
6531            "IsPresent does nothing when the selector is absent"
6532        );
6533    }
6534
6535    #[test]
6536    fn an_equals_condition_binds_a_conditional_default() {
6537        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6538            .parse()
6539            .unwrap();
6540
6541        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6542        assert_eq!(
6543            json.as_env().get("usage_style").map(String::as_str),
6544            Some("pretty")
6545        );
6546        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6547        assert!(!yaml.as_env().contains_key("usage_style"));
6548    }
6549
6550    #[test]
6551    fn an_equals_condition_reads_a_negated_flag() {
6552        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6553            .parse()
6554            .unwrap();
6555
6556        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6557        assert_eq!(
6558            off.as_env().get("usage_pretty").map(String::as_str),
6559            Some("true")
6560        );
6561        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6562        assert!(
6563            !on.as_env().contains_key("usage_pretty"),
6564            "--json is true, so when=false should miss"
6565        );
6566    }
6567
6568    #[test]
6569    fn the_first_matching_conditional_default_wins() {
6570        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"
6571            .parse()
6572            .unwrap();
6573
6574        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6575        assert_eq!(
6576            out.as_env().get("usage_style").map(String::as_str),
6577            Some("compact")
6578        );
6579    }
6580
6581    #[test]
6582    fn argv_and_env_suppress_a_conditional_default() {
6583        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6584            .parse()
6585            .unwrap();
6586
6587        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6588        assert_eq!(
6589            from_env.as_env().get("usage_bin_names").map(String::as_str),
6590            Some("false"),
6591            "the target's environment wins over default_if"
6592        );
6593    }
6594
6595    #[test]
6596    fn a_sibling_env_activates_a_conditional_default() {
6597        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6598            .parse()
6599            .unwrap();
6600
6601        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6602        assert_eq!(
6603            out.as_env().get("usage_bin_names").map(String::as_str),
6604            Some("true")
6605        );
6606    }
6607
6608    #[test]
6609    fn a_default_does_not_activate_a_conditional_default() {
6610        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
6611        // bind would put json's default into `out.flags` and then treat it as
6612        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
6613        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6614            .parse()
6615            .unwrap();
6616
6617        let out = parse(&spec, &input(&["ex"])).unwrap();
6618        assert_eq!(
6619            out.as_env().get("usage_json").map(String::as_str),
6620            Some("true")
6621        );
6622        assert!(
6623            !out.as_env().contains_key("usage_pretty"),
6624            "a default is not an explicit value for default_if"
6625        );
6626    }
6627
6628    #[test]
6629    fn a_conditional_default_does_not_activate_requires_if() {
6630        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"
6631            .parse()
6632            .unwrap();
6633
6634        parse(&spec, &input(&["ex", "--json"]))
6635            .expect("a default_if value is not explicit for requires_if");
6636        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6637    }
6638
6639    #[test]
6640    fn a_conditional_default_satisfies_a_requirement() {
6641        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6642            .parse()
6643            .unwrap();
6644
6645        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6646            .expect("default_if fills the required flag");
6647        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6648    }
6649
6650    #[test]
6651    fn an_environment_value_satisfies_a_requirement() {
6652        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6653            .parse()
6654            .unwrap();
6655
6656        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6657        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6658            .expect("the environment supplies it");
6659    }
6660
6661    #[test]
6662    fn a_requirement_is_satisfied_by_a_short_form() {
6663        // The selector may spell the other flag any way it answers to, so the check
6664        // resolves it the way every other selector is resolved rather than matching
6665        // text. The error names the flag, not the selector.
6666        let spec: Spec =
6667            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6668                .parse()
6669                .unwrap();
6670
6671        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6672
6673        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6674        assert!(err.to_string().contains("key"), "{err}");
6675    }
6676
6677    #[test]
6678    fn conflicting_flags_are_rejected_in_either_order() {
6679        // Declared once, on `--file`, which is all clap exposes — so the check has to
6680        // be order-independent by looking at every flag that was given rather than at
6681        // the one that declared the conflict.
6682        let spec: Spec =
6683            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6684                .parse()
6685                .unwrap();
6686
6687        for words in [
6688            &["ex", "--file", "a.txt", "--stdin"][..],
6689            &["ex", "--stdin", "--file", "a.txt"][..],
6690        ] {
6691            let err = parse(&spec, &input(words)).unwrap_err();
6692            assert!(
6693                err.to_string().contains("conflicts with --stdin"),
6694                "{words:?} should be refused: {err}"
6695            );
6696        }
6697
6698        // Either one alone is fine.
6699        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6700        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6701    }
6702
6703    #[test]
6704    fn unknown_flags_are_values_by_default() {
6705        // The default, and the reason it is the default: a spec often parses a
6706        // command line whose flags belong to something else.
6707        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6708            .parse()
6709            .unwrap();
6710        let out = parse(
6711            &spec,
6712            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6713        )
6714        .unwrap();
6715        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6716        assert_eq!(out.args[rest].to_string(), "--wat x");
6717    }
6718
6719    #[test]
6720    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6721        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6722            .parse()
6723            .unwrap();
6724        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6725            .expect("a repeat is a correction by default");
6726        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6727        assert_eq!(out.flags[jobs].to_string(), "2");
6728        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6729            .expect("switches use the same default");
6730
6731        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6732            .parse()
6733            .unwrap();
6734        for words in [
6735            &["ex", "--jobs", "1", "--jobs", "2"][..],
6736            &["ex", "--verbose", "--verbose"][..],
6737        ] {
6738            let err = parse(&strict, &input(words)).unwrap_err();
6739            assert!(
6740                err.to_string().contains("cannot be used multiple times"),
6741                "{err}"
6742            );
6743        }
6744
6745        let reparsed: Spec = strict.to_string().parse().unwrap();
6746        assert!(!reparsed.cmd.args_override_self);
6747    }
6748
6749    #[test]
6750    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6751        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6752            .parse()
6753            .unwrap();
6754
6755        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6756            .expect("opposite forms override each other");
6757        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6758        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6759
6760        for words in [
6761            &["ex", "--color", "--color"][..],
6762            &["ex", "--no-color", "--no-color"][..],
6763        ] {
6764            let err = parse(&spec, &input(words)).unwrap_err();
6765            assert!(err.to_string().contains("cannot be used multiple times"));
6766        }
6767    }
6768
6769    #[test]
6770    fn strict_global_flags_may_repeat_across_command_levels() {
6771        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"
6772            .parse()
6773            .unwrap();
6774
6775        let out = parse(
6776            &spec,
6777            &input(&[
6778                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6779            ]),
6780        )
6781        .expect("an inherited global is allowed once at each command level");
6782        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6783        assert_eq!(out.flags[jobs].to_string(), "2");
6784
6785        for words in [
6786            &["ex", "--color", "--color", "run", "--no-color"][..],
6787            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6788        ] {
6789            let err = parse(&spec, &input(words)).unwrap_err();
6790            assert!(err.to_string().contains("cannot be used multiple times"));
6791        }
6792    }
6793
6794    #[test]
6795    fn a_subcommand_can_negate_only_its_parents_requirements() {
6796        let base = r#"name "ex"
6797bin "ex"
6798flag "--config" required=#true
6799flag "--mode" requires="--config"
6800flag "--other"
6801arg "<input>"
6802group "source" "--config" "--other" required=#true
6803cmd "run" { flag "--child" required=#true }
6804"#;
6805        let strict: Spec = base.parse().unwrap();
6806        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6807        let message = err.to_string();
6808        assert!(
6809            message.contains("input") || message.contains("config"),
6810            "{message}"
6811        );
6812
6813        let negated: Spec = base
6814            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6815            .parse()
6816            .unwrap();
6817        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6818        assert!(
6819            err.to_string().contains("child"),
6820            "the selected command keeps its own requirements: {err}"
6821        );
6822
6823        let mut child_optional = negated.clone();
6824        child_optional.cmd.subcommands["run"].flags[0].required = false;
6825        parse(&child_optional, &input(&["ex", "run"]))
6826            .expect("the child selection satisfies all parent requirements");
6827        parse(&child_optional, &input(&["ex", "--mode", "run"]))
6828            .expect("parent requires relationships are negated too");
6829    }
6830
6831    #[test]
6832    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6833        let spec: Spec = r#"name "ex"
6834bin "ex"
6835args_conflicts_with_subcommands #true
6836flag "--verbose"
6837cmd "run"
6838"#
6839        .parse()
6840        .unwrap();
6841
6842        parse(&spec, &input(&["ex", "run"]))
6843            .expect("the subcommand is valid without a parent argument");
6844        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6845        assert!(
6846            err.to_string().contains("cannot be used with arguments"),
6847            "{err}"
6848        );
6849    }
6850
6851    #[test]
6852    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6853        let base = r#"name "ex"
6854bin "ex"
6855flag "--values <value>..."
6856cmd "run"
6857"#;
6858        let plain: Spec = base.parse().unwrap();
6859        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6860        assert_eq!(out.cmd.name, "ex");
6861
6862        let precedence: Spec = base
6863            .replacen(
6864                "bin \"ex\"",
6865                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6866                1,
6867            )
6868            .parse()
6869            .unwrap();
6870        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6871        assert_eq!(out.cmd.name, "run");
6872    }
6873
6874    #[test]
6875    fn a_required_positional_can_follow_an_unfilled_optional_one() {
6876        let base = r#"name "ex"
6877bin "ex"
6878arg "[optional]"
6879arg "<required>"
6880"#;
6881        let plain: Spec = base.parse().unwrap();
6882        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6883        assert!(err.to_string().contains("required"), "{err}");
6884
6885        let enabled: Spec = base
6886            .replacen(
6887                "bin \"ex\"",
6888                "bin \"ex\"\nallow_missing_positional #true",
6889                1,
6890            )
6891            .parse()
6892            .unwrap();
6893        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6894        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6895        let value = &out
6896            .args
6897            .iter()
6898            .find(|(arg, _)| arg.name == "required")
6899            .unwrap()
6900            .1;
6901        assert!(matches!(value, ParseValue::String(value) if value == "value"));
6902    }
6903
6904    #[test]
6905    fn sigil_args_do_not_block_optional_positional_skipping() {
6906        let spec: Spec = r#"name "ex"
6907bin "ex"
6908allow_missing_positional #true
6909arg "[optional]"
6910arg "[tool]" sigil="@"
6911arg "<required>"
6912"#
6913        .parse()
6914        .unwrap();
6915
6916        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
6917        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6918        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
6919        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
6920        assert_eq!(out.args[tool].to_string(), "node");
6921        assert_eq!(out.args[required].to_string(), "value");
6922    }
6923
6924    #[test]
6925    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6926        let spec: Spec =
6927            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6928                .parse()
6929                .unwrap();
6930        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6931        assert!(
6932            err.to_string().contains("--wat"),
6933            "the message should name the token: {err}"
6934        );
6935
6936        // The positional opts into the narrower negative-number carve-out without
6937        // accepting arbitrary unknown flags.
6938        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6939        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6940        assert_eq!(out.args[rest].to_string(), "-1");
6941
6942        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6943        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6944        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6945    }
6946
6947    #[test]
6948    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6949        let spec: Spec = r#"
6950name "ex"
6951bin "ex"
6952unknown_flags "error"
6953flag "-0 --print0" global=#true
6954cmd "run" {
6955  flag "--force"
6956}
6957"#
6958        .parse()
6959        .unwrap();
6960        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6961        assert_eq!(out.cmd.name, "run");
6962        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6963        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6964        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6965        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6966    }
6967
6968    #[test]
6969    fn a_command_may_override_the_cli_wide_setting() {
6970        // Strict overall, lenient for the one command that forwards options.
6971        let spec: Spec = r#"
6972name "ex"
6973bin "ex"
6974unknown_flags "error"
6975cmd "exec" unknown_flags="value" {
6976  arg "[rest]..."
6977}
6978cmd "build" {
6979  arg "[rest]..."
6980}
6981"#
6982        .parse()
6983        .unwrap();
6984
6985        let out = parse(
6986            &spec,
6987            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6988        )
6989        .unwrap();
6990        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6991        assert_eq!(out.args[rest].to_string(), "--wat");
6992
6993        assert!(
6994            parse(
6995                &spec,
6996                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6997            )
6998            .is_err(),
6999            "a command that says nothing inherits the CLI's choice"
7000        );
7001    }
7002
7003    #[test]
7004    fn the_setting_survives_a_round_trip() {
7005        let spec: Spec =
7006            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7007                .parse()
7008                .unwrap();
7009        let reparsed: Spec = spec.to_string().parse().unwrap();
7010        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7011        assert_eq!(
7012            reparsed.cmd.subcommands["x"].unknown_flags,
7013            Some(UnknownFlags::Value)
7014        );
7015    }
7016
7017    #[test]
7018    fn test_default_subcommand() {
7019        // Test that default_subcommand routes to the specified subcommand
7020        let run_cmd = SpecCommand::builder()
7021            .name("run")
7022            .arg(SpecArg::builder().name("task").build())
7023            .build();
7024        let mut cmd = SpecCommand::builder().name("test").build();
7025        cmd.subcommands.insert("run".to_string(), run_cmd);
7026
7027        let spec = Spec {
7028            name: "test".to_string(),
7029            bin: "test".to_string(),
7030            cmd,
7031            default_subcommand: Some("run".to_string()),
7032            ..Default::default()
7033        };
7034
7035        // "test mytask" should be parsed as if it were "test run mytask"
7036        let input = vec!["test".to_string(), "mytask".to_string()];
7037        let parsed = parse(&spec, &input).unwrap();
7038
7039        // Should have two commands: root and "run"
7040        assert_eq!(parsed.cmds.len(), 2);
7041        assert_eq!(parsed.cmds[1].name, "run");
7042
7043        // Should have parsed the task argument
7044        assert_eq!(parsed.args.len(), 1);
7045        let arg = parsed.args.keys().next().unwrap();
7046        assert_eq!(arg.name, "task");
7047        let value = parsed.args.values().next().unwrap();
7048        assert_eq!(value.to_string(), "mytask");
7049    }
7050
7051    #[test]
7052    fn default_subcommand_outranks_root_sigil_arg() {
7053        let spec: Spec = r#"
7054name "test"
7055bin "test"
7056default_subcommand "run"
7057arg "[tools]..." sigil="+"
7058cmd "run" { arg "<task>" }
7059"#
7060        .parse()
7061        .unwrap();
7062
7063        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7064        assert_eq!(parsed.cmd.name, "run");
7065        let value = |name| {
7066            parsed
7067                .args
7068                .iter()
7069                .find(|(arg, _)| arg.name == name)
7070                .map(|(_, value)| value.to_string())
7071                .unwrap()
7072        };
7073        assert_eq!(value("tools"), "node");
7074        assert_eq!(value("task"), "node");
7075    }
7076
7077    #[test]
7078    fn test_default_subcommand_explicit_still_works() {
7079        // Test that explicit subcommand takes precedence
7080        let run_cmd = SpecCommand::builder()
7081            .name("run")
7082            .arg(SpecArg::builder().name("task").build())
7083            .build();
7084        let other_cmd = SpecCommand::builder()
7085            .name("other")
7086            .arg(SpecArg::builder().name("other_arg").build())
7087            .build();
7088        let mut cmd = SpecCommand::builder().name("test").build();
7089        cmd.subcommands.insert("run".to_string(), run_cmd);
7090        cmd.subcommands.insert("other".to_string(), other_cmd);
7091
7092        let spec = Spec {
7093            name: "test".to_string(),
7094            bin: "test".to_string(),
7095            cmd,
7096            default_subcommand: Some("run".to_string()),
7097            ..Default::default()
7098        };
7099
7100        // "test other foo" should use "other" subcommand, not default
7101        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7102        let parsed = parse(&spec, &input).unwrap();
7103
7104        // Should have used "other" subcommand
7105        assert_eq!(parsed.cmds.len(), 2);
7106        assert_eq!(parsed.cmds[1].name, "other");
7107    }
7108
7109    #[test]
7110    fn test_default_subcommand_applies_only_at_the_root() {
7111        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
7112        // was being looked up wherever the parser happened to be standing, so a command with
7113        // an unrelated subcommand of the same name acquired a default of its own: with
7114        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
7115        // `zzz` there. Nothing declared that, and nothing could have.
7116        let mut config_ls = SpecCommand::builder().name("ls").build();
7117        config_ls.args.push(SpecArg::builder().name("what").build());
7118        let mut config_cmd = SpecCommand::builder().name("config").build();
7119        config_cmd.subcommands.insert("ls".to_string(), config_ls);
7120
7121        // The root's own `ls`, which is what its default points at. It takes an argument so
7122        // that a routed word has somewhere to land.
7123        let mut root_ls = SpecCommand::builder().name("ls").build();
7124        root_ls.args.push(SpecArg::builder().name("what").build());
7125        let mut cmd = SpecCommand::builder().name("ex").build();
7126        cmd.subcommands.insert("ls".to_string(), root_ls);
7127        cmd.subcommands.insert("config".to_string(), config_cmd);
7128
7129        let spec = Spec {
7130            name: "ex".to_string(),
7131            bin: "ex".to_string(),
7132            cmd,
7133            default_subcommand: Some("ls".to_string()),
7134            ..Default::default()
7135        };
7136
7137        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
7138        // own business — and `config` takes no argument, so this is an error rather than a
7139        // silent descent.
7140        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7141        assert!(
7142            parse(&spec, &input).is_err(),
7143            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7144        );
7145
7146        // At the root, where it is declared, it still applies.
7147        let input = vec!["ex".to_string(), "zzz".to_string()];
7148        let parsed = parse(&spec, &input).expect("the root's default applies");
7149        assert_eq!(
7150            parsed
7151                .cmds
7152                .iter()
7153                .map(|c| c.name.as_str())
7154                .collect::<Vec<_>>(),
7155            ["ex", "ls"]
7156        );
7157        assert_eq!(
7158            parsed.args.values().next().map(|v| v.to_string()),
7159            Some("zzz".to_string()),
7160            "and the word binds inside the command it reached"
7161        );
7162    }
7163
7164    #[test]
7165    fn test_default_subcommand_with_nested_subcommands() {
7166        // Test that default_subcommand works when the default subcommand has nested subcommands.
7167        // This is the mise use case: "mise say" should be parsed as "mise run say"
7168        // where "say" is a subcommand of "run" (a task).
7169        let say_cmd = SpecCommand::builder()
7170            .name("say")
7171            .arg(SpecArg::builder().name("name").build())
7172            .build();
7173        let mut run_cmd = SpecCommand::builder().name("run").build();
7174        run_cmd.subcommands.insert("say".to_string(), say_cmd);
7175
7176        let mut cmd = SpecCommand::builder().name("test").build();
7177        cmd.subcommands.insert("run".to_string(), run_cmd);
7178
7179        let spec = Spec {
7180            name: "test".to_string(),
7181            bin: "test".to_string(),
7182            cmd,
7183            default_subcommand: Some("run".to_string()),
7184            ..Default::default()
7185        };
7186
7187        // "test say hello" should be parsed as "test run say hello"
7188        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
7189        let parsed = parse(&spec, &input).unwrap();
7190
7191        // Should have three commands: root, "run", and "say"
7192        assert_eq!(parsed.cmds.len(), 3);
7193        assert_eq!(parsed.cmds[0].name, "test");
7194        assert_eq!(parsed.cmds[1].name, "run");
7195        assert_eq!(parsed.cmds[2].name, "say");
7196
7197        // Should have parsed the "name" argument
7198        assert_eq!(parsed.args.len(), 1);
7199        let arg = parsed.args.keys().next().unwrap();
7200        assert_eq!(arg.name, "name");
7201        let value = parsed.args.values().next().unwrap();
7202        assert_eq!(value.to_string(), "hello");
7203    }
7204
7205    /// Build a spec equivalent to the post-mount structure produced by mise's
7206    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
7207    /// subcommand that re-declares the same flag as NON-global, and a mounted task
7208    /// (`sample:run`) carrying a positional arg with `choices`.
7209    ///
7210    /// We construct the merged structure directly instead of executing a real mount so the
7211    /// test stays hermetic and cross-platform while still exercising the parser defect.
7212    fn mounted_global_flag_spec() -> Spec {
7213        let task_cmd = SpecCommand::builder()
7214            .name("sample:run")
7215            .arg(
7216                SpecArg::builder()
7217                    .name("profile")
7218                    .choices(["alpha", "beta", "gamma"])
7219                    .build(),
7220            )
7221            .build();
7222        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
7223        let mut run_cmd = SpecCommand::builder()
7224            .name("run")
7225            .flag(
7226                SpecFlag::builder()
7227                    .name("cd")
7228                    .short('C')
7229                    .long("cd")
7230                    .arg(SpecArg::builder().name("dir").build())
7231                    .global(false)
7232                    .build(),
7233            )
7234            .build();
7235        run_cmd
7236            .subcommands
7237            .insert("sample:run".to_string(), task_cmd);
7238
7239        let mut cmd = SpecCommand::builder()
7240            .name("test")
7241            .flag(
7242                SpecFlag::builder()
7243                    .name("cd")
7244                    .short('C')
7245                    .long("cd")
7246                    .arg(SpecArg::builder().name("dir").build())
7247                    .global(true)
7248                    .build(),
7249            )
7250            .build();
7251        cmd.subcommands.insert("run".to_string(), run_cmd);
7252
7253        Spec {
7254            name: "test".to_string(),
7255            bin: "test".to_string(),
7256            cmd,
7257            ..Default::default()
7258        }
7259    }
7260
7261    #[test]
7262    fn test_prefix_global_flag_does_not_pollute_choices() {
7263        // Regression for the parser-side root cause referenced by jdx/mise#10069.
7264        //
7265        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
7266        // then into the mounted `sample:run`) used to drop the inherited global flag from
7267        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
7268        // mis-validated against the task's `choices` positional arg.
7269        let spec = mounted_global_flag_spec();
7270
7271        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
7272        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
7273        for words in [
7274            &["test", "-C", "/tmp", "run", "sample:run"][..],
7275            // Embedded-value form must behave identically.
7276            &["test", "--cd=/tmp", "run", "sample:run"][..],
7277        ] {
7278            let parsed = parse_partial(&spec, &input(words)).unwrap();
7279            assert_eq!(
7280                parsed
7281                    .cmds
7282                    .iter()
7283                    .map(|c| c.name.as_str())
7284                    .collect::<Vec<_>>(),
7285                vec!["test", "run", "sample:run"],
7286            );
7287            // No positional arg should have been consumed by the leftover global-flag tokens.
7288            assert!(
7289                parsed.args.is_empty(),
7290                "args should be empty, got {:?}",
7291                parsed.args
7292            );
7293
7294            // Fix (B): the inherited global flag survives the descent even though `run`
7295            // re-declares `-C/--cd` as non-global.
7296            let cd = parsed
7297                .available_flags
7298                .get("--cd")
7299                .expect("--cd should remain available after descending into the subcommand");
7300            assert!(cd.global, "--cd must stay global after descent");
7301            assert!(
7302                parsed.available_flags.get("-C").is_some_and(|f| f.global),
7303                "-C must stay global after descent",
7304            );
7305
7306            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
7307            // for normal execution and for the env passed to mount scripts. (Removing the
7308            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
7309            assert_eq!(
7310                parsed.as_env().get("usage_cd").map(String::as_str),
7311                Some("/tmp"),
7312                "global flag value must survive in as_env(), got {:?}",
7313                parsed.as_env(),
7314            );
7315        }
7316
7317        // A real, valid choice still parses through the global flag prefix.
7318        let parsed = parse_partial(
7319            &spec,
7320            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
7321        )
7322        .unwrap();
7323        assert_eq!(parsed.args.len(), 1);
7324        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7325
7326        // And genuinely invalid choices are still rejected (we didn't disable validation).
7327        assert_parse_err(
7328            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
7329            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7330        );
7331    }
7332
7333    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
7334    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
7335    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
7336    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
7337    fn mounted_orphan_short_spec() -> Spec {
7338        let task_cmd = SpecCommand::builder()
7339            .name("sample:run")
7340            .arg(
7341                SpecArg::builder()
7342                    .name("profile")
7343                    .choices(["alpha", "beta", "gamma"])
7344                    .build(),
7345            )
7346            .build();
7347        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
7348        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
7349        let mut run_cmd = SpecCommand::builder()
7350            .name("run")
7351            .flag(
7352                SpecFlag::builder()
7353                    .name("raw")
7354                    .short('r')
7355                    .long("raw")
7356                    .global(false)
7357                    .build(),
7358            )
7359            .flag(
7360                SpecFlag::builder()
7361                    .name("force")
7362                    .short('f')
7363                    .long("force")
7364                    .global(false)
7365                    .build(),
7366            )
7367            .build();
7368        run_cmd
7369            .subcommands
7370            .insert("sample:run".to_string(), task_cmd);
7371
7372        // Root global is LONG-ONLY: `--raw` with no short.
7373        let mut cmd = SpecCommand::builder()
7374            .name("test")
7375            .flag(
7376                SpecFlag::builder()
7377                    .name("raw")
7378                    .long("raw")
7379                    .global(true)
7380                    .build(),
7381            )
7382            .build();
7383        cmd.subcommands.insert("run".to_string(), run_cmd);
7384
7385        Spec {
7386            name: "test".to_string(),
7387            bin: "test".to_string(),
7388            cmd,
7389            ..Default::default()
7390        }
7391    }
7392
7393    #[test]
7394    fn test_orphan_short_alias_survives_merge() {
7395        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
7396        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
7397        // added short `-r` must be unioned onto the surviving inherited global flag instead of
7398        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
7399        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
7400        let spec = mounted_orphan_short_spec();
7401
7402        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
7403        assert_eq!(
7404            parsed
7405                .cmds
7406                .iter()
7407                .map(|c| c.name.as_str())
7408                .collect::<Vec<_>>(),
7409            vec!["test", "run", "sample:run"],
7410        );
7411
7412        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
7413        // and the original long `--raw` is still global too.
7414        assert!(
7415            parsed.available_flags.get("-r").is_some_and(|f| f.global),
7416            "-r must be merged onto the inherited global flag and stay global after descent",
7417        );
7418        assert!(
7419            parsed
7420                .available_flags
7421                .get("--raw")
7422                .is_some_and(|f| f.global),
7423            "--raw must stay global after descent",
7424        );
7425
7426        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
7427        assert!(
7428            parsed.args.is_empty(),
7429            "args should be empty, got {:?}",
7430            parsed.args
7431        );
7432
7433        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
7434        assert_eq!(
7435            parsed.as_env().get("usage_raw").map(String::as_str),
7436            Some("true"),
7437            "merged short's value must survive in as_env(), got {:?}",
7438            parsed.as_env(),
7439        );
7440
7441        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
7442        // promoted/merged — it is correctly dropped when descending into the mount.
7443        assert!(
7444            !parsed.available_flags.contains_key("-f"),
7445            "purely-local -f must not be promoted onto a global",
7446        );
7447        assert!(
7448            !parsed.available_flags.contains_key("--force"),
7449            "purely-local --force must not be promoted onto a global",
7450        );
7451
7452        // A real, valid choice still parses through the merged short prefix.
7453        let parsed =
7454            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
7455        assert_eq!(parsed.args.len(), 1);
7456        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7457
7458        // And genuinely invalid choices are still rejected.
7459        assert_parse_err(
7460            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
7461            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7462        );
7463    }
7464
7465    #[test]
7466    fn test_orphan_short_does_not_clobber_unrelated_global() {
7467        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
7468        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
7469        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
7470        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
7471        let run_cmd = SpecCommand::builder()
7472            .name("run")
7473            .flag(
7474                SpecFlag::builder()
7475                    .name("raw")
7476                    .short('r')
7477                    .long("raw")
7478                    .global(false)
7479                    .build(),
7480            )
7481            .build();
7482        let mut cmd = SpecCommand::builder()
7483            .name("test")
7484            .flag(
7485                SpecFlag::builder()
7486                    .name("raw")
7487                    .long("raw")
7488                    .global(true)
7489                    .build(),
7490            )
7491            .flag(
7492                SpecFlag::builder()
7493                    .name("restrict")
7494                    .short('r')
7495                    .long("restrict")
7496                    .global(true)
7497                    .build(),
7498            )
7499            .build();
7500        cmd.subcommands.insert("run".to_string(), run_cmd);
7501        let spec = Spec {
7502            name: "test".to_string(),
7503            bin: "test".to_string(),
7504            cmd,
7505            ..Default::default()
7506        };
7507
7508        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7509        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
7510        assert_eq!(
7511            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7512            Some("restrict"),
7513            "-r must remain owned by the unrelated global it already belonged to",
7514        );
7515        // Both globals are still recognized and global after the descent.
7516        assert!(parsed
7517            .available_flags
7518            .get("--raw")
7519            .is_some_and(|f| f.global));
7520        assert!(parsed
7521            .available_flags
7522            .get("--restrict")
7523            .is_some_and(|f| f.global));
7524    }
7525
7526    #[test]
7527    fn test_redeclared_global_aliases_share_one_flag() {
7528        // A global declared with BOTH a short and a long, re-declared non-globally by a
7529        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
7530        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
7531        // the time `-y` is reached the long already points at the merged flag. That merged flag is
7532        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
7533        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
7534        let spec = r#"
7535flag "-y --yes" global=#true effect="write"
7536cmd "run" {
7537    flag "-y --yes --assume-yes"
7538}
7539"#
7540        .parse::<Spec>()
7541        .unwrap();
7542
7543        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7544
7545        for key in ["-y", "--yes", "--assume-yes"] {
7546            let flag = parsed
7547                .available_flags
7548                .get(key)
7549                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7550            assert!(flag.global, "{key} must stay global after the descent");
7551            assert_eq!(
7552                flag.long,
7553                vec!["yes".to_string(), "assume-yes".to_string()],
7554                "{key} must resolve to the flag carrying every alias",
7555            );
7556            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7557        }
7558
7559        // One logical flag means one object: all three keys share a single `Arc`.
7560        assert_eq!(
7561            unique_flags(parsed.available_flags.values()).count(),
7562            1,
7563            "all aliases must point at one flag object, got {:?}",
7564            parsed.available_flags,
7565        );
7566
7567        // The global's effect survives the merge, so `-y` still marks the command as writing.
7568        assert_eq!(
7569            parsed.available_flags["-y"].effect,
7570            Some(crate::SpecCommandEffect::Write),
7571        );
7572    }
7573
7574    #[test]
7575    fn test_redeclared_global_keeps_hidden_alias_metadata() {
7576        let spec = r#"
7577flag "--yes" global=#true {
7578    alias "-q" "--quietly" hide=#true
7579}
7580cmd "run" {
7581    flag "--yes --assume-yes" {
7582        alias "-s" "--secret" hide=#true
7583    }
7584}
7585"#
7586        .parse::<Spec>()
7587        .unwrap();
7588
7589        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7590        let merged = &parsed.available_flags["--yes"];
7591        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7592        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7593        for key in ["-q", "-s", "--quietly", "--secret"] {
7594            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7595        }
7596    }
7597
7598    #[test]
7599    fn test_redeclared_global_can_promote_hidden_aliases() {
7600        let spec = r#"
7601flag "--yes" global=#true {
7602    alias "-q" "--quietly" hide=#true
7603}
7604cmd "run" {
7605    flag "-q --yes --quietly"
7606}
7607"#
7608        .parse::<Spec>()
7609        .unwrap();
7610
7611        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7612        let merged = &parsed.available_flags["--yes"];
7613        assert!(merged.hidden_short_aliases.is_empty());
7614        assert!(merged.hidden_aliases.is_empty());
7615        for key in ["-q", "--quietly"] {
7616            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7617        }
7618    }
7619
7620    #[test]
7621    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7622        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
7623        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
7624        // aliases the child omits are never visited by the merge loop, so they must be rebound to
7625        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
7626        // pre-merge global and miss the added `assume-yes`.
7627        let spec = r#"
7628flag "-y --yes --confirm" global=#true
7629cmd "run" {
7630    flag "--yes --assume-yes"
7631}
7632"#
7633        .parse::<Spec>()
7634        .unwrap();
7635
7636        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7637
7638        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7639            let flag = parsed
7640                .available_flags
7641                .get(key)
7642                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7643            assert!(flag.global, "{key} must stay global after the descent");
7644            assert_eq!(
7645                flag.long,
7646                vec![
7647                    "yes".to_string(),
7648                    "confirm".to_string(),
7649                    "assume-yes".to_string()
7650                ],
7651                "{key} must resolve to the flag carrying every alias",
7652            );
7653        }
7654
7655        assert_eq!(
7656            unique_flags(parsed.available_flags.values()).count(),
7657            1,
7658            "all aliases must point at one flag object, got {:?}",
7659            parsed.available_flags,
7660        );
7661    }
7662
7663    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
7664    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
7665    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
7666    ///
7667    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
7668    /// commands it merges in, so the test stays hermetic (no mount subprocess).
7669    fn mounted_task_flag_spec() -> Spec {
7670        let mut task_cmd = SpecCommand::builder()
7671            .name("mytask")
7672            .flag(
7673                SpecFlag::builder()
7674                    .name("env")
7675                    .long("env")
7676                    .arg(
7677                        SpecArg::builder()
7678                            .name("name")
7679                            .choices(["dev", "stage", "prod"])
7680                            .build(),
7681                    )
7682                    .global(false)
7683                    .build(),
7684            )
7685            .flag(
7686                SpecFlag::builder()
7687                    .name("bump")
7688                    .long("bump")
7689                    .arg(
7690                        SpecArg::builder()
7691                            .name("type")
7692                            .choices(["auto", "major"])
7693                            .build(),
7694                    )
7695                    .global(false)
7696                    .build(),
7697            )
7698            .build();
7699        task_cmd.mounted = true;
7700
7701        let mut run_cmd = SpecCommand::builder()
7702            .name("run")
7703            .flag(
7704                SpecFlag::builder()
7705                    .name("force")
7706                    .short('f')
7707                    .long("force")
7708                    .global(false)
7709                    .build(),
7710            )
7711            .build();
7712        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7713
7714        let mut cmd = SpecCommand::builder()
7715            .name("test")
7716            .flag(
7717                SpecFlag::builder()
7718                    .name("env")
7719                    .short('E')
7720                    .long("env")
7721                    .arg(SpecArg::builder().name("ENV").build())
7722                    .global(true)
7723                    .build(),
7724            )
7725            .flag(
7726                SpecFlag::builder()
7727                    .name("silent")
7728                    .long("silent")
7729                    .global(true)
7730                    .build(),
7731            )
7732            .build();
7733        cmd.subcommands.insert("run".to_string(), run_cmd);
7734
7735        Spec {
7736            name: "test".to_string(),
7737            bin: "test".to_string(),
7738            cmd,
7739            ..Default::default()
7740        }
7741    }
7742
7743    #[test]
7744    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7745        // The mounted program's own commands are ordinary commands relative to each other, so
7746        // descending *within* the mounted tree must follow the normal rules — including keeping
7747        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
7748        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
7749        // global, which the next descent's `retain(global)` then dropped entirely.
7750        let deep = SpecCommand::builder().name("deep").build();
7751        let mut sub = SpecCommand::builder()
7752            .name("sub")
7753            // Re-declares the mounted program's own global as non-global.
7754            .flag(
7755                SpecFlag::builder()
7756                    .name("cd")
7757                    .short('C')
7758                    .long("cd")
7759                    .arg(SpecArg::builder().name("dir").build())
7760                    .global(false)
7761                    .build(),
7762            )
7763            .build();
7764        sub.subcommands.insert("deep".to_string(), deep);
7765        let mut task = SpecCommand::builder()
7766            .name("task")
7767            .flag(
7768                SpecFlag::builder()
7769                    .name("cd")
7770                    .short('C')
7771                    .long("cd")
7772                    .arg(SpecArg::builder().name("dir").build())
7773                    .global(true)
7774                    .build(),
7775            )
7776            .build();
7777        task.subcommands.insert("sub".to_string(), sub);
7778        task.mark_mounted();
7779
7780        let mut run_cmd = SpecCommand::builder().name("run").build();
7781        run_cmd.subcommands.insert("task".to_string(), task);
7782        let mut cmd = SpecCommand::builder().name("test").build();
7783        cmd.subcommands.insert("run".to_string(), run_cmd);
7784        let spec = Spec {
7785            name: "test".to_string(),
7786            bin: "test".to_string(),
7787            cmd,
7788            ..Default::default()
7789        };
7790
7791        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7792        assert!(
7793            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7794            "the mounted program's own global must survive descents inside the mounted tree",
7795        );
7796        assert!(
7797            parsed.completion_flags().contains_key("--cd"),
7798            "and must still be offered there: it belongs to the mounted program",
7799        );
7800        assert!(
7801            parsed.completion_flags().contains_key("-C"),
7802            "including the short the nested command re-declared",
7803        );
7804    }
7805
7806    #[test]
7807    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7808        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
7809        // into the command the mount sits on. They belong to the mounted program, so they must
7810        // be offered inside the mounted commands rather than filtered out with the mounting
7811        // CLI's own flags.
7812        let mut task = SpecCommand::builder()
7813            .name("task")
7814            .flag(
7815                SpecFlag::builder()
7816                    .name("bump")
7817                    .long("bump")
7818                    .global(false)
7819                    .build(),
7820            )
7821            .build();
7822        task.mark_mounted();
7823
7824        let mut run_cmd = SpecCommand::builder().name("run").build();
7825        run_cmd.subcommands.insert("task".to_string(), task);
7826        // What `mount()` leaves behind when the mounted spec's root declares flags.
7827        run_cmd.flags = vec![
7828            SpecFlag::builder()
7829                .name("tglobal")
7830                .long("tglobal")
7831                .global(true)
7832                .build(),
7833            SpecFlag::builder()
7834                .name("tlocal")
7835                .long("tlocal")
7836                .global(false)
7837                .build(),
7838        ];
7839        run_cmd.flags_from_mount = true;
7840
7841        let mut cmd = SpecCommand::builder()
7842            .name("test")
7843            .flag(
7844                SpecFlag::builder()
7845                    .name("silent")
7846                    .long("silent")
7847                    .global(true)
7848                    .build(),
7849            )
7850            .build();
7851        cmd.subcommands.insert("run".to_string(), run_cmd);
7852        let spec = Spec {
7853            name: "test".to_string(),
7854            bin: "test".to_string(),
7855            cmd,
7856            ..Default::default()
7857        };
7858
7859        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7860        assert_eq!(
7861            parsed.completion_flags().keys().collect::<Vec<_>>(),
7862            vec!["--bump", "--tglobal"],
7863            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7864             `--silent` does not, and the mount's non-global root flag is not inherited",
7865        );
7866    }
7867
7868    #[test]
7869    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7870        // Regression for jdx/mise#11282. A mounted command describes another program, which
7871        // does not accept the mounting CLI's globals (mise forwards everything after a task
7872        // name to the task). They must stay recognized — they may appear before the mounted
7873        // command — but must not be offered in completions there.
7874        let spec = mounted_task_flag_spec();
7875        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7876
7877        // Still recognized for parsing...
7878        assert!(parsed.available_flags.contains_key("--silent"));
7879        assert!(parsed.available_flags.contains_key("-E"));
7880        // ...but belonging to a command above the mount, so not offered.
7881        assert_eq!(
7882            parsed.completion_flags().keys().collect::<Vec<_>>(),
7883            vec!["--bump", "--env"],
7884            "only the mounted command's own flags may be offered",
7885        );
7886
7887        // `run`'s own non-global flag is dropped on descent, as it always was.
7888        assert!(!parsed.available_flags.contains_key("--force"));
7889    }
7890
7891    #[test]
7892    fn test_mounted_cmd_flag_wins_over_inherited_global() {
7893        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
7894        // by the root's `--env` global, so completing its value fell back to file completion.
7895        let spec = mounted_task_flag_spec();
7896        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7897
7898        let awaiting = parsed
7899            .flag_awaiting_value
7900            .first()
7901            .expect("--env should await a value");
7902        assert_eq!(
7903            awaiting
7904                .arg
7905                .as_ref()
7906                .and_then(|a| a.choices.as_ref())
7907                .map(|c| c.choices.clone()),
7908            Some(vec![
7909                "dev".to_string(),
7910                "stage".to_string(),
7911                "prod".to_string()
7912            ]),
7913            "the mounted command's own --env must win over the inherited global",
7914        );
7915
7916        // The global's short is not declared by the mounted command, so it keeps pointing at
7917        // the global and a value passed before the mounted command still parses.
7918        let parsed =
7919            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7920        assert!(
7921            parsed.args.is_empty(),
7922            "prefix global tokens must not be consumed as positionals, got {:?}",
7923            parsed.args
7924        );
7925        assert_eq!(
7926            parsed.as_env().get("usage_env").map(String::as_str),
7927            Some("anything"),
7928        );
7929    }
7930
7931    #[test]
7932    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7933        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
7934        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
7935        // global's value would be validated against the mounted flag's choices and a legitimate
7936        // value would be rejected.
7937        let spec = mounted_task_flag_spec();
7938        let parsed = parse_partial(
7939            &spec,
7940            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7941        )
7942        .unwrap();
7943        assert!(
7944            parsed.errors.is_empty(),
7945            "prefix global value must not be validated against the mounted flag: {:?}",
7946            parsed
7947                .errors
7948                .iter()
7949                .map(|e| e.to_string())
7950                .collect::<Vec<_>>(),
7951        );
7952        assert_eq!(
7953            parsed.as_env().get("usage_env").map(String::as_str),
7954            Some("not-a-task-choice"),
7955        );
7956
7957        // The embedded-value form binds the same way.
7958        let parsed = parse_partial(
7959            &spec,
7960            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7961        )
7962        .unwrap();
7963        assert!(parsed.errors.is_empty());
7964        assert_eq!(
7965            parsed.as_env().get("usage_env").map(String::as_str),
7966            Some("not-a-task-choice"),
7967        );
7968
7969        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
7970        // the same name was already used before it.
7971        let parsed = parse_partial(
7972            &spec,
7973            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7974        )
7975        .unwrap();
7976        let awaiting = parsed
7977            .flag_awaiting_value
7978            .first()
7979            .expect("--env should await a value");
7980        assert_eq!(
7981            awaiting
7982                .arg
7983                .as_ref()
7984                .and_then(|a| a.choices.as_ref())
7985                .map(|c| c.choices.clone()),
7986            Some(vec![
7987                "dev".to_string(),
7988                "stage".to_string(),
7989                "prod".to_string()
7990            ]),
7991            "the mounted command's --env must own the name after the mounted command",
7992        );
7993    }
7994
7995    #[test]
7996    fn test_non_global_flag_does_not_hide_subcommand() {
7997        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
7998        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
7999        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
8000        let spec = mounted_task_flag_spec();
8001
8002        for words in [
8003            // `run` declares `-f/--force` as non-global.
8004            &["test", "run", "--force", "mytask"][..],
8005            &["test", "run", "-f", "mytask"][..],
8006            // Mixed with a global before the subcommand.
8007            &["test", "-E", "prod", "run", "--force", "mytask"][..],
8008        ] {
8009            let parsed = parse_partial(&spec, &input(words)).unwrap();
8010            assert_eq!(
8011                parsed
8012                    .cmds
8013                    .iter()
8014                    .map(|c| c.name.as_str())
8015                    .collect::<Vec<_>>(),
8016                vec!["test", "run", "mytask"],
8017                "{words:?} should descend into the mounted command",
8018            );
8019            assert!(
8020                parsed.args.is_empty(),
8021                "{words:?} should not consume a positional, got {:?}",
8022                parsed.args,
8023            );
8024            assert_eq!(
8025                parsed.as_env().get("usage_force").map(String::as_str),
8026                Some("true"),
8027                "the non-global flag must still be recorded for {words:?}",
8028            );
8029        }
8030
8031        // A non-global flag that takes a value consumes it, rather than reading the value as the
8032        // subcommand.
8033        let mut run_cmd = SpecCommand::builder()
8034            .name("run")
8035            .flag(
8036                SpecFlag::builder()
8037                    .name("output")
8038                    .short('o')
8039                    .long("output")
8040                    .arg(SpecArg::builder().name("mode").build())
8041                    .global(false)
8042                    .build(),
8043            )
8044            .build();
8045        run_cmd.subcommands.insert(
8046            "task".to_string(),
8047            SpecCommand::builder().name("task").build(),
8048        );
8049        let mut cmd = SpecCommand::builder().name("test").build();
8050        cmd.subcommands.insert("run".to_string(), run_cmd);
8051        let spec = Spec {
8052            name: "test".to_string(),
8053            bin: "test".to_string(),
8054            cmd,
8055            ..Default::default()
8056        };
8057
8058        let parsed =
8059            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8060        assert_eq!(
8061            parsed
8062                .cmds
8063                .iter()
8064                .map(|c| c.name.as_str())
8065                .collect::<Vec<_>>(),
8066            vec!["test", "run", "task"],
8067        );
8068        assert_eq!(
8069            parsed.as_env().get("usage_output").map(String::as_str),
8070            Some("quiet"),
8071        );
8072
8073        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
8074        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
8075        assert_parse_err(
8076            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8077            "unexpected word: --nope",
8078        );
8079    }
8080
8081    #[test]
8082    fn test_non_mounted_subcommand_offers_inherited_globals() {
8083        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
8084        // still both recognized and offered.
8085        let mut run_cmd = SpecCommand::builder().name("run").build();
8086        run_cmd.subcommands.insert(
8087            "nested".to_string(),
8088            SpecCommand::builder().name("nested").build(),
8089        );
8090        let mut cmd = SpecCommand::builder()
8091            .name("test")
8092            .flag(
8093                SpecFlag::builder()
8094                    .name("silent")
8095                    .long("silent")
8096                    .global(true)
8097                    .build(),
8098            )
8099            .build();
8100        cmd.subcommands.insert("run".to_string(), run_cmd);
8101        let spec = Spec {
8102            name: "test".to_string(),
8103            bin: "test".to_string(),
8104            cmd,
8105            ..Default::default()
8106        };
8107
8108        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8109        assert_eq!(
8110            parsed.completion_flags().keys().collect::<Vec<_>>(),
8111            parsed.available_flags.keys().collect::<Vec<_>>(),
8112        );
8113        assert!(parsed.completion_flags().contains_key("--silent"));
8114    }
8115
8116    #[test]
8117    fn test_subcommand_alias_collision_keeps_last_owner() {
8118        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
8119        // share an alias are resolved. Historically the flattened flag map gave the shared
8120        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
8121        let run_cmd = SpecCommand::builder()
8122            .name("run")
8123            .flag(
8124                SpecFlag::builder()
8125                    .name("alpha")
8126                    .short('x')
8127                    .long("alpha")
8128                    .global(false)
8129                    .build(),
8130            )
8131            .flag(
8132                SpecFlag::builder()
8133                    .name("beta")
8134                    .short('x')
8135                    .long("beta")
8136                    .global(false)
8137                    .build(),
8138            )
8139            .build();
8140        let mut cmd = SpecCommand::builder().name("test").build();
8141        cmd.subcommands.insert("run".to_string(), run_cmd);
8142        let spec = Spec {
8143            name: "test".to_string(),
8144            bin: "test".to_string(),
8145            cmd,
8146            ..Default::default()
8147        };
8148
8149        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8150        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
8151        assert_eq!(
8152            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8153            Some("beta"),
8154            "the last-declared flag must keep a shared short alias",
8155        );
8156        // Both distinct long aliases remain recognized and point to their own flag.
8157        assert_eq!(
8158            parsed
8159                .available_flags
8160                .get("--alpha")
8161                .map(|f| f.name.as_str()),
8162            Some("alpha"),
8163        );
8164        assert_eq!(
8165            parsed
8166                .available_flags
8167                .get("--beta")
8168                .map(|f| f.name.as_str()),
8169            Some("beta"),
8170        );
8171    }
8172
8173    #[test]
8174    fn test_default_subcommand_same_name_child() {
8175        // Test that default_subcommand doesn't cause issues when the default subcommand
8176        // has a child with the same name (e.g., "run" has a task named "run").
8177        // This verifies we don't switch multiple times or get stuck in a loop.
8178        let run_task = SpecCommand::builder()
8179            .name("run")
8180            .arg(SpecArg::builder().name("args").build())
8181            .build();
8182        let mut run_cmd = SpecCommand::builder().name("run").build();
8183        run_cmd.subcommands.insert("run".to_string(), run_task);
8184
8185        let mut cmd = SpecCommand::builder().name("test").build();
8186        cmd.subcommands.insert("run".to_string(), run_cmd);
8187
8188        let spec = Spec {
8189            name: "test".to_string(),
8190            bin: "test".to_string(),
8191            cmd,
8192            default_subcommand: Some("run".to_string()),
8193            ..Default::default()
8194        };
8195
8196        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
8197        let input = vec!["test".to_string(), "run".to_string()];
8198        let parsed = parse(&spec, &input).unwrap();
8199
8200        // Should have two commands: root and "run"
8201        assert_eq!(parsed.cmds.len(), 2);
8202        assert_eq!(parsed.cmds[0].name, "test");
8203        assert_eq!(parsed.cmds[1].name, "run");
8204
8205        // "test run run" should descend into the "run" task (child of "run" subcommand)
8206        let input = vec![
8207            "test".to_string(),
8208            "run".to_string(),
8209            "run".to_string(),
8210            "hello".to_string(),
8211        ];
8212        let parsed = parse(&spec, &input).unwrap();
8213
8214        assert_eq!(parsed.cmds.len(), 3);
8215        assert_eq!(parsed.cmds[0].name, "test");
8216        assert_eq!(parsed.cmds[1].name, "run");
8217        assert_eq!(parsed.cmds[2].name, "run");
8218        assert_eq!(parsed.args.len(), 1);
8219        let value = parsed.args.values().next().unwrap();
8220        assert_eq!(value.to_string(), "hello");
8221
8222        // Key test case: "test other" should switch to default subcommand "run"
8223        // and treat "other" as a positional arg (not try to switch again because
8224        // "run" also has a "run" child).
8225        let mut run_cmd = SpecCommand::builder()
8226            .name("run")
8227            .arg(SpecArg::builder().name("task").build())
8228            .build();
8229        let run_task = SpecCommand::builder().name("run").build();
8230        run_cmd.subcommands.insert("run".to_string(), run_task);
8231
8232        let mut cmd = SpecCommand::builder().name("test").build();
8233        cmd.subcommands.insert("run".to_string(), run_cmd);
8234
8235        let spec = Spec {
8236            name: "test".to_string(),
8237            bin: "test".to_string(),
8238            cmd,
8239            default_subcommand: Some("run".to_string()),
8240            ..Default::default()
8241        };
8242
8243        let input = vec!["test".to_string(), "other".to_string()];
8244        let parsed = parse(&spec, &input).unwrap();
8245
8246        // Should have two commands: root and "run" (the default)
8247        // We should NOT have switched again to the "run" task child
8248        assert_eq!(parsed.cmds.len(), 2);
8249        assert_eq!(parsed.cmds[0].name, "test");
8250        assert_eq!(parsed.cmds[1].name, "run");
8251
8252        // "other" should be parsed as a positional arg
8253        assert_eq!(parsed.args.len(), 1);
8254        let value = parsed.args.values().next().unwrap();
8255        assert_eq!(value.to_string(), "other");
8256    }
8257
8258    #[test]
8259    fn test_restart_token() {
8260        // Test that restart_token resets argument parsing
8261        let run_cmd = SpecCommand::builder()
8262            .name("run")
8263            .arg(SpecArg::builder().name("task").build())
8264            .restart_token(":::".to_string())
8265            .build();
8266        let mut cmd = SpecCommand::builder().name("test").build();
8267        cmd.subcommands.insert("run".to_string(), run_cmd);
8268
8269        let spec = Spec {
8270            name: "test".to_string(),
8271            bin: "test".to_string(),
8272            cmd,
8273            ..Default::default()
8274        };
8275
8276        // "test run task1 ::: task2" - should end up with task2 as the arg
8277        let input = vec![
8278            "test".to_string(),
8279            "run".to_string(),
8280            "task1".to_string(),
8281            ":::".to_string(),
8282            "task2".to_string(),
8283        ];
8284        let parsed = parse(&spec, &input).unwrap();
8285
8286        // After restart, args were cleared and task2 was parsed
8287        assert_eq!(parsed.args.len(), 1);
8288        let value = parsed.args.values().next().unwrap();
8289        assert_eq!(value.to_string(), "task2");
8290    }
8291
8292    #[test]
8293    fn test_restart_token_multiple() {
8294        // Test multiple restart tokens
8295        let run_cmd = SpecCommand::builder()
8296            .name("run")
8297            .arg(SpecArg::builder().name("task").build())
8298            .restart_token(":::".to_string())
8299            .build();
8300        let mut cmd = SpecCommand::builder().name("test").build();
8301        cmd.subcommands.insert("run".to_string(), run_cmd);
8302
8303        let spec = Spec {
8304            name: "test".to_string(),
8305            bin: "test".to_string(),
8306            cmd,
8307            ..Default::default()
8308        };
8309
8310        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
8311        let input = vec![
8312            "test".to_string(),
8313            "run".to_string(),
8314            "task1".to_string(),
8315            ":::".to_string(),
8316            "task2".to_string(),
8317            ":::".to_string(),
8318            "task3".to_string(),
8319        ];
8320        let parsed = parse(&spec, &input).unwrap();
8321
8322        // After multiple restarts, args were cleared and task3 was parsed
8323        assert_eq!(parsed.args.len(), 1);
8324        let value = parsed.args.values().next().unwrap();
8325        assert_eq!(value.to_string(), "task3");
8326    }
8327
8328    #[test]
8329    fn test_restart_token_clears_flag_awaiting_value() {
8330        // Test that restart_token clears pending flag values
8331        let run_cmd = SpecCommand::builder()
8332            .name("run")
8333            .arg(SpecArg::builder().name("task").build())
8334            .flag(
8335                SpecFlag::builder()
8336                    .name("jobs")
8337                    .long("jobs")
8338                    .arg(SpecArg::builder().name("count").build())
8339                    .build(),
8340            )
8341            .restart_token(":::".to_string())
8342            .build();
8343        let mut cmd = SpecCommand::builder().name("test").build();
8344        cmd.subcommands.insert("run".to_string(), run_cmd);
8345
8346        let spec = Spec {
8347            name: "test".to_string(),
8348            bin: "test".to_string(),
8349            cmd,
8350            ..Default::default()
8351        };
8352
8353        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
8354        let input = vec![
8355            "test".to_string(),
8356            "run".to_string(),
8357            "task1".to_string(),
8358            "--jobs".to_string(),
8359            ":::".to_string(),
8360            "task2".to_string(),
8361        ];
8362        let parsed = parse(&spec, &input).unwrap();
8363
8364        // task2 should be parsed as the task arg, not as --jobs value
8365        assert_eq!(parsed.args.len(), 1);
8366        let value = parsed.args.values().next().unwrap();
8367        assert_eq!(value.to_string(), "task2");
8368        // --jobs should not have a value
8369        assert!(parsed.flag_awaiting_value.is_empty());
8370    }
8371
8372    #[test]
8373    fn test_restart_token_resets_double_dash() {
8374        // Test that restart_token resets the -- separator effect
8375        let run_cmd = SpecCommand::builder()
8376            .name("run")
8377            .arg(SpecArg::builder().name("task").build())
8378            .arg(SpecArg::builder().name("extra_args").var(true).build())
8379            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
8380            .restart_token(":::".to_string())
8381            .build();
8382        let mut cmd = SpecCommand::builder().name("test").build();
8383        cmd.subcommands.insert("run".to_string(), run_cmd);
8384
8385        let spec = Spec {
8386            name: "test".to_string(),
8387            bin: "test".to_string(),
8388            cmd,
8389            ..Default::default()
8390        };
8391
8392        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
8393        let input = vec![
8394            "test".to_string(),
8395            "run".to_string(),
8396            "task1".to_string(),
8397            "--".to_string(),
8398            "extra".to_string(),
8399            ":::".to_string(),
8400            "--verbose".to_string(),
8401            "task2".to_string(),
8402        ];
8403        let parsed = parse(&spec, &input).unwrap();
8404
8405        // --verbose should be parsed as a flag (not an arg) after the restart
8406        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
8407        // task2 should be the arg after restart
8408        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8409        let value = parsed.args.get(task_arg).unwrap();
8410        assert_eq!(value.to_string(), "task2");
8411    }
8412
8413    #[test]
8414    fn test_double_dashes_without_preserve() {
8415        // Only the first `--` is a separator; a later one is a value, because flag
8416        // parsing has already stopped and there is nothing left for it to do.
8417        // `preserve` is about the *first* one — see the test below, where none is
8418        // consumed at all.
8419        let run_cmd = SpecCommand::builder()
8420            .name("run")
8421            .arg(SpecArg::builder().name("args").var(true).build())
8422            .build();
8423        let mut cmd = SpecCommand::builder().name("test").build();
8424        cmd.subcommands.insert("run".to_string(), run_cmd);
8425
8426        let spec = Spec {
8427            name: "test".to_string(),
8428            bin: "test".to_string(),
8429            cmd,
8430            ..Default::default()
8431        };
8432
8433        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
8434        let input = vec![
8435            "test".to_string(),
8436            "run".to_string(),
8437            "arg1".to_string(),
8438            "--".to_string(),
8439            "arg2".to_string(),
8440            "--".to_string(),
8441            "arg3".to_string(),
8442        ];
8443        let parsed = parse(&spec, &input).unwrap();
8444
8445        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8446        let value = parsed.args.get(args_arg).unwrap();
8447        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
8448    }
8449
8450    #[test]
8451    fn test_double_dashes_with_preserve() {
8452        // Test that variadic args WITH `preserve` keep all double dashes
8453        let run_cmd = SpecCommand::builder()
8454            .name("run")
8455            .arg(
8456                SpecArg::builder()
8457                    .name("args")
8458                    .var(true)
8459                    .double_dash(SpecDoubleDashChoices::Preserve)
8460                    .build(),
8461            )
8462            .build();
8463        let mut cmd = SpecCommand::builder().name("test").build();
8464        cmd.subcommands.insert("run".to_string(), run_cmd);
8465
8466        let spec = Spec {
8467            name: "test".to_string(),
8468            bin: "test".to_string(),
8469            cmd,
8470            ..Default::default()
8471        };
8472
8473        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
8474        let input = vec![
8475            "test".to_string(),
8476            "run".to_string(),
8477            "arg1".to_string(),
8478            "--".to_string(),
8479            "arg2".to_string(),
8480            "--".to_string(),
8481            "arg3".to_string(),
8482        ];
8483        let parsed = parse(&spec, &input).unwrap();
8484
8485        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8486        let value = parsed.args.get(args_arg).unwrap();
8487        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
8488    }
8489
8490    #[test]
8491    fn test_double_dashes_with_preserve_only_dashes() {
8492        // Test that variadic args WITH `preserve` keep all double dashes even
8493        // if the values are just double dashes
8494        let run_cmd = SpecCommand::builder()
8495            .name("run")
8496            .arg(
8497                SpecArg::builder()
8498                    .name("args")
8499                    .var(true)
8500                    .double_dash(SpecDoubleDashChoices::Preserve)
8501                    .build(),
8502            )
8503            .build();
8504        let mut cmd = SpecCommand::builder().name("test").build();
8505        cmd.subcommands.insert("run".to_string(), run_cmd);
8506
8507        let spec = Spec {
8508            name: "test".to_string(),
8509            bin: "test".to_string(),
8510            cmd,
8511            ..Default::default()
8512        };
8513
8514        // "test run -- --" - all double dashes should be preserved
8515        let input = vec![
8516            "test".to_string(),
8517            "run".to_string(),
8518            "--".to_string(),
8519            "--".to_string(),
8520        ];
8521        let parsed = parse(&spec, &input).unwrap();
8522
8523        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8524        let value = parsed.args.get(args_arg).unwrap();
8525        assert_eq!(value.to_string(), "-- --");
8526    }
8527
8528    #[test]
8529    fn test_double_dashes_with_preserve_multiple_args() {
8530        // Test with multiple args where only the second has has `preserve`
8531        let run_cmd = SpecCommand::builder()
8532            .name("run")
8533            .arg(SpecArg::builder().name("task").build())
8534            .arg(
8535                SpecArg::builder()
8536                    .name("extra_args")
8537                    .var(true)
8538                    .double_dash(SpecDoubleDashChoices::Preserve)
8539                    .build(),
8540            )
8541            .build();
8542        let mut cmd = SpecCommand::builder().name("test").build();
8543        cmd.subcommands.insert("run".to_string(), run_cmd);
8544
8545        let spec = Spec {
8546            name: "test".to_string(),
8547            bin: "test".to_string(),
8548            cmd,
8549            ..Default::default()
8550        };
8551
8552        // The first arg "task1" is captured normally
8553        // Then extra_args with `preserve` captures everything, including the "--" tokens
8554        let input = vec![
8555            "test".to_string(),
8556            "run".to_string(),
8557            "task1".to_string(),
8558            "--".to_string(),
8559            "arg1".to_string(),
8560            "--".to_string(),
8561            "--foo".to_string(),
8562        ];
8563        let parsed = parse(&spec, &input).unwrap();
8564
8565        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8566        let task_value = parsed.args.get(task_arg).unwrap();
8567        assert_eq!(task_value.to_string(), "task1");
8568
8569        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8570        let extra_value = parsed.args.get(extra_arg).unwrap();
8571        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8572    }
8573
8574    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8575        let cmd = SpecCommand::builder().name("test").args(args).build();
8576        Spec {
8577            name: "test".to_string(),
8578            bin: "test".to_string(),
8579            cmd,
8580            ..Default::default()
8581        }
8582    }
8583
8584    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8585        let arg = parsed
8586            .args
8587            .keys()
8588            .find(|a| a.name == name)
8589            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8590        parsed.args.get(arg).unwrap().to_string()
8591    }
8592
8593    fn required_arg(name: &str) -> SpecArg {
8594        SpecArg::builder()
8595            .name(name)
8596            .var(true)
8597            .required(false)
8598            .double_dash(SpecDoubleDashChoices::Required)
8599            .build()
8600    }
8601
8602    #[test]
8603    fn test_double_dash_required_reports_error_once_for_variadic() {
8604        // A variadic arg is offered every remaining word, but the mistake is one mistake.
8605        let spec = spec_with_args([required_arg("files")]);
8606
8607        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8608
8609        assert!(parsed.args.is_empty());
8610        assert_eq!(parsed.errors.len(), 1);
8611        assert!(
8612            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8613        );
8614    }
8615
8616    #[test]
8617    fn test_double_dash_required_suppresses_missing_arg() {
8618        // The arg is never filled, so the end-of-parse check would also call it missing.
8619        let spec = spec_with_args([SpecArg::builder()
8620            .name("file")
8621            .required(true)
8622            .double_dash(SpecDoubleDashChoices::Required)
8623            .build()]);
8624
8625        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8626
8627        assert_eq!(parsed.errors.len(), 1);
8628        assert!(matches!(
8629            &parsed.errors[0],
8630            UsageErr::ArgRequiresDoubleDash(_)
8631        ));
8632        // The cursor stays put, so a completion keeps offering the same arg.
8633        assert_eq!(
8634            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8635            Some("file")
8636        );
8637        assert!(!parsed.double_dash_seen);
8638    }
8639
8640    #[test]
8641    fn test_double_dash_routes_to_required_arg() {
8642        // Everything after `--` belongs to the arg that requires it, even though the greedy
8643        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
8644        let spec = spec_with_args([
8645            SpecArg::builder()
8646                .name("tool")
8647                .var(true)
8648                .required(false)
8649                .build(),
8650            required_arg("command"),
8651        ]);
8652
8653        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8654
8655        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8656        assert_eq!(arg_value(&parsed, "command"), "node app.js");
8657        assert!(parsed.double_dash_seen);
8658    }
8659
8660    #[test]
8661    fn test_double_dash_routes_with_gap_reports_missing_arg() {
8662        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
8663        // "is it filled?" check cannot be a count of how many args were filled.
8664        let spec = spec_with_args([
8665            SpecArg::builder()
8666                .name("tool")
8667                .var(true)
8668                .required(true)
8669                .build(),
8670            required_arg("command"),
8671        ]);
8672
8673        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8674
8675        assert_eq!(arg_value(&parsed, "command"), "ls");
8676        assert!(parsed.args.keys().all(|a| a.name != "tool"));
8677        assert!(parsed
8678            .errors
8679            .iter()
8680            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8681    }
8682
8683    #[test]
8684    fn test_double_dash_gap_applies_defaults() {
8685        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
8686        let spec = spec_with_args([
8687            SpecArg::builder()
8688                .name("tool")
8689                .var(true)
8690                .required(false)
8691                .default_value("node@20")
8692                .build(),
8693            required_arg("command"),
8694        ]);
8695
8696        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8697
8698        assert_eq!(arg_value(&parsed, "command"), "ls");
8699        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8700    }
8701
8702    fn spec_with_restart_token_and_required_arg() -> Spec {
8703        let run_cmd = SpecCommand::builder()
8704            .name("run")
8705            .arg(SpecArg::builder().name("task").build())
8706            .arg(required_arg("run_args"))
8707            .restart_token(":::".to_string())
8708            .build();
8709        let mut cmd = SpecCommand::builder().name("test").build();
8710        cmd.subcommands.insert("run".to_string(), run_cmd);
8711        Spec {
8712            name: "test".to_string(),
8713            bin: "test".to_string(),
8714            cmd,
8715            ..Default::default()
8716        }
8717    }
8718
8719    #[test]
8720    fn test_double_dash_required_restart_token_resets_separator() {
8721        // The `--` before `:::` belongs to the previous invocation only.
8722        let spec = spec_with_restart_token_and_required_arg();
8723
8724        let parsed = parse_partial(
8725            &spec,
8726            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8727        )
8728        .unwrap();
8729
8730        assert_eq!(arg_value(&parsed, "task"), "task2");
8731        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8732        // Reported once even though the arg was violated after already succeeding once.
8733        assert_eq!(
8734            parsed
8735                .errors
8736                .iter()
8737                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8738                .count(),
8739            1
8740        );
8741    }
8742
8743    #[test]
8744    fn test_double_dash_required_restart_token_accepts_new_separator() {
8745        let spec = spec_with_restart_token_and_required_arg();
8746
8747        let parsed = parse(
8748            &spec,
8749            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8750        )
8751        .unwrap();
8752
8753        assert_eq!(arg_value(&parsed, "task"), "task2");
8754        assert_eq!(arg_value(&parsed, "run_args"), "c");
8755    }
8756
8757    #[test]
8758    fn test_double_dash_preserve_is_not_a_separator() {
8759        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
8760        // arg that requires a separator. Deliberate: one token cannot be both.
8761        let spec = spec_with_args([
8762            SpecArg::builder()
8763                .name("kept")
8764                .var(true)
8765                .var_max(1)
8766                .required(false)
8767                .double_dash(SpecDoubleDashChoices::Preserve)
8768                .build(),
8769            required_arg("rest"),
8770        ]);
8771
8772        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8773
8774        assert_eq!(arg_value(&parsed, "kept"), "--");
8775        assert!(parsed.args.keys().all(|a| a.name != "rest"));
8776        assert!(!parsed.double_dash_seen);
8777        assert_eq!(parsed.errors.len(), 1);
8778    }
8779
8780    #[test]
8781    fn test_double_dash_required_does_not_bail_in_parse_partial() {
8782        // Completions parse half-typed command lines; they must still get a result.
8783        let spec = spec_with_args([required_arg("file")]);
8784
8785        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8786        assert!(parse(&spec, &input(&["test", "x"])).is_err());
8787    }
8788
8789    #[test]
8790    fn test_double_dash_without_required_arg_does_not_move_cursor() {
8791        // Specs with no `double_dash="required"` arg are untouched by the jump.
8792        let spec = spec_with_args([
8793            SpecArg::builder().name("first").required(false).build(),
8794            SpecArg::builder().name("second").required(false).build(),
8795        ]);
8796
8797        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8798
8799        assert_eq!(arg_value(&parsed, "first"), "a");
8800        assert_eq!(arg_value(&parsed, "second"), "b");
8801        assert!(parsed.next_arg.is_none());
8802    }
8803
8804    #[test]
8805    fn test_parser_with_custom_env_for_required_arg() {
8806        let spec = spec_with_arg(
8807            SpecArg::builder()
8808                .name("name")
8809                .env("NAME")
8810                .required(true)
8811                .build(),
8812        );
8813        std::env::remove_var("NAME");
8814
8815        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8816            .expect("parse should succeed with custom env");
8817        assert_eq!(parsed.args.len(), 1);
8818        assert_eq!(first_string_value(&parsed), "john");
8819    }
8820
8821    #[test]
8822    fn test_parser_with_custom_env_for_required_flag() {
8823        let spec = spec_with_flag(
8824            SpecFlag::builder()
8825                .long("name")
8826                .env("NAME")
8827                .required(true)
8828                .arg(SpecArg::builder().name("name").build())
8829                .build(),
8830        );
8831        std::env::remove_var("NAME");
8832
8833        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8834            .expect("parse should succeed with custom env");
8835        assert_eq!(parsed.flags.len(), 1);
8836        assert_eq!(first_string_value(&parsed), "jane");
8837    }
8838
8839    #[test]
8840    fn test_flag_environment_fallbacks_preserve_declaration_order() {
8841        let spec = spec_with_flag(
8842            SpecFlag::builder()
8843                .long("name")
8844                .env("NAME")
8845                .env_fallback("OLD_NAME")
8846                .env_fallback("OLDER_NAME")
8847                .deprecated_env("DEPRECATED_NAME")
8848                .arg(SpecArg::builder().name("name").build())
8849                .build(),
8850        );
8851
8852        let parsed = parse_with_env(
8853            &spec,
8854            &["test"],
8855            &[
8856                ("NAME", "canonical"),
8857                ("OLD_NAME", "fallback"),
8858                ("DEPRECATED_NAME", "deprecated"),
8859            ],
8860        )
8861        .unwrap();
8862        assert_eq!(first_string_value(&parsed), "canonical");
8863
8864        let parsed = parse_with_env(
8865            &spec,
8866            &["test"],
8867            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8868        )
8869        .unwrap();
8870        assert_eq!(first_string_value(&parsed), "old");
8871
8872        let parsed =
8873            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8874        assert_eq!(first_string_value(&parsed), "deprecated");
8875    }
8876
8877    #[test]
8878    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8879        let spec = spec_with_flag(
8880            SpecFlag::builder()
8881                .long("name")
8882                .env("NAME")
8883                .deprecated_env("DEPRECATED_NAME")
8884                .arg(SpecArg::builder().name("name").build())
8885                .build(),
8886        );
8887
8888        // The current name is not a deprecated one, and says nothing.
8889        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8890        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8891
8892        let parsed =
8893            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8894        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8895        assert_eq!(
8896            parsed.warnings[0].kind,
8897            crate::warn::WarningKind::DeprecatedEnv
8898        );
8899        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8900        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8901        // Reported, not printed, and the value still arrives.
8902        assert_eq!(first_string_value(&parsed), "deprecated");
8903    }
8904
8905    #[test]
8906    fn a_deprecated_flag_reports_only_when_it_was_used() {
8907        let spec = spec_with_flag(
8908            SpecFlag::builder()
8909                .long("output")
8910                .deprecated("use --out")
8911                .deprecated_remove_at("3.0.0")
8912                .arg(SpecArg::builder().name("output").build())
8913                .build(),
8914        );
8915
8916        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8917        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8918
8919        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8920        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8921        assert_eq!(
8922            parsed.warnings[0].kind,
8923            crate::warn::WarningKind::DeprecatedFlag
8924        );
8925        // Named the way it was typed, dashes and all.
8926        assert_eq!(parsed.warnings[0].name, "--output");
8927        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8928        assert_eq!(
8929            parsed.warnings[0].render(),
8930            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8931        );
8932    }
8933
8934    #[test]
8935    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8936        let flag = SpecFlag::builder()
8937            .long("output")
8938            .deprecated("use --out")
8939            .deprecated_warn_at("9.0.0")
8940            .arg(SpecArg::builder().name("output").build())
8941            .build();
8942        let mut spec = spec_with_flag(flag);
8943        spec.version = Some("2.0.0".to_string());
8944
8945        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8946        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8947
8948        // And once the CLI is the release that was named, it speaks up.
8949        spec.version = Some("9.0.0".to_string());
8950        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8951        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8952    }
8953
8954    #[test]
8955    fn test_parser_with_custom_env_still_fails_when_missing() {
8956        let spec = spec_with_arg(
8957            SpecArg::builder()
8958                .name("name")
8959                .env("NAME")
8960                .required(true)
8961                .build(),
8962        );
8963        std::env::remove_var("NAME");
8964        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8965    }
8966
8967    #[test]
8968    fn test_parser_does_not_treat_env_choice_value_as_help() {
8969        let spec = spec_with_arg(
8970            SpecArg::builder()
8971                .name("env")
8972                .env("CURRENT_ENV")
8973                .choices(["dev", "staging"])
8974                .required(false)
8975                .build(),
8976        );
8977
8978        assert_parse_err(
8979            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8980            "Invalid choice for arg env: --help, expected one of dev, staging",
8981        );
8982    }
8983
8984    #[test]
8985    fn test_parser_does_not_treat_default_choice_value_as_help() {
8986        let spec = spec_with_flag(
8987            SpecFlag::builder()
8988                .long("env")
8989                .arg(
8990                    SpecArg::builder()
8991                        .name("env")
8992                        .choices(["dev", "staging"])
8993                        .build(),
8994                )
8995                .default_value("--help")
8996                .build(),
8997        );
8998
8999        assert_parse_err(
9000            parse_with_env(&spec, &["test"], &[]),
9001            "Invalid choice for option env: --help, expected one of dev, staging",
9002        );
9003    }
9004
9005    /// argv as `parse` wants it, program name included.
9006    fn words(of: &[&str]) -> Vec<String> {
9007        of.iter().map(|s| s.to_string()).collect()
9008    }
9009
9010    #[test]
9011    fn a_command_that_needs_a_subcommand_says_so() {
9012        // The spec has carried `subcommand_required` since the derive needed it, and this parser
9013        // never read it — so `mise generate`, which declares it, parsed as a complete
9014        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
9015        let spec: Spec = r#"
9016name "ex"
9017bin "ex"
9018cmd "gen" subcommand_required=#true {
9019    cmd "two" {}
9020    cmd "one" {}
9021    cmd "secret" hide=#true {}
9022    alias "g"
9023}
9024cmd "open" {
9025    cmd "sub" {}
9026}
9027"#
9028        .parse()
9029        .unwrap();
9030
9031        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9032        // Sorted, so the message does not depend on map order; hidden commands left out,
9033        // because a message telling someone to type a hidden name is worse than a vague one;
9034        // and the alias not listed beside the name it points at.
9035        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9036
9037        // Reached through its alias, and still about the command rather than the spelling.
9038        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9039        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9040
9041        // Given one: fine.
9042        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9043
9044        // And a command that has subcommands without declaring them required is untouched —
9045        // this is the half that keeps the check from being "any command with children".
9046        parse(&spec, &words(&["ex", "open"])).unwrap();
9047        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9048    }
9049
9050    #[test]
9051    fn arg_required_else_help_observes_the_selected_commands_argv() {
9052        let spec: Spec = r#"
9053name "ex"
9054bin "ex"
9055flag "--verbose" global=#true
9056cmd "run" arg_required_else_help=#true {
9057    flag "--all"
9058}
9059"#
9060        .parse()
9061        .unwrap();
9062        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9063
9064        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9065        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9066
9067        // A global before the command belongs to the ancestor. It selected `run`, but did not
9068        // give `run` an argument of its own.
9069        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9070        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9071
9072        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9073    }
9074
9075    #[test]
9076    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9077        let spec: Spec = r#"
9078name "ex"
9079bin "ex"
9080unknown_flags "error"
9081external_subcommand #true
9082cmd "install"
9083flag "-v --verbose" global=#true
9084"#
9085        .parse()
9086        .unwrap();
9087
9088        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9089        assert_eq!(
9090            parsed.external,
9091            Some(vec!["foo".into(), "--help".into(), "bar".into()])
9092        );
9093        assert!(parsed.flags.is_empty());
9094
9095        // Known subcommands still win.
9096        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9097        assert_eq!(parsed.cmd.name, "install");
9098        assert!(parsed.external.is_none());
9099
9100        // A global flag before the unmatched word still binds on the parent.
9101        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9102        assert_eq!(
9103            parsed.external,
9104            Some(vec!["foo".into(), "--verbose".into()])
9105        );
9106        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9107
9108        // An unknown flag on the parent is still an error, which is what clap does.
9109        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9110
9111        // A negative number is a value, not a flag, so it can be the unmatched word.
9112        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
9113        // token as a flag and never reach the catch-all.
9114        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9115        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9116    }
9117
9118    #[test]
9119    fn an_external_subcommand_satisfies_subcommand_required() {
9120        let mut spec: Spec = r#"
9121name "ex"
9122bin "ex"
9123external_subcommand #true
9124cmd "install"
9125"#
9126        .parse()
9127        .unwrap();
9128        spec.cmd.subcommand_required = true;
9129
9130        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9131        assert!(parse(&spec, &input(&["ex"])).is_err());
9132    }
9133
9134    #[test]
9135    fn a_default_subcommand_outranks_an_external_one() {
9136        let spec: Spec = r#"
9137name "ex"
9138bin "ex"
9139default_subcommand "run"
9140external_subcommand #true
9141cmd "run" {
9142    arg "[task]"
9143}
9144"#
9145        .parse()
9146        .unwrap();
9147
9148        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
9149        assert_eq!(parsed.cmd.name, "run");
9150        assert!(parsed.external.is_none());
9151        assert_eq!(first_string_value(&parsed), "build");
9152    }
9153
9154    #[test]
9155    fn multicall_basename_strips_a_path_and_exe() {
9156        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
9157        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
9158        assert_eq!(multicall_basename("LS.EXE"), "LS");
9159        assert_eq!(multicall_basename("busybox"), "busybox");
9160    }
9161
9162    #[test]
9163    fn a_multicall_applet_is_the_first_word() {
9164        let spec: Spec = r#"
9165name "busybox"
9166bin "busybox"
9167multicall #true
9168cmd "ls" {
9169    arg "[ARGS]" var=#true
9170}
9171cmd "cat"
9172"#
9173        .parse()
9174        .unwrap();
9175
9176        // A symlink: argv[0] is the applet.
9177        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
9178        assert_eq!(parsed.cmd.name, "ls");
9179        match parsed.args.values().next() {
9180            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
9181            other => panic!("expected ARGS to collect -l, got {other:?}"),
9182        }
9183
9184        // A dispatcher invocation still skips argv[0].
9185        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
9186        assert_eq!(parsed.cmd.name, "ls");
9187
9188        // Configured dispatcher values receive the same path and extension normalization.
9189        let mut configured = spec.clone();
9190        configured.name = "BusyBox".to_string();
9191        configured.bin = "/opt/bin/busybox.exe".to_string();
9192        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
9193        assert_eq!(parsed.cmd.name, "ls");
9194
9195        // `.exe` is stripped so Windows and Unix agree.
9196        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
9197        assert_eq!(parsed.cmd.name, "ls");
9198
9199        // Without the property, argv[0] is discarded as usual.
9200        let mut plain = spec.clone();
9201        plain.multicall = false;
9202        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
9203        assert_eq!(parsed.cmd.name, "ls");
9204    }
9205
9206    #[test]
9207    fn a_multicall_unknown_applet_can_be_external() {
9208        let spec: Spec = r#"
9209name "busybox"
9210bin "busybox"
9211multicall #true
9212unknown_flags "error"
9213external_subcommand #true
9214cmd "ls"
9215"#
9216        .parse()
9217        .unwrap();
9218
9219        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
9220        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
9221
9222        let mut closed = spec.clone();
9223        closed.cmd.external_subcommand = false;
9224        assert!(parse(&closed, &input(&["wat"])).is_err());
9225    }
9226
9227    #[cfg(feature = "unstable_choices_env")]
9228    #[test]
9229    fn test_parser_arg_choices_from_custom_env() {
9230        let spec = spec_arg_choices_env("DEPLOY_ENVS");
9231
9232        let parsed =
9233            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
9234        assert_eq!(first_string_value(&parsed), "bar");
9235
9236        assert_parse_err(
9237            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
9238            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
9239        );
9240        assert_parse_err(
9241            parse_with_env(&spec, &["test", "prod"], &[]),
9242            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
9243        );
9244    }
9245
9246    #[cfg(feature = "unstable_choices_env")]
9247    #[test]
9248    fn test_parser_validates_flag_choices_from_custom_env() {
9249        let spec = spec_flag_choices_env("DEPLOY_ENVS");
9250        let parsed = parse_with_env(
9251            &spec,
9252            &["test", "--env", "baz"],
9253            &[("DEPLOY_ENVS", "foo,bar baz")],
9254        )
9255        .unwrap();
9256        assert_eq!(first_string_value(&parsed), "baz");
9257    }
9258
9259    #[cfg(feature = "unstable_choices_env")]
9260    #[test]
9261    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
9262        let arg_env_spec = spec_with_arg(
9263            SpecArg::builder()
9264                .name("env")
9265                .env("CURRENT_ENV")
9266                .choices_env("DEPLOY_ENVS")
9267                .build(),
9268        );
9269        assert_parse_err(
9270            parse_with_env(
9271                &arg_env_spec,
9272                &["test"],
9273                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
9274            ),
9275            "Invalid choice for arg env: prod, expected one of dev, staging",
9276        );
9277
9278        let flag_default_spec = spec_with_flag(
9279            SpecFlag::builder()
9280                .long("env")
9281                .arg(
9282                    SpecArg::builder()
9283                        .name("env")
9284                        .choices_env("DEPLOY_ENVS")
9285                        .build(),
9286                )
9287                .default_value("prod")
9288                .build(),
9289        );
9290        assert_parse_err(
9291            parse_with_env(
9292                &flag_default_spec,
9293                &["test"],
9294                &[("DEPLOY_ENVS", "dev,staging")],
9295            ),
9296            "Invalid choice for option env: prod, expected one of dev, staging",
9297        );
9298    }
9299
9300    #[test]
9301    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
9302        let spec: Spec = r#"
9303            flag "-v --verbose" var=#true
9304            arg "[database]" default="myapp_dev"
9305            arg "[args...]"
9306        "#
9307        .parse()
9308        .unwrap();
9309        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9310            .into_iter()
9311            .map(String::from)
9312            .collect();
9313        let parsed = parse(&spec, &input).unwrap();
9314        let env = parsed.as_env();
9315        assert_eq!(env.get("usage_database").unwrap(), "mydb");
9316        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
9317    }
9318
9319    #[test]
9320    fn test_variadic_arg_captures_unknown_flags() {
9321        let cmd = SpecCommand::builder()
9322            .name("test")
9323            .flag(SpecFlag::builder().short('v').long("verbose").build())
9324            .arg(SpecArg::builder().name("database").required(false).build())
9325            .arg(
9326                SpecArg::builder()
9327                    .name("args")
9328                    .required(false)
9329                    .var(true)
9330                    .build(),
9331            )
9332            .build();
9333        let spec = Spec {
9334            name: "test".to_string(),
9335            bin: "test".to_string(),
9336            cmd,
9337            ..Default::default()
9338        };
9339
9340        // Unknown --host flag and its value should be captured by [args...]
9341        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9342            .into_iter()
9343            .map(String::from)
9344            .collect();
9345        let parsed = parse(&spec, &input).unwrap();
9346        assert_eq!(parsed.args.len(), 2);
9347        let args_val = parsed
9348            .args
9349            .iter()
9350            .find(|(a, _)| a.name == "args")
9351            .unwrap()
9352            .1;
9353        match args_val {
9354            ParseValue::MultiString(v) => {
9355                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9356            }
9357            _ => panic!("Expected MultiString, got {:?}", args_val),
9358        }
9359    }
9360
9361    #[test]
9362    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
9363        let cmd = SpecCommand::builder()
9364            .name("test")
9365            .flag(SpecFlag::builder().short('v').long("verbose").build())
9366            .arg(SpecArg::builder().name("database").required(false).build())
9367            .arg(
9368                SpecArg::builder()
9369                    .name("args")
9370                    .required(false)
9371                    .var(true)
9372                    .build(),
9373            )
9374            .build();
9375        let spec = Spec {
9376            name: "test".to_string(),
9377            bin: "test".to_string(),
9378            cmd,
9379            ..Default::default()
9380        };
9381
9382        // With explicit -- separator
9383        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
9384            .into_iter()
9385            .map(String::from)
9386            .collect();
9387        let parsed = parse(&spec, &input).unwrap();
9388        assert_eq!(parsed.args.len(), 2);
9389        let args_val = parsed
9390            .args
9391            .iter()
9392            .find(|(a, _)| a.name == "args")
9393            .unwrap()
9394            .1;
9395        match args_val {
9396            ParseValue::MultiString(v) => {
9397                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9398            }
9399            _ => panic!("Expected MultiString, got {:?}", args_val),
9400        }
9401    }
9402
9403    #[test]
9404    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
9405        // Regression: --flag=value should be treated as a single positional token when
9406        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
9407        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
9408
9409        // Single unknown --flag=value: must not produce a stray "3" positional.
9410        // as_env() shell-joins values, so "=" gets quoted.
9411        let input: Vec<String> = vec!["test", "--option=3"]
9412            .into_iter()
9413            .map(String::from)
9414            .collect();
9415        let parsed = parse(&spec, &input).unwrap();
9416        let env = parsed.as_env();
9417        assert_eq!(
9418            env.get("usage_other_args").map(String::as_str),
9419            Some("'--option=3'"),
9420            "expected a single --option=3 token, got {:?}",
9421            env.get("usage_other_args"),
9422        );
9423
9424        // Multiple unknown --flag=value args should each be kept intact
9425        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
9426            .into_iter()
9427            .map(String::from)
9428            .collect();
9429        let parsed2 = parse(&spec, &input2).unwrap();
9430        let env2 = parsed2.as_env();
9431        assert_eq!(
9432            env2.get("usage_other_args").map(String::as_str),
9433            Some("'--foo=bar' '--baz=qux'"),
9434            "expected two intact tokens, got {:?}",
9435            env2.get("usage_other_args"),
9436        );
9437
9438        // Mix of plain positional args and unknown --flag=value tokens
9439        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
9440            .into_iter()
9441            .map(String::from)
9442            .collect();
9443        let parsed3 = parse(&spec, &input3).unwrap();
9444        let env3 = parsed3.as_env();
9445        assert_eq!(
9446            env3.get("usage_other_args").map(String::as_str),
9447            Some("positional1 '--option=3' positional2"),
9448            "expected positional args and intact flag token, got {:?}",
9449            env3.get("usage_other_args"),
9450        );
9451    }
9452
9453    #[test]
9454    fn test_allow_hyphen_values_consumes_short_flag_collision() {
9455        let spec = r#"
9456flag "-d --working-dir <DIR>"
9457flag "-a --args <ARGS>" allow_hyphen_values=#true
9458"#
9459        .parse::<Spec>()
9460        .unwrap();
9461
9462        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9463
9464        assert_eq!(parsed.flags.len(), 1);
9465        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9466    }
9467
9468    #[test]
9469    fn test_allow_hyphen_values_consumes_embedded_long_value() {
9470        let spec = r#"
9471flag "-d --working-dir <DIR>"
9472flag "-a --args <ARGS>" allow_hyphen_values=#true
9473"#
9474        .parse::<Spec>()
9475        .unwrap();
9476
9477        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
9478
9479        assert_eq!(parsed.flags.len(), 1);
9480        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9481    }
9482
9483    #[test]
9484    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
9485        // The flag is declared to accept a token that looks like a flag, and `--` looks
9486        // like one, so it binds — which is what clap does with the same declaration.
9487        // Letting the separator arm run first consumed it and left the flag hungry, and
9488        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
9489        let spec = r#"
9490flag "-a --args <ARGS>" allow_hyphen_values=#true
9491arg "[rest]..."
9492"#
9493        .parse::<Spec>()
9494        .unwrap();
9495
9496        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
9497
9498        assert_eq!(flag_string_value(&parsed, "args"), "--");
9499        let rest = parsed
9500            .args
9501            .values()
9502            .next()
9503            .expect("expected the word after the separator to reach the argument");
9504        assert_eq!(rest.to_string(), "-x");
9505    }
9506
9507    #[test]
9508    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9509        // Which token supplied the first value says nothing about how many the argument
9510        // takes, so collection carries on from a hyphenated one exactly as from a plain
9511        // one. It still stops at the next flag-like token, which is what keeps a second
9512        // occurrence of the flag from being eaten as a value.
9513        let spec = r#"
9514flag "-a --args <ARGS>..." allow_hyphen_values=#true
9515"#
9516        .parse::<Spec>()
9517        .unwrap();
9518
9519        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9520
9521        let flag = parsed
9522            .flags
9523            .keys()
9524            .find(|flag| flag.name == "args")
9525            .expect("expected args flag");
9526        match parsed.flags.get(flag).expect("expected args value") {
9527            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9528            other => panic!("expected a list of values, got {other:?}"),
9529        }
9530    }
9531
9532    #[test]
9533    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9534        let spec = r#"
9535flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9536"#
9537        .parse::<Spec>()
9538        .unwrap();
9539
9540        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9541
9542        let flag = parsed
9543            .flags
9544            .keys()
9545            .find(|flag| flag.name == "args")
9546            .expect("expected args flag");
9547        let value = parsed.flags.get(flag).expect("expected args value");
9548        match value {
9549            ParseValue::MultiString(values) => {
9550                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9551            }
9552            _ => panic!("expected MultiString, got {value:?}"),
9553        }
9554    }
9555
9556    #[test]
9557    fn test_require_equals_accepts_attached_and_refuses_detached() {
9558        let spec = r#"
9559flag "--inspect <PORT>" require_equals=#true
9560"#
9561        .parse::<Spec>()
9562        .unwrap();
9563
9564        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9565        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9566
9567        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9568        let msg = format!("{err}");
9569        assert!(
9570            msg.contains("requires an argument") || msg.contains("inspect"),
9571            "detached value must be refused: {msg}"
9572        );
9573    }
9574
9575    #[test]
9576    fn boolean_flags_can_accept_attached_values_when_enabled() {
9577        let spec: Spec = r#"
9578name "ex"
9579bin "ex"
9580flag "--color" negate="--no-color" bool_value=#true
9581arg "[rest]"
9582"#
9583        .parse()
9584        .unwrap();
9585
9586        for (token, expected) in [
9587            ("--color", true),
9588            ("--color=true", true),
9589            ("--color=false", false),
9590            ("--no-color", false),
9591            ("--no-color=false", true),
9592        ] {
9593            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9594            assert!(
9595                matches!(
9596                    parsed.flags.get(&spec.cmd.flags[0]),
9597                    Some(ParseValue::Bool(value)) if *value == expected
9598                ),
9599                "{token}"
9600            );
9601        }
9602
9603        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9604        assert!(matches!(
9605            parsed.args.get(&spec.cmd.args[0]),
9606            Some(ParseValue::String(value)) if value == "word"
9607        ));
9608        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9609        assert!(err.to_string().contains("expected `true` or `false`"));
9610
9611        let strict: Spec = r#"
9612name "ex"
9613bin "ex"
9614args_override_self #false
9615flag "--color" negate="--no-color" bool_value=#true
9616"#
9617        .parse()
9618        .unwrap();
9619        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9620        let parsed = parse(
9621            &strict,
9622            &input(&["ex", "--color=false", "--no-color=false"]),
9623        )
9624        .unwrap();
9625        assert!(matches!(
9626            parsed.flags.get(&strict.cmd.flags[0]),
9627            Some(ParseValue::Bool(true))
9628        ));
9629    }
9630
9631    #[test]
9632    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9633        let spec = r#"
9634flag "-a --all"
9635flag "-i --inspect <PORT>" require_equals=#true
9636"#
9637        .parse::<Spec>()
9638        .unwrap();
9639
9640        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9641        let msg = format!("{err}");
9642        assert!(
9643            msg.contains("requires an argument") || msg.contains("inspect"),
9644            "bundled short must refuse the following word: {msg}"
9645        );
9646    }
9647
9648    #[test]
9649    fn test_default_missing_binds_when_the_value_is_left_off() {
9650        let spec = r#"
9651flag "-c --color <WHEN>" default_missing="always"
9652flag "-v --verbose"
9653"#
9654        .parse::<Spec>()
9655        .unwrap();
9656
9657        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9658        assert_eq!(flag_string_value(&parsed, "color"), "always");
9659
9660        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9661        assert_eq!(flag_string_value(&parsed, "color"), "never");
9662
9663        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9664        assert_eq!(flag_string_value(&parsed, "color"), "never");
9665
9666        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9667        assert_eq!(flag_string_value(&parsed, "color"), "always");
9668        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9669
9670        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9671        assert_eq!(flag_string_value(&parsed, "color"), "");
9672
9673        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9674        assert_eq!(flag_string_value(&parsed, "color"), "never");
9675
9676        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9677        assert_eq!(flag_string_value(&parsed, "color"), "always");
9678        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9679    }
9680
9681    #[test]
9682    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
9683        let spec = r#"
9684flag "--apps <N>"
9685flag "--jobs <N>" default_missing="default missing"
9686flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
9687"#
9688        .parse::<Spec>()
9689        .unwrap();
9690
9691        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
9692        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9693
9694        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
9695        assert!(
9696            err.to_string().contains("unexpected word: -1"),
9697            "default_missing must keep an unopted negative value separate: {err}"
9698        );
9699
9700        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
9701        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
9702
9703        let external_spec = r#"
9704external_subcommand #true
9705flag "--apps <N>"
9706"#
9707        .parse::<Spec>()
9708        .unwrap();
9709        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
9710        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9711        assert!(parsed.external.is_none());
9712
9713        for words in [
9714            &["test", "--apps", "--jobs", "1"][..],
9715            &["test", "--apps", "--kids", "-1"][..],
9716        ] {
9717            let err = parse(&spec, &input(words)).unwrap_err();
9718            let message = err.to_string();
9719            assert!(
9720                message.contains("--apps") && message.contains("requires an argument"),
9721                "the earlier flag must report its missing value for {words:?}: {message}"
9722            );
9723        }
9724    }
9725
9726    #[test]
9727    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9728        let spec = r#"
9729flag "--bump [LEVEL]" value_optional=#true
9730flag "--verbose"
9731arg "[FILE]"
9732"#
9733        .parse::<Spec>()
9734        .unwrap();
9735
9736        let absent = parse(&spec, &input(&["test"])).unwrap();
9737        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9738
9739        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9740        let bump = bare
9741            .flags
9742            .iter()
9743            .find(|(flag, _)| flag.name == "bump")
9744            .map(|(_, value)| value)
9745            .unwrap();
9746        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9747        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9748        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9749
9750        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9751        assert_eq!(flag_string_value(&explicit, "bump"), "");
9752
9753        let corrected = parse(
9754            &spec,
9755            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9756        )
9757        .unwrap();
9758        let bump = corrected
9759            .flags
9760            .iter()
9761            .find(|(flag, _)| flag.name == "bump")
9762            .map(|(_, value)| value)
9763            .unwrap();
9764        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9765
9766        let collecting = r#"
9767flag "--tag [TAG]..." value_optional=#true
9768flag "--verbose"
9769"#
9770        .parse::<Spec>()
9771        .unwrap();
9772        let valued = parse(
9773            &collecting,
9774            &input(&["test", "--tag", "one", "two", "--verbose"]),
9775        )
9776        .unwrap();
9777        let tag = valued
9778            .flags
9779            .iter()
9780            .find(|(flag, _)| flag.name == "tag")
9781            .map(|(_, value)| value)
9782            .unwrap();
9783        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9784    }
9785
9786    #[test]
9787    fn test_repeatable_bare_optional_values_count_each_occurrence() {
9788        let spec = r#"
9789flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9790"#
9791        .parse::<Spec>()
9792        .unwrap();
9793
9794        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9795        let tag = parsed
9796            .flags
9797            .iter()
9798            .find(|(flag, _)| flag.name == "tag")
9799            .map(|(_, value)| value)
9800            .unwrap();
9801        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9802
9803        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9804        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9805    }
9806
9807    #[test]
9808    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9809        let spec = r#"
9810flag "--tag [TAG]..." var=#true value_optional=#true
9811flag "--verbose"
9812"#
9813        .parse::<Spec>()
9814        .unwrap();
9815
9816        for argv in [
9817            &["test", "--tag", "one", "two"][..],
9818            &["test", "--tag", "one", "two", "--verbose"][..],
9819            &["test", "--tag", "one", "--tag", "two"][..],
9820        ] {
9821            let parsed = parse(&spec, &input(argv)).unwrap();
9822            let tag = parsed
9823                .flags
9824                .iter()
9825                .find(|(flag, _)| flag.name == "tag")
9826                .map(|(_, value)| value)
9827                .unwrap();
9828            assert!(
9829                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9830                "argv={argv:?}: {tag:?}"
9831            );
9832        }
9833
9834        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9835        let tag = bare
9836            .flags
9837            .iter()
9838            .find(|(flag, _)| flag.name == "tag")
9839            .map(|(_, value)| value)
9840            .unwrap();
9841        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9842    }
9843
9844    #[test]
9845    fn test_default_missing_with_require_equals_refuses_the_following_word() {
9846        let spec = r#"
9847flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9848arg "[rest]"
9849"#
9850        .parse::<Spec>()
9851        .unwrap();
9852
9853        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9854        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9855
9856        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9857        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9858
9859        // The following word is not the value; the missing value is, and 80 is a positional.
9860        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9861        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9862        assert_eq!(
9863            parsed
9864                .args
9865                .values()
9866                .next()
9867                .map(|v| v.to_string())
9868                .as_deref(),
9869            Some("80")
9870        );
9871
9872        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9873        assert_eq!(flag_string_value(&parsed, "inspect"), "");
9874    }
9875
9876    #[test]
9877    fn test_default_missing_must_be_a_choice() {
9878        let spec = r#"
9879flag "--color <WHEN>" default_missing="always" {
9880    choices "auto" "always" "never"
9881}
9882"#
9883        .parse::<Spec>()
9884        .unwrap();
9885
9886        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9887        assert_eq!(flag_string_value(&parsed, "color"), "always");
9888
9889        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9890        assert_eq!(flag_string_value(&parsed, "color"), "never");
9891
9892        let spec = r#"
9893flag "--color <WHEN>" default_missing="wat" {
9894    choices "auto" "always" "never"
9895}
9896"#
9897        .parse::<Spec>()
9898        .unwrap();
9899
9900        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9901        let msg = format!("{err}");
9902        assert!(
9903            msg.contains("Invalid choice for option color: wat"),
9904            "missing default has to pass choices the same way a typed value does: {msg}"
9905        );
9906
9907        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9908        let msg = format!("{err}");
9909        assert!(
9910            msg.contains("Invalid choice for option color: wat"),
9911            "an attached value that is not a choice is still refused: {msg}"
9912        );
9913
9914        let spec = r#"
9915flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9916    choices "9229" "80"
9917}
9918arg "[rest]"
9919"#
9920        .parse::<Spec>()
9921        .unwrap();
9922
9923        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9924        let msg = format!("{err}");
9925        assert!(
9926            msg.contains("Invalid choice for option inspect: wat"),
9927            "require_equals still binds the missing string, so the error is the choice: {msg}"
9928        );
9929    }
9930
9931    #[test]
9932    fn test_hyphen_values_still_start_short_flag_parsing() {
9933        let spec = r#"
9934flag "-d --working-dir <DIR>"
9935flag "-a --args <ARGS>"
9936"#
9937        .parse::<Spec>()
9938        .unwrap();
9939
9940        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
9941        let message = err.to_string();
9942        assert!(
9943            message.contains("--args") && message.contains("requires an argument"),
9944            "the recognized -d must leave the earlier -a missing: {message}"
9945        );
9946    }
9947
9948    /// `available_flags` has to agree with what an actual parse accepts, since
9949    /// its whole reason to exist is answering that question without one.
9950    mod available_flags {
9951        use super::*;
9952
9953        fn spec() -> Spec {
9954            r#"
9955bin "test"
9956flag "-v --verbose" global=#true
9957flag "--raw" global=#true effect="write"
9958flag "--local-only"
9959cmd "run" {
9960    flag "-r --raw"
9961    flag "-w --watch"
9962    cmd "once"
9963}
9964"#
9965            .parse::<Spec>()
9966            .unwrap()
9967        }
9968
9969        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9970            let mut chain = vec![&spec.cmd];
9971            for segment in path {
9972                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9973            }
9974            chain
9975        }
9976
9977        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9978            let mut names: Vec<_> = available_flags(&chain(spec, path))
9979                .iter()
9980                .map(|f| f.name.clone())
9981                .collect();
9982            names.sort();
9983            names
9984        }
9985
9986        #[test]
9987        fn an_empty_chain_yields_nothing() {
9988            assert!(available_flags(&[]).is_empty());
9989        }
9990
9991        #[test]
9992        fn the_root_gets_its_own_flags() {
9993            let spec = spec();
9994            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9995        }
9996
9997        #[test]
9998        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9999            let spec = spec();
10000            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10001        }
10002
10003        #[test]
10004        fn a_re_declared_global_is_listed_once() {
10005            // The merge can leave the long key on the merged flag and the short
10006            // key on the pre-merge one. Same flag; it must not be listed twice.
10007            let spec = r#"
10008bin "test"
10009flag "-y --yes" global=#true effect="write"
10010cmd "rm" {
10011    flag "-y --yes"
10012}
10013"#
10014            .parse::<Spec>()
10015            .unwrap();
10016            let flags = available_flags(&chain(&spec, &["rm"]));
10017            assert_eq!(flags.len(), 1, "{flags:?}");
10018            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10019        }
10020
10021        #[test]
10022        fn a_re_declared_global_keeps_the_globals_declaration() {
10023            // `run` re-declares the long-only global `--raw` as `-r --raw`
10024            // without `global`. That is the same flag: the global's `effect`
10025            // survives, the orphan short is unioned in, and it stays global.
10026            let spec = spec();
10027            let flags = available_flags(&chain(&spec, &["run"]));
10028            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10029            assert!(raw.global);
10030            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10031            assert_eq!(raw.short, ['r']);
10032        }
10033
10034        #[test]
10035        fn it_matches_what_a_parse_accepts() {
10036            // The invariant. If these ever disagree, one of them is lying to a
10037            // caller about which flags a command takes.
10038            let spec = spec();
10039            for path in [vec![], vec!["run"], vec!["run", "once"]] {
10040                let argv = std::iter::once("test".to_string())
10041                    .chain(path.iter().map(|s| s.to_string()))
10042                    .collect::<Vec<_>>();
10043                let parsed = parse_partial(&spec, &argv).unwrap();
10044
10045                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10046                    .map(|f| f.name.clone())
10047                    .collect();
10048                from_parse.sort();
10049                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10050            }
10051        }
10052    }
10053
10054    // Provenance: which token bound what, and where a value came from when no token did.
10055
10056    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
10057    /// assert on the whole picture rather than on one field at a time.
10058    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10059        parsed
10060            .tokens
10061            .iter()
10062            .find(|token| token.index == index)
10063            .unwrap_or_else(|| panic!("no token at {index}"))
10064            .roles
10065            .iter()
10066            .map(render_role)
10067            .collect()
10068    }
10069
10070    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10071        parsed
10072            .flag_origins
10073            .iter()
10074            .find(|(f, _)| f.name == flag)
10075            .map(|(_, origins)| origins.clone())
10076            .unwrap_or_default()
10077    }
10078
10079    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10080        let env = env
10081            .iter()
10082            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10083            .collect();
10084        Parser::new(spec)
10085            .with_env(env)
10086            .explain(&input(words))
10087            .unwrap()
10088    }
10089
10090    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10091        explain_with_env(spec, words, &[])
10092    }
10093
10094    #[test]
10095    fn an_attached_long_value_is_recorded_on_the_flag_token() {
10096        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10097            .parse()
10098            .unwrap();
10099
10100        let parsed = explain(&spec, &["ex", "--env=prod"]);
10101
10102        assert_eq!(roles(&parsed, 0), ["program"]);
10103        assert_eq!(
10104            roles(&parsed, 1),
10105            ["flag env as --env", "value of env = [\"prod\"], attached"]
10106        );
10107        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
10108        // form while the detached one worked, and nothing could show the difference.
10109        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10110    }
10111
10112    #[test]
10113    fn a_detached_long_value_is_recorded_on_its_own_token() {
10114        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10115            .parse()
10116            .unwrap();
10117
10118        let parsed = explain(&spec, &["ex", "--env", "prod"]);
10119
10120        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10121        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10122    }
10123
10124    #[test]
10125    fn a_short_bundle_is_attributed_to_the_bundle_token() {
10126        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10127            .parse()
10128            .unwrap();
10129
10130        let parsed = explain(&spec, &["ex", "-abj8"]);
10131
10132        // One word the caller wrote, four things it did — and the re-queued tails are
10133        // folded back onto it rather than appearing as tokens nobody typed.
10134        assert_eq!(
10135            roles(&parsed, 1),
10136            [
10137                "flag a as -a",
10138                "flag b as -b",
10139                "flag j as -j",
10140                "value of j = [\"8\"], attached",
10141            ]
10142        );
10143        assert_eq!(parsed.tokens.len(), 2);
10144    }
10145
10146    #[test]
10147    fn a_delimiter_splits_one_token_into_several_values() {
10148        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
10149            .parse()
10150            .unwrap();
10151
10152        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
10153
10154        assert_eq!(
10155            roles(&parsed, 2),
10156            ["value of tags = [\"a\", \"b\", \"c\"]"],
10157            "the values meant, not the word typed"
10158        );
10159    }
10160
10161    #[test]
10162    fn a_separator_and_the_words_after_it_are_distinguished() {
10163        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
10164            .parse()
10165            .unwrap();
10166
10167        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
10168
10169        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
10170        assert_eq!(roles(&parsed, 2), ["separator"]);
10171        // Past the separator `-x` is data, not an unknown flag.
10172        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
10173    }
10174
10175    #[test]
10176    fn a_second_separator_is_data() {
10177        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
10178            .parse()
10179            .unwrap();
10180
10181        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
10182
10183        assert_eq!(roles(&parsed, 1), ["separator"]);
10184        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
10185    }
10186
10187    #[test]
10188    fn an_unknown_flag_says_what_took_it() {
10189        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10190            .parse()
10191            .unwrap();
10192
10193        let parsed = explain(&spec, &["ex", "--wat"]);
10194
10195        // The default is lax, so the word became data. Which is the useful thing to be
10196        // told: the alternative reading is "you have a typo".
10197        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
10198    }
10199
10200    #[test]
10201    fn a_subcommand_word_is_not_a_positional() {
10202        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
10203            .parse()
10204            .unwrap();
10205
10206        let parsed = explain(&spec, &["ex", "build", "a"]);
10207
10208        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
10209        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
10210    }
10211
10212    #[test]
10213    fn a_multicall_applet_is_read_at_argv0() {
10214        let spec: Spec =
10215            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
10216                .parse()
10217                .unwrap();
10218
10219        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
10220
10221        // argv[0] is both the program and the word that selected the applet, and the word
10222        // read there is not the word the caller wrote.
10223        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
10224        assert!(parsed.tokens[0].synthesized);
10225        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
10226    }
10227
10228    #[test]
10229    fn words_the_parse_never_reached_say_so() {
10230        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10231            .parse()
10232            .unwrap();
10233
10234        let parsed = Parser::new(&spec)
10235            .explain(&input(&["ex", "--help", "a"]))
10236            .unwrap();
10237
10238        assert_eq!(roles(&parsed, 2), ["unread"]);
10239    }
10240
10241    #[test]
10242    fn an_env_origin_names_the_variable_that_fired() {
10243        let spec: Spec =
10244            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
10245                .parse()
10246                .unwrap();
10247
10248        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
10249        assert_eq!(
10250            origins(&primary, "token"),
10251            [ValueOrigin::Env("EX_TOKEN".to_string())]
10252        );
10253
10254        // The fallback firing is a different fact from the primary firing, and which one it
10255        // was is what says which declaration to delete.
10256        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
10257        assert_eq!(
10258            origins(&fallback, "token"),
10259            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
10260        );
10261    }
10262
10263    #[test]
10264    fn a_default_origin_is_recorded_for_flags_and_args() {
10265        let spec: Spec =
10266            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
10267                .parse()
10268                .unwrap();
10269
10270        let parsed = explain(&spec, &["ex"]);
10271
10272        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
10273        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
10274        assert_eq!(arg.name, "src");
10275        assert_eq!(origins, &[ValueOrigin::Default]);
10276    }
10277
10278    #[test]
10279    fn a_default_if_origin_carries_the_condition_that_fired() {
10280        let spec: Spec = r#"
10281name "ex"
10282bin "ex"
10283flag "--profile <p>"
10284flag "--strict" {
10285    default_if "--profile" "prod" "true"
10286}
10287        "#
10288        .parse()
10289        .unwrap();
10290
10291        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
10292
10293        // The selector alone is ambiguous: several conditions may name it with different
10294        // `when` values, so the report has to say which one matched.
10295        assert_eq!(
10296            origins(&parsed, "strict"),
10297            [ValueOrigin::DefaultIf {
10298                selector: "--profile".to_string(),
10299                when: Some("prod".to_string()),
10300            }]
10301        );
10302    }
10303
10304    #[test]
10305    fn a_bare_optional_value_flag_records_default_missing() {
10306        let spec: Spec =
10307            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
10308                .parse()
10309                .unwrap();
10310
10311        let parsed = explain(&spec, &["ex", "--color", "-v"]);
10312
10313        // The flag was typed and the value was not, which is the distinction a spec author
10314        // is asking about when they ask why `--color` came out `always`.
10315        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
10316        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10317        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
10318    }
10319
10320    #[test]
10321    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
10322        let spec: Spec =
10323            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
10324                .parse()
10325                .unwrap();
10326
10327        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
10328
10329        // Why origins are a list: one declaration, two occurrences, two different answers.
10330        assert_eq!(
10331            roles(&parsed, 1),
10332            [
10333                "flag color as --color",
10334                "value of color = [\"red\"], attached"
10335            ]
10336        );
10337        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10338    }
10339
10340    #[test]
10341    fn an_override_names_the_flag_that_did_it() {
10342        let spec: Spec =
10343            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
10344                .parse()
10345                .unwrap();
10346
10347        let parsed = explain(&spec, &["ex", "--loud"]);
10348
10349        // Without the overriding name, "`--quiet` is unset despite its default" has no
10350        // answer: the fallback phase silently declines to fill an overridden flag.
10351        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
10352        assert!(origins(&parsed, "quiet").is_empty());
10353    }
10354
10355    #[test]
10356    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
10357        let spec: Spec = r#"
10358name "ex"
10359bin "ex"
10360cmd "run" restart_token=":::" {
10361    arg "<task>" default="build"
10362}
10363        "#
10364        .parse()
10365        .unwrap();
10366
10367        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
10368
10369        // The values belong to the last invocation, so provenance must too — but the words
10370        // of the first were still read, and a report that dropped them would show a command
10371        // line with a hole in it.
10372        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
10373        // And the token that did the resetting says so: without a role of its own it reads
10374        // as a word that did nothing, next to a `lint` that filled an arg now empty.
10375        assert_eq!(roles(&parsed, 3), ["restart"]);
10376        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
10377        assert!(parsed.arg_origins.is_empty());
10378    }
10379
10380    #[test]
10381    fn a_value_terminator_says_which_run_it_ended() {
10382        let spec: Spec = r#"
10383name "ex"
10384bin "ex"
10385flag "--exec <cmd>..." value_terminator=";"
10386arg "<src>"
10387        "#
10388        .parse()
10389        .unwrap();
10390
10391        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
10392
10393        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
10394        // The terminator is consumed and is not one of the values, which is the whole reason
10395        // it was declared — so it needs a row saying that rather than an empty one.
10396        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
10397        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
10398    }
10399
10400    #[test]
10401    fn an_args_value_terminator_says_which_run_it_ended() {
10402        let spec: Spec = r#"
10403name "ex"
10404bin "ex"
10405arg "<files>..." value_terminator=";"
10406arg "[dest]"
10407        "#
10408        .parse()
10409        .unwrap();
10410
10411        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
10412
10413        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
10414        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
10415        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
10416    }
10417
10418    #[test]
10419    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
10420        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
10421            .parse()
10422            .unwrap();
10423
10424        let parsed = Parser::new(&spec)
10425            .explain(&input(&["ex", "--env=prod"]))
10426            .unwrap();
10427
10428        // `parse` reports "missing required <src>" and nothing else, which is the report the
10429        // caller already had. This is the case the whole thing exists for.
10430        assert!(Parser::new(&spec)
10431            .parse(&input(&["ex", "--env=prod"]))
10432            .is_err());
10433        assert_eq!(
10434            roles(&parsed, 1),
10435            ["flag env as --env", "value of env = [\"prod\"], attached"]
10436        );
10437        assert!(
10438            parsed.errors.iter().any(|e| e.to_string().contains("src")),
10439            "{:?}",
10440            parsed.errors
10441        );
10442    }
10443
10444    #[test]
10445    fn an_external_subcommand_forwards_whole_tokens() {
10446        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
10447            .parse()
10448            .unwrap();
10449
10450        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
10451
10452        assert_eq!(roles(&parsed, 1), ["external"]);
10453        assert_eq!(roles(&parsed, 2), ["external"]);
10454    }
10455
10456    #[test]
10457    fn a_view_keeps_the_callers_argv_positions() {
10458        let spec: Spec = r#"
10459bin "ex"
10460view "runner" root="run"
10461cmd "run" {
10462    flag "--token <token>"
10463}
10464        "#
10465        .parse()
10466        .unwrap();
10467
10468        let parsed = explain(&spec, &["runner", "--token", "secret"]);
10469
10470        // A view re-enters the parse with the same argv, so the positions still mean what
10471        // the caller wrote.
10472        assert_eq!(roles(&parsed, 0), ["program"]);
10473        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10474    }
10475}