Skip to main content

usage/
parse.rs

1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27///
28/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
29/// command describes another program, which does not accept the mounting CLI's globals.
30/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
31/// re-parses them), but the mounted command's own flags take precedence over them, so its
32/// choices/completions are not replaced by a global's. Which flags a completion may offer
33/// there is a separate question, answered by [`ParseOutput::completion_flags`].
34fn merge_subcommand_flags(
35    available: &mut BTreeMap<String, Arc<SpecFlag>>,
36    new_flags: BTreeMap<String, Arc<SpecFlag>>,
37    crossing_mount: bool,
38) {
39    // Keep only inherited global flags from the parent.
40    available.retain(|_, f| f.global);
41
42    if crossing_mount {
43        // A mounted command owns its flags outright, including names an inherited global also
44        // uses: a word after the mounted command belongs to the mounted program. Words before
45        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
46        // mounted command does not declare (e.g. a global's short) stay inherited.
47        for (key, flag) in new_flags {
48            available.insert(key, flag);
49        }
50        return;
51    }
52
53    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
54    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
55    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56    // Maps each merged flag produced below back to the inherited global it was merged from, so
57    // the collision check can compare *origins*: a flag this loop already merged is not a
58    // different global, even though it is a different `Arc`.
59    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
61    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62        let ptr = Arc::as_ptr(flag) as usize;
63        *merged_origin.get(&ptr).unwrap_or(&ptr)
64    }
65
66    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
67    // map's existing intra-subcommand collision resolution: when two flags in the same command
68    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
69    // to its last-declared owner, and we must not change which flag owns it.
70    for (key, flag) in new_flags {
71        if flag.global {
72            // A child that re-declares (or adds) a global flag stays recognized everywhere.
73            available.insert(key, flag);
74            continue;
75        }
76
77        // A non-global re-declaration that shares a LONG name with an inherited global flag is
78        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
79        // global). Keep the global flag (global precedence, so it survives the next descent's
80        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
81        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
82        // deliberate: a re-declaration sharing only a short letter with an unrelated global
83        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
84        // handled by the `contains_key` skip below instead.
85        let inherited_global = flag.long.iter().find_map(|l| {
86            available
87                .get(&format!("--{l}"))
88                .filter(|f| f.global)
89                .cloned()
90        });
91        if let Some(global_flag) = inherited_global {
92            // Never clobber a *different* inherited global's alias. If this re-declaration's
93            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
94            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
95            // precedence dictates, instead of stealing the alias for the merged flag.
96            //
97            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
98            // earlier key of this same child already replaced some of them with the merged flag,
99            // which the lookups above may now resolve to. That is the same logical flag, so it
100            // must not read as a collision and leave this key on the pre-merge global.
101            let global_origin = origin_of(&merged_origin, &global_flag);
102            if available.get(&key).is_some_and(|existing| {
103                existing.global && origin_of(&merged_origin, existing) != global_origin
104            }) {
105                continue;
106            }
107            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108                Some(merged) => merged.clone(),
109                None => {
110                    let mut merged = (*global_flag).clone();
111                    // `exclusive` is deliberately *not* reconciled here, in either direction.
112                    // One object now answers to two alias sets that may disagree: the child
113                    // owns the spellings it declared, the ancestor keeps the ones only it
114                    // declared. A single bool cannot hold both, so the merged flag carries the
115                    // ancestor's and validation resolves the occurrence by the spelling that
116                    // was typed — the ledger it already consults to decide whether selecting
117                    // the child is company.
118                    for s in &flag.short {
119                        if !merged.short.contains(s) {
120                            merged.short.push(*s);
121                        }
122                    }
123                    for l in &flag.long {
124                        if !merged.long.contains(l) {
125                            merged.long.push(l.clone());
126                        }
127                    }
128                    // A child may deliberately promote one of the ancestor's hidden aliases.
129                    // Hidden lists are subsets of the accepted spellings, so a spelling present
130                    // on the child but absent from its hidden subset is visible at this level.
131                    merged.hidden_short_aliases.retain(|alias| {
132                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133                    });
134                    merged.hidden_aliases.retain(|alias| {
135                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136                    });
137                    for s in &flag.hidden_short_aliases {
138                        if !merged.hidden_short_aliases.contains(s) {
139                            merged.hidden_short_aliases.push(*s);
140                        }
141                    }
142                    for l in &flag.hidden_aliases {
143                        if !merged.hidden_aliases.contains(l) {
144                            merged.hidden_aliases.push(l.clone());
145                        }
146                    }
147                    let merged = Arc::new(merged);
148                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150                    // Rebind the global's *other* aliases onto the merged flag. The loop only
151                    // visits keys the child declared, so an alias the child left out (the `-y` of
152                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
153                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
154                    // logical flag must be one object under every key it answers to.
155                    for existing in available.values_mut() {
156                        if origin_of(&merged_origin, existing) == global_origin {
157                            *existing = Arc::clone(&merged);
158                        }
159                    }
160                    merged
161                }
162            };
163            available.insert(key, merged);
164            continue;
165        }
166
167        // Purely-local flag (shares nothing with an inherited global), or one that collides only
168        // on a short with an unrelated global. Insert this alias but never shadow an inherited
169        // global flag. Such non-global flags are dropped by the next descent's `retain`.
170        if available.contains_key(&key) {
171            continue;
172        }
173        available.insert(key, flag);
174    }
175}
176
177/// Build the lookup keys a flag is registered under in `available_flags`:
178/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
179fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180    let mut keys: Vec<String> = flag
181        .long
182        .iter()
183        .map(|l| format!("--{l}"))
184        .chain(flag.short.iter().map(|s| format!("-{s}")))
185        .collect();
186    if let Some(negate) = &flag.negate {
187        keys.push(negate.clone());
188    }
189    keys
190}
191
192/// The flags a command declares, keyed by each of their aliases.
193fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194    cmd.flags
195        .iter()
196        .chain(cmd.clause.iter().flat_map(|clause| &clause.flags))
197        .flat_map(|f| {
198            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
199            flag_keys(&f)
200                .into_iter()
201                .map(|key| (key, Arc::clone(&f)))
202                .collect::<Vec<_>>()
203        })
204        .collect()
205}
206
207fn unique_flags<'a>(
208    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
209) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
210    let mut seen = HashSet::new();
211    flags
212        .into_iter()
213        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
214}
215
216/// Every flag a command accepts, resolved the way parsing an invocation of it
217/// resolves them.
218///
219/// `chain` runs from the root command (`spec.cmd`) down to the command in
220/// question; an empty chain yields no flags.
221///
222/// This is not "the command's flags plus its ancestors' globals". A subcommand
223/// that re-declares a global's long name is describing the *same* flag rather
224/// than a new one, so the global's help, argument and effect survive and only
225/// the re-declaration's extra aliases are added — see
226/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
227/// going through this will disagree with what the parser actually accepts.
228pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
229    let Some((root, rest)) = chain.split_first() else {
230        return vec![];
231    };
232    let mut available = gather_flags(root);
233    for cmd in rest {
234        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
235    }
236
237    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
238    // global that has both a short and a long, the merged flag is written under
239    // the long key while the short key keeps pointing at the pre-merge `Arc` —
240    // two objects for one logical flag. That is harmless for parsing, which
241    // looks flags up by key, but a caller listing flags would see it twice.
242    //
243    // Names break the tie because a long key always sorts before a short one
244    // (`--x` < `-y` at the second byte), so the merged declaration is the one
245    // reached first. Two genuinely distinct flags sharing a name is a spec bug
246    // that `usage lint` reports as a duplicate flag.
247    let mut seen_names = HashSet::new();
248    unique_flags(available.values())
249        .filter(|f| seen_names.insert(f.name.clone()))
250        .cloned()
251        .collect()
252}
253
254/// Extract the flag key from a flag word for lookup in available_flags map
255/// Handles both long flags (--flag, --flag=value) and short flags (-f)
256fn get_flag_key(word: &str) -> &str {
257    if word.starts_with("--") {
258        // Long flag: strip =value if present
259        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
260    } else if let Some((end, _)) = word.char_indices().nth(2) {
261        // Short flag: the dash and one letter, which is one character and not
262        // necessarily one byte.
263        &word[0..end]
264    } else {
265        word
266    }
267}
268
269/// Where a value came from, when it did not come from the command line.
270///
271/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
272/// token for the flag and none for the value, and that distinction is the whole question
273/// a spec author is asking when they ask why `--color` came out `always`. Values that were
274/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
275#[derive(Debug, Clone, PartialEq, Eq)]
276#[non_exhaustive]
277pub enum ValueOrigin {
278    /// A flag that takes a value was given without one, so the declaration supplied it:
279    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
280    /// One variant for both, because from argv's side the same thing happened — the flag
281    /// was typed and the value was not.
282    DefaultMissing,
283    /// An environment variable, named.
284    ///
285    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
286    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
287    /// not say which declaration fired or which one to delete.
288    Env(String),
289    /// A declared `default`, on the flag or on the flag's argument.
290    ///
291    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
292    /// a fact about the value, and `usage lint` is the place to complain about declaring
293    /// both.
294    Default,
295    /// A `default_if` whose condition matched, with the condition that decided it. The
296    /// selector alone is ambiguous — several conditions may name it with different `when`
297    /// values.
298    DefaultIf {
299        selector: String,
300        when: Option<String>,
301    },
302}
303
304/// What one word of the command line became.
305///
306/// Several because a single token can do more than one thing: `-abc` sets three flags,
307/// `-j8` is a flag and its value.
308#[derive(Debug, Clone)]
309#[non_exhaustive]
310pub enum TokenRole {
311    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
312    Program,
313    /// Selected a subcommand.
314    Command { name: String },
315    /// Named a flag, in this spelling. `negated` for the `negate` form.
316    Flag {
317        flag: Arc<SpecFlag>,
318        spelling: String,
319        negated: bool,
320    },
321    /// Supplied a flag's value. Several values when a `delimiter` split the word.
322    Value {
323        flag: Arc<SpecFlag>,
324        values: Vec<String>,
325        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
326        /// rather than following it as its own word.
327        attached: bool,
328    },
329    /// Filled a positional argument. Several values when a `delimiter` split the word.
330    Arg {
331        arg: Arc<SpecArg>,
332        values: Vec<String>,
333    },
334    /// An explicit `--`, consumed as a separator.
335    Separator,
336    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
337    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
338    /// without a role the word reads as having done nothing while a whole help page arrives
339    /// in the error list.
340    Builtin { spelling: String },
341    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
342    /// declaration whose run it closed — the word is not one of that run's values, which is
343    /// the whole reason it was declared.
344    ValueTerminator { ends: String },
345    /// A declared `restart_token`: the positional cursor and the values it had filled start
346    /// over here. Recorded because the words before it are still in the report, and without
347    /// this row they look like they filled arguments that then came back empty.
348    Restart,
349    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
350    /// under `unknown_flags="value"`, and `None` when the word was refused.
351    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
352    /// The word reached a declaration that would not take it, and was dropped. Without this
353    /// the token reads as having done nothing, which is the one thing it did not do.
354    Refused { reason: String },
355    /// Forwarded to an external subcommand.
356    External,
357    /// The parser stopped before this word — a help request, a refused value.
358    Unread,
359    /// Filled a sigil-classified positional after removing its declared prefix.
360    Sigil {
361        arg: Arc<SpecArg>,
362        sigil: String,
363        values: Vec<String>,
364    },
365    /// Ended one instance of a repeatable clause and began the next.
366    ClauseSeparator { name: String },
367}
368
369/// One word of the command line, and what it became.
370#[derive(Debug, Clone)]
371#[non_exhaustive]
372pub struct TokenBinding {
373    /// Position in the argv slice the parse was given, argv[0] included.
374    pub index: usize,
375    pub word: String,
376    /// Roles a word the parser made up contributed, folded onto the token it was derived
377    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
378    /// argv[0]. `word` is what the caller wrote, not what the parser read.
379    pub synthesized: bool,
380    pub roles: Vec<TokenRole>,
381}
382
383#[non_exhaustive]
384pub struct ParseOutput {
385    pub cmd: SpecCommand,
386    pub cmds: Vec<SpecCommand>,
387    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
388    /// Separator-delimited positional instances, keyed by clause name.
389    pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
390    /// Per-instance flags belonging to a repeatable clause, keyed by clause name.
391    pub clause_flags: IndexMap<String, Vec<IndexMap<Arc<SpecFlag>, ParseValue>>>,
392    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
393    /// What each word of the command line became, in argv order, one entry per word.
394    ///
395    /// The token half of provenance; [`ParseOutput::flag_origins`] and
396    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
397    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
398    /// a token that bound to nothing, so both exist.
399    pub tokens: Vec<TokenBinding>,
400    /// Where a flag's value came from when it did not come from argv, in the order the
401    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
402    ///
403    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
404    /// `default_missing` value, so one flag can have several.
405    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
406    /// Where an argument's value came from when it did not come from argv. Keyed as
407    /// [`ParseOutput::args`] is.
408    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
409    /// Flags a later occurrence removed, and the flag that removed them.
410    ///
411    /// The overriding name is the half a caller needs: the fallback phase silently declines
412    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
413    /// otherwise" has no answer without it.
414    pub overridden_flags: BTreeMap<String, String>,
415    /// Every flag the parser recognizes at this point, keyed by each of its aliases
416    /// (`--long`, `-s`, negations).
417    ///
418    /// This includes flags that only remain recognized because they may appear *before* a
419    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
420    /// should offer.
421    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
422    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
423    pub errors: Vec<UsageErr>,
424    /// Deprecated declarations this command line used, for the caller to render when its
425    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
426    /// not used anything yet.
427    pub warnings: Vec<Warning>,
428    /// The positional argument the next word would have filled, i.e. where the parser's
429    /// cursor stopped. `None` once every argument is satisfied.
430    ///
431    /// Completions need exactly this: the parser already accounts for `var_max`, for
432    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
433    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
434    pub next_arg: Option<Arc<SpecArg>>,
435    /// Whether an explicit `--` was consumed *as a separator*.
436    ///
437    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
438    /// of the variadic argument collecting it, not a separator, so it does not unlock a
439    /// `double_dash="required"` argument.
440    pub double_dash_seen: bool,
441    /// Remaining argv captured when an unmatched word was forwarded as an external
442    /// subcommand: the command name first, then every token after it.
443    ///
444    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
445    pub external: Option<Vec<String>>,
446}
447
448impl ParseOutput {
449    /// The flags a completion should offer for the parsed command.
450    ///
451    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
452    /// command has been reached, though, the commands above it belong to the mounting CLI and
453    /// their flags are not accepted there — mise, for example, forwards everything after a task
454    /// name to the task itself — so only the flags declared from the mount boundary down are
455    /// offered. Those globals stay in `available_flags` because they may legitimately appear
456    /// *before* the mounted command.
457    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
458        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
459            return self.available_flags.clone();
460        };
461        // A mount can also merge flags from its spec's root into the command it is mounted on
462        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
463        // replay starts one level up to inherit its globals.
464        let start = match boundary.checked_sub(1) {
465            Some(prev) if self.cmds[prev].flags_from_mount => prev,
466            _ => boundary,
467        };
468        // Re-run the descent from there, which starts with no inherited flags. Below the
469        // boundary the mounted program's commands are ordinary commands, so the descents use
470        // the same merge as the real parse.
471        let mut offered = gather_flags(&self.cmds[start]);
472        for cmd in &self.cmds[start + 1..] {
473            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
474        }
475        offered
476    }
477}
478
479#[derive(Debug, Clone)]
480pub enum ParseValue {
481    Bool(bool),
482    String(String),
483    MultiBool(Vec<bool>),
484    MultiString(Vec<String>),
485}
486
487impl ParseValue {
488    pub fn try_as_bool(self) -> Option<bool> {
489        match self {
490            Self::Bool(value) => Some(value),
491            _ => None,
492        }
493    }
494
495    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
496        match self {
497            Self::Bool(value) => Some(value),
498            _ => None,
499        }
500    }
501
502    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
503        match self {
504            Self::Bool(value) => Some(value),
505            _ => None,
506        }
507    }
508
509    pub fn try_as_string(self) -> Option<String> {
510        match self {
511            Self::String(value) => Some(value),
512            _ => None,
513        }
514    }
515
516    pub const fn try_as_string_ref(&self) -> Option<&String> {
517        match self {
518            Self::String(value) => Some(value),
519            _ => None,
520        }
521    }
522
523    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
524        match self {
525            Self::String(value) => Some(value),
526            _ => None,
527        }
528    }
529
530    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
531        match self {
532            Self::MultiBool(value) => Some(value),
533            _ => None,
534        }
535    }
536
537    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
538        match self {
539            Self::MultiBool(value) => Some(value),
540            _ => None,
541        }
542    }
543
544    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
545        match self {
546            Self::MultiBool(value) => Some(value),
547            _ => None,
548        }
549    }
550
551    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
552        match self {
553            Self::MultiString(value) => Some(value),
554            _ => None,
555        }
556    }
557
558    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
559        match self {
560            Self::MultiString(value) => Some(value),
561            _ => None,
562        }
563    }
564
565    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
566        match self {
567            Self::MultiString(value) => Some(value),
568            _ => None,
569        }
570    }
571}
572
573/// The deprecated declarations argv itself named: the commands it descended through, and the
574/// flags it bound.
575///
576/// Called before the environment and defaults have filled anything, because afterwards nothing
577/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
578/// differently, at the point where each is applied.
579///
580/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
581/// including `--help`, and the compiled parser reports selected commands rather than the one the
582/// process already is.
583fn collect_deprecations(out: &mut ParseOutput) {
584    for cmd in out.cmds.iter().skip(1) {
585        if cmd.deprecated.is_none()
586            && cmd.deprecated_warn_at.is_none()
587            && cmd.deprecated_remove_at.is_none()
588        {
589            continue;
590        }
591        out.warnings.push(Warning::command(
592            cmd.name.clone(),
593            cmd.deprecated.clone(),
594            cmd.deprecated_warn_at.clone(),
595            cmd.deprecated_remove_at.clone(),
596        ));
597    }
598    for flag in out.flags.keys() {
599        if let Some(warning) = flag_deprecation(flag) {
600            out.warnings.push(warning);
601        }
602    }
603}
604
605/// A warning for a flag that was used, if its declaration is deprecated at all.
606fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
607    if flag.deprecated.is_none()
608        && flag.deprecated_warn_at.is_none()
609        && flag.deprecated_remove_at.is_none()
610    {
611        return None;
612    }
613    Some(Warning::flag(
614        flag_spelling(flag),
615        flag.deprecated.clone(),
616        flag.deprecated_warn_at.clone(),
617        flag.deprecated_remove_at.clone(),
618    ))
619}
620
621/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
622/// about `old-flag` would be about a word nobody typed.
623fn flag_spelling(flag: &SpecFlag) -> String {
624    flag.long
625        .first()
626        .map(|long| format!("--{long}"))
627        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
628        .unwrap_or_else(|| flag.name.clone())
629}
630
631/// The name this flag reads first, which is what to use instead of a deprecated alias.
632fn flag_current_env(flag: &SpecFlag) -> Option<String> {
633    flag.env
634        .clone()
635        .or_else(|| flag.env_fallback.first().cloned())
636}
637
638fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
639    flag.deprecated_env.iter().any(|declared| declared == name)
640}
641
642/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
643fn arg_current_env(arg: &SpecArg) -> Option<String> {
644    arg.env
645        .clone()
646        .or_else(|| arg.env_fallback.first().cloned())
647}
648
649fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
650    arg.deprecated_env.iter().any(|declared| declared == name)
651}
652
653/// The first of `names` that is set, and which one it was.
654///
655/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
656/// aliases, so the winner's identity is what says whether a value arrived through an alias.
657/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
658/// disagree with it.
659fn first_set_env<'a>(
660    mut names: impl Iterator<Item = &'a str>,
661    get_env: &impl Fn(&str) -> Option<String>,
662) -> Option<(&'a str, String)> {
663    names.find_map(|name| get_env(name).map(|value| (name, value)))
664}
665
666/// Builder for parsing command-line arguments with custom options.
667///
668/// Use this when you need to customize parsing behavior, such as providing
669/// a custom environment variable map instead of using the process environment.
670///
671/// # Example
672/// ```
673/// use std::collections::HashMap;
674/// use usage::Spec;
675/// use usage::parse::Parser;
676///
677/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
678/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
679///
680/// let result = Parser::new(&spec)
681///     .with_env(env)
682///     .parse(&["cmd".into()])
683///     .unwrap();
684/// ```
685#[non_exhaustive]
686pub struct Parser<'a> {
687    spec: &'a Spec,
688    env: Option<HashMap<String, String>>,
689    mount_outputs: Option<HashMap<String, String>>,
690}
691
692impl<'a> Parser<'a> {
693    /// Create a new parser for the given spec.
694    pub fn new(spec: &'a Spec) -> Self {
695        Self {
696            spec,
697            env: None,
698            mount_outputs: None,
699        }
700    }
701
702    /// Use a custom environment variable map instead of the process environment.
703    ///
704    /// This is useful when parsing for tasks in a monorepo where the env vars
705    /// come from a child config file rather than the current process environment.
706    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
707        self.env = Some(env);
708        self
709    }
710
711    /// Inject deterministic outputs for mount commands instead of executing them.
712    ///
713    /// Keys are the exact `run` strings declared by mount nodes and values are the
714    /// usage specs those commands would print. When this is set, every encountered
715    /// mount must have an entry. Production parsing remains process-backed unless a
716    /// caller explicitly opts into injection.
717    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
718        self.mount_outputs = Some(outputs);
719        self
720    }
721
722    /// Parse the input arguments.
723    ///
724    /// Returns the parsed arguments and flags, with defaults and env vars applied.
725    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
726        let out = self.parse_collecting(input)?;
727        if let Some(err) = out
728            .errors
729            .iter()
730            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
731        {
732            bail!("{err}");
733        }
734        if !out.errors.is_empty() {
735            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
736        }
737        Ok(out)
738    }
739
740    /// Everything the parse learned, whether or not it succeeded.
741    ///
742    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
743    /// caller about to act on a command line. A caller that wants to *explain* one wants
744    /// the opposite: the bindings that worked and every complaint about the rest, since a
745    /// report saying only "missing required <src>" is the report you already had.
746    ///
747    /// Failures that stop the parse dead — a mount that will not run, a word no
748    /// declaration can take — still come back as `Err`. There is no output to describe in
749    /// those cases; see [`Parser::explain`] for what to do about it.
750    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
751        self.parse_collecting(input)
752    }
753
754    /// The binding phase's own answer for a line [`Parser::explain`] refused.
755    ///
756    /// `Ok` when the binding phase finished and the failure came after it — a flag left
757    /// waiting for a value, say. Everything argv supplied is there and only the
758    /// environment-and-defaults phase is missing.
759    ///
760    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
761    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
762    /// is more useful next to the three tokens that did bind than on its own. The word that
763    /// caused the failure carries a role saying so, and everything still queued behind it is
764    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
765    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
766    /// than in the line, such as a mount that will not run, stops the trace where it stopped
767    /// and the words past it carry no role.
768    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
769        let mut trace = Trace::new(input);
770        match parse_partial_traced(
771            self.spec,
772            input,
773            self.env.as_ref(),
774            self.mount_outputs.as_ref(),
775            MountTiming::WhenAWordIsUnknown,
776            true,
777            &mut trace,
778        ) {
779            // A parse that got as far as stopping normally already moved its tokens onto the
780            // output, which is where a caller should read them from.
781            Ok((out, _)) => Ok(out),
782            Err(_) => Err(trace.tokens),
783        }
784    }
785
786    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
787        let custom_env = self.env.as_ref();
788        let (mut out, overridden_flags) = parse_partial_with_env(
789            self.spec,
790            input,
791            custom_env,
792            self.mount_outputs.as_ref(),
793            MountTiming::WhenAWordIsUnknown,
794            false,
795        )?;
796        restore_current_clause(&mut out);
797        trace!("{out:?}");
798
799        // A flag still waiting for a value never got one, so the command line ended
800        // mid-flag. `parse_partial` leaves this for completions to look at — a
801        // half-typed `--jobs ` is exactly what a completion is asked about — but a
802        // full parse has nothing left to wait for, and dropping the flag silently
803        // made a forgotten value look like a working command.
804        while try_bind_default_missing(
805            &mut out.flags,
806            &mut out.flag_awaiting_value,
807            custom_env,
808            &mut out.flag_origins,
809        )? {}
810        if let Some(flag) = out.flag_awaiting_value.first() {
811            let token = flag
812                .long
813                .first()
814                .map(|l| format!("--{l}"))
815                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
816                .unwrap_or_else(|| flag.name.clone());
817            let rendered = input.join(" ");
818            let span = rendered
819                .rfind(&token)
820                .map(|at| (at, token.len()))
821                .unwrap_or((0, 0));
822            return Err(UsageErr::InvalidFlag {
823                token,
824                reason: "requires an argument".to_string(),
825                span: span.into(),
826                input: rendered,
827            }
828            .into());
829        }
830
831        // Before the environment and defaults have their turn, because both mark a field as
832        // filled and only argv can be reported as something the user typed. Env is reported
833        // where it is applied, below; a default is nobody's request and reports nothing.
834        collect_deprecations(&mut out);
835
836        let get_env = |key: &str| -> Option<String> {
837            if let Some(env_map) = custom_env {
838                env_map.get(key).cloned()
839            } else {
840                std::env::var(key).ok()
841            }
842        };
843
844        // Apply env vars and defaults for args
845        //
846        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
847        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
848        for arg in active_args(&out.cmd) {
849            // Clause instances contain argv only: defaults and environment values do not
850            // manufacture fields inside a repeated group.
851            if out.cmd.clause.is_some() {
852                break;
853            }
854            if out.args.contains_key(arg) {
855                continue;
856            }
857            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
858                if arg_env_is_deprecated(arg, env_name) {
859                    out.warnings
860                        .push(Warning::env(env_name, arg_current_env(arg)));
861                }
862                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
863                validate_choice_values(
864                    ChoiceTarget::arg(arg),
865                    &values,
866                    arg.choices.as_ref(),
867                    custom_env,
868                )?;
869                let parsed = if arg.var {
870                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
871                    ParseValue::MultiString(values)
872                } else {
873                    ParseValue::String(values.into_iter().next().unwrap_or_default())
874                };
875                out.args.insert(Arc::new(arg.clone()), parsed);
876                out.arg_origins
877                    .entry(Arc::new(arg.clone()))
878                    .or_default()
879                    .push(ValueOrigin::Env(env_name.to_string()));
880                continue;
881            }
882            if !arg.default.is_empty() {
883                // Consider var when deciding the type of default return value
884                if arg.var {
885                    let values = split_fallback_values(&arg.default, arg.delimiter);
886                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
887                    validate_choice_values(
888                        ChoiceTarget::arg(arg),
889                        &values,
890                        arg.choices.as_ref(),
891                        custom_env,
892                    )?;
893                    // For var=true, always return a vec (MultiString)
894                    out.args
895                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
896                    out.arg_origins
897                        .entry(Arc::new(arg.clone()))
898                        .or_default()
899                        .push(ValueOrigin::Default);
900                } else {
901                    validate_choice_value(
902                        ChoiceTarget::arg(arg),
903                        &arg.default[0],
904                        arg.choices.as_ref(),
905                        custom_env,
906                    )?;
907                    // For var=false, return the first default value as String
908                    out.args.insert(
909                        Arc::new(arg.clone()),
910                        ParseValue::String(arg.default[0].clone()),
911                    );
912                    out.arg_origins
913                        .entry(Arc::new(arg.clone()))
914                        .or_default()
915                        .push(ValueOrigin::Default);
916                }
917            }
918        }
919
920        // Environment first, for every flag, so a `default_if` can see a sibling
921        // that was filled from env. Applying both in one pass would make the
922        // answer depend on declaration order: `--bin-names` before `--json`
923        // would miss `EX_JSON=1`.
924        let flags: Vec<Arc<SpecFlag>> = out
925            .available_flags
926            .values()
927            .filter(|flag| !is_clause_scoped_flag(&out, flag))
928            .cloned()
929            .collect();
930        for flag in &flags {
931            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
932                continue;
933            }
934            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
935                // The flag's own deprecation before the alias's, which is the order the
936                // compiled parser reports them in: it walks a command's flags and then its
937                // aliases. Using a deprecated flag through a variable is still using it.
938                if let Some(warning) = flag_deprecation(flag) {
939                    out.warnings.push(warning);
940                }
941                if flag_env_is_deprecated(flag, env_name) {
942                    out.warnings
943                        .push(Warning::env(env_name, flag_current_env(flag)));
944                }
945                if let Some(arg) = flag.arg.as_ref() {
946                    let values =
947                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
948                    validate_choice_values(
949                        ChoiceTarget::option(flag),
950                        &values,
951                        arg.choices.as_ref(),
952                        custom_env,
953                    )?;
954                    let parsed = if flag.var || arg.var {
955                        if flag.var {
956                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
957                        }
958                        if arg.var {
959                            validate_flag_arg_fallback_count(
960                                flag,
961                                arg,
962                                values.len(),
963                                &mut out.errors,
964                            );
965                        }
966                        ParseValue::MultiString(values)
967                    } else {
968                        ParseValue::String(values.into_iter().next().unwrap_or_default())
969                    };
970                    out.flags.insert(Arc::clone(flag), parsed);
971                } else {
972                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
973                    out.flags
974                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
975                }
976                out.flag_origins
977                    .entry(Arc::clone(flag))
978                    .or_default()
979                    .push(ValueOrigin::Env(env_name.to_string()));
980            }
981        }
982        // Decide every `default_if` against argv+env only. Binding as we go would put
983        // a default into `out.flags` and make the next flag's condition treat it as
984        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
985        // here, so an unconditional `default` on `--json` must not fire
986        // `default_if "--json"`.
987        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
988        for flag in &flags {
989            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
990                continue;
991            }
992            if let Some(condition) = flag.default_if.iter().find(|condition| {
993                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
994            }) {
995                from_default_if.push((Arc::clone(flag), condition.clone()));
996            }
997        }
998        for (flag, condition) in &from_default_if {
999            // The whole condition, not just the value: several conditions may name the same
1000            // selector with different `when` values, so the selector alone does not say
1001            // which one fired.
1002            bind_flag_fallback(
1003                flag,
1004                std::slice::from_ref(&condition.value),
1005                &mut out,
1006                custom_env,
1007                ValueOrigin::DefaultIf {
1008                    selector: condition.selector.clone(),
1009                    when: condition.when.clone(),
1010                },
1011            )?;
1012        }
1013        for flag in &flags {
1014            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1015                continue;
1016            }
1017            if !flag.default.is_empty() {
1018                bind_flag_fallback(
1019                    flag,
1020                    &flag.default,
1021                    &mut out,
1022                    custom_env,
1023                    ValueOrigin::Default,
1024                )?;
1025                continue;
1026            }
1027            if let Some(arg) = flag.arg.as_ref() {
1028                if !arg.default.is_empty() {
1029                    bind_flag_fallback(
1030                        flag,
1031                        &arg.default,
1032                        &mut out,
1033                        custom_env,
1034                        ValueOrigin::Default,
1035                    )?;
1036                }
1037            }
1038        }
1039        // The binding phase leaves the last clause instance in `args`/`flags` so
1040        // completion can inspect it. A full parse closes it before applying scoped
1041        // fallbacks: defaults and environment values fill instances that argv made,
1042        // but never manufacture a repeated group on their own.
1043        finalize_current_clause(&mut out);
1044        let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1045        validate_clause_relationships(
1046            &mut out,
1047            &overridden_flags,
1048            custom_env,
1049            clause_explicit.as_deref(),
1050        );
1051        // Declarative value validation is deliberately post-binding. Defaults and
1052        // environment fallbacks have landed by here, and delimiters were already split
1053        // while binding. Like clap's value parsers, a declaration judges each resulting
1054        // raw value independently.
1055        for (arg, parsed) in &out.args {
1056            validate_expression(
1057                &arg.name,
1058                arg.validate.as_deref(),
1059                arg.validate_error.as_deref(),
1060                parsed,
1061                &mut out.errors,
1062            );
1063        }
1064        if let Some(clause) = &out.cmd.clause {
1065            let mut clause_errors = Vec::new();
1066            for (index, instance) in out
1067                .clauses
1068                .get(&clause.name)
1069                .into_iter()
1070                .flatten()
1071                .enumerate()
1072            {
1073                for arg in &clause.args {
1074                    let Some(value) = instance.get(arg) else {
1075                        if arg.required {
1076                            clause_errors.push(UsageErr::MissingClauseArg {
1077                                clause: clause.name.clone(),
1078                                instance: index + 1,
1079                                arg: arg.name.clone(),
1080                            });
1081                        }
1082                        continue;
1083                    };
1084                    if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1085                        if let Some(min) = arg.var_min {
1086                            if values.len() < min {
1087                                clause_errors.push(UsageErr::VarArgTooFew {
1088                                    name: format!(
1089                                        "{} instance {}: {}",
1090                                        clause.name,
1091                                        index + 1,
1092                                        arg.name
1093                                    ),
1094                                    min,
1095                                    got: values.len(),
1096                                });
1097                            }
1098                        }
1099                        if let Some(max) = arg.var_max {
1100                            if values.len() > max {
1101                                clause_errors.push(UsageErr::VarArgTooMany {
1102                                    name: format!(
1103                                        "{} instance {}: {}",
1104                                        clause.name,
1105                                        index + 1,
1106                                        arg.name
1107                                    ),
1108                                    max,
1109                                    got: values.len(),
1110                                });
1111                            }
1112                        }
1113                    }
1114                }
1115            }
1116            out.errors.extend(clause_errors);
1117        }
1118        let clause_flag_names = out
1119            .cmd
1120            .clause
1121            .iter()
1122            .flat_map(|clause| &clause.flags)
1123            .map(|flag| flag.name.as_str())
1124            .collect::<HashSet<_>>();
1125        for (flag, parsed) in out
1126            .flags
1127            .iter()
1128            .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1129        {
1130            if let Some(arg) = &flag.arg {
1131                validate_expression(
1132                    &flag.name,
1133                    arg.validate.as_deref(),
1134                    arg.validate_error.as_deref(),
1135                    parsed,
1136                    &mut out.errors,
1137                );
1138            }
1139        }
1140        // Applied once, here, because this is where the CLI's own version is known: a
1141        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1142        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1143        Ok(out)
1144    }
1145}
1146
1147/// Parse command-line arguments according to a spec.
1148///
1149/// Returns the parsed arguments and flags, with defaults and env vars applied.
1150/// Uses `std::env::var` for environment variable lookups.
1151///
1152/// For custom environment variable handling, use [`Parser`] instead.
1153#[must_use = "parsing result should be used"]
1154pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1155    Parser::new(spec).parse(input)
1156}
1157
1158/// Parse command-line arguments without applying defaults.
1159///
1160/// Use this for help text generation or when you need the raw parsed values.
1161#[must_use = "parsing result should be used"]
1162pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1163    parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1164}
1165
1166/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1167/// `.exe` stripped so Windows and Unix agree.
1168pub fn multicall_basename(argv0: &str) -> &str {
1169    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1170    match name.get(name.len().saturating_sub(4)..) {
1171        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1172        _ => name,
1173    }
1174}
1175
1176/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1177///
1178/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1179/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1180pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1181    let base = multicall_basename(argv0);
1182    if !name.is_empty() && base == multicall_basename(name) {
1183        return None;
1184    }
1185    if let Some(bin) = bin {
1186        if !bin.is_empty() && base == multicall_basename(bin) {
1187            return None;
1188        }
1189    }
1190    Some(base)
1191}
1192
1193/// Internal version of parse_partial that accepts an optional custom env map.
1194/// When a command's own `mount` runs, for the root — which nothing descends into.
1195///
1196/// A completion has to know every command before it can offer one, even with
1197/// nothing typed yet, so it resolves up front. An execution knows the word it was
1198/// given, so it only pays for discovery when that word matches nothing declared —
1199/// and a CLI that declares its commands and mounts a few more does not spawn a
1200/// process on every invocation.
1201#[derive(Clone, Copy, PartialEq, Eq)]
1202enum MountTiming {
1203    Eager,
1204    WhenAWordIsUnknown,
1205}
1206
1207/// One word on its way through the parser, with what the parser has learned about it.
1208///
1209/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1210/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1211/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1212/// the word itself there is nothing to keep aligned. The argv position is here for the
1213/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1214/// removed from the middle, so position in the queue stops meaning position in argv on the
1215/// first descent.
1216struct Token {
1217    word: String,
1218    /// Where in the caller's argv this word came from.
1219    ///
1220    /// A word the parser made up points at the token it was derived from — the tail of a
1221    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1222    /// token a reader would point at, and there is nothing else to point at.
1223    argv: usize,
1224    /// The flag Phase 1 read this word as, and the command level it read it at.
1225    ///
1226    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1227    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1228    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1229    /// then the recognized flags have changed, because each descent drops the parent's
1230    /// non-global flags and a mounted command may declare the same name as a global seen
1231    /// here. Recording the owner keeps a word bound to the flag it was read as.
1232    ///
1233    /// The level matters to strict parsing: clap permits an inherited global once on each
1234    /// side of a subcommand boundary.
1235    binding: Option<(Arc<SpecFlag>, usize)>,
1236}
1237
1238impl Token {
1239    fn new(word: String, argv: usize) -> Self {
1240        Self {
1241            word,
1242            argv,
1243            binding: None,
1244        }
1245    }
1246}
1247
1248/// The token trace, while it is being built.
1249///
1250/// One row per word of the caller's argv, so a role can be recorded against a position
1251/// without the recorder having to know how many words came before it. Words the parser
1252/// made up have no row of their own and fold onto the row they were derived from.
1253struct Trace {
1254    tokens: Vec<TokenBinding>,
1255}
1256
1257impl Trace {
1258    fn new(input: &[String]) -> Self {
1259        Self {
1260            tokens: input
1261                .iter()
1262                .enumerate()
1263                .map(|(index, word)| TokenBinding {
1264                    index,
1265                    word: word.clone(),
1266                    synthesized: false,
1267                    roles: vec![],
1268                })
1269                .collect(),
1270        }
1271    }
1272
1273    fn record(&mut self, argv: usize, role: TokenRole) {
1274        if let Some(token) = self.tokens.get_mut(argv) {
1275            token.roles.push(role);
1276        }
1277    }
1278
1279    /// Note that what was read at this position is not what the caller wrote there.
1280    fn note_synthesized(&mut self, argv: usize) {
1281        if let Some(token) = self.tokens.get_mut(argv) {
1282            token.synthesized = true;
1283        }
1284    }
1285
1286    /// Every word the parse never reached, once it has stopped.
1287    fn close(&mut self, unread: &VecDeque<Token>) {
1288        for token in unread {
1289            self.record(token.argv, TokenRole::Unread);
1290        }
1291    }
1292}
1293
1294fn parse_partial_with_env(
1295    spec: &Spec,
1296    input: &[String],
1297    custom_env: Option<&HashMap<String, String>>,
1298    mount_outputs: Option<&HashMap<String, String>>,
1299    mount_timing: MountTiming,
1300    validate_clauses: bool,
1301) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1302    let mut trace = Trace::new(input);
1303    parse_partial_traced(
1304        spec,
1305        input,
1306        custom_env,
1307        mount_outputs,
1308        mount_timing,
1309        validate_clauses,
1310        &mut trace,
1311    )
1312}
1313
1314/// The binding phase, with the trace left somewhere the caller can still read it.
1315///
1316/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1317/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1318/// words read before the failure are most of what a report wants, so the caller owns the
1319/// trace instead and keeps them. See [`Parser::explain_refused`].
1320fn parse_partial_traced(
1321    spec: &Spec,
1322    input: &[String],
1323    custom_env: Option<&HashMap<String, String>>,
1324    mount_outputs: Option<&HashMap<String, String>>,
1325    mount_timing: MountTiming,
1326    validate_clauses: bool,
1327    trace: &mut Trace,
1328) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1329    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1330        let viewed = spec.for_view(view)?;
1331        return parse_partial_traced(
1332            &viewed,
1333            input,
1334            custom_env,
1335            mount_outputs,
1336            mount_timing,
1337            validate_clauses,
1338            trace,
1339        );
1340    }
1341    trace!("parse_partial: {input:?}");
1342    let mut input = input
1343        .iter()
1344        .enumerate()
1345        .map(|(argv, word)| Token::new(word.clone(), argv))
1346        .collect::<VecDeque<_>>();
1347    let argv0 = input.pop_front();
1348    if let Some(argv0) = argv0.as_ref() {
1349        trace.record(argv0.argv, TokenRole::Program);
1350    }
1351    if spec.multicall {
1352        if let Some(raw) = argv0 {
1353            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1354                // A symlink invocation reads a word the caller never typed — the basename of
1355                // the program itself — so argv[0] is both the program and, below, whatever
1356                // that word selects.
1357                trace.note_synthesized(raw.argv);
1358                input.push_front(Token::new(applet.to_string(), raw.argv));
1359            }
1360        }
1361    }
1362    // The policy observes the selected command's own argv, not values eventually filled from
1363    // env/default. Start at the root, then reset on every explicit descent. A default
1364    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1365    let mut command_has_argv = !input.is_empty();
1366
1367    let mut out = ParseOutput {
1368        cmd: spec.cmd.clone(),
1369        cmds: vec![spec.cmd.clone()],
1370        args: IndexMap::new(),
1371        clauses: IndexMap::new(),
1372        clause_flags: IndexMap::new(),
1373        flags: IndexMap::new(),
1374        tokens: vec![],
1375        flag_origins: IndexMap::new(),
1376        arg_origins: IndexMap::new(),
1377        overridden_flags: BTreeMap::new(),
1378        available_flags: gather_flags(&spec.cmd),
1379        flag_awaiting_value: vec![],
1380        errors: vec![],
1381        warnings: vec![],
1382        next_arg: None,
1383        double_dash_seen: false,
1384        external: None,
1385    };
1386    // Keep this internal so adding relationship support remains semver-compatible. The full
1387    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1388    let mut overridden_flags = HashSet::new();
1389    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1390    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1391    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1392    // or an inherited `-c` belonged to the ancestor.
1393    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1394
1395    // Phase 1: Scan for subcommands and collect global flags
1396    //
1397    // This phase identifies subcommands early because they may have mount points
1398    // that need to be executed with the global flags that appeared before them.
1399    //
1400    // Example: "usage --verbose run task"
1401    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1402    //   -> then finds "task" as a subcommand of "run" (if it exists)
1403    //
1404    // We only collect global flags for mounts because:
1405    // - Non-global flags are specific to the current command, not subcommands
1406    // - Global flags affect all commands and should be passed to mount points
1407    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1408    // Which flag each word skipped here belongs to is recorded on the word — see
1409    // `Token::binding`.
1410    let mut command_arg_found = false;
1411    let mut variadic_flag_active = false;
1412    let mut idx = 0;
1413    // Track whether we've already applied the default_subcommand to prevent
1414    // multiple switches (e.g., if default is "run" and there's a task named "run")
1415    let mut used_default_subcommand = false;
1416    // Whether the command in scope has had its own mounts run. A mount on the root
1417    // is the case that needs this: a subcommand's mounts are run when the parser
1418    // descends into it, but nothing descends into the root.
1419    let mut mounts_resolved = false;
1420    // A completion needs the whole command list before it can offer anything, and
1421    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1422    // mean a root mount never contributed to the very thing it exists for.
1423    //
1424    // The default-subcommand gate applies here too, and has to: offering a discovered
1425    // command that a real parse would hand to the default instead would be worse than
1426    // not offering it. A root mount under a `default_subcommand` that does not say
1427    // `overrides_default` therefore contributes nothing anywhere, which is what
1428    // "the default outranks discovery" means.
1429    let default_outranks_mounts =
1430        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1431    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1432    {
1433        mounts_resolved = true;
1434        let mut mounted = out.cmd.clone();
1435        mounted.mount(&[], mount_outputs)?;
1436        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1437        if let Some(last) = out.cmds.last_mut() {
1438            *last = mounted.clone();
1439        }
1440        out.cmd = mounted;
1441    }
1442
1443    while idx < input.len() {
1444        // Only for a word that could name a command, and only when it matches
1445        // nothing already declared. A CLI that declares its commands and mounts more
1446        // does not spawn a process for every invocation, and a flag — `--help`, or
1447        // anything unrecognized — never triggers discovery at all, which it would
1448        // otherwise do simply by not being a subcommand.
1449        // A declared `default_subcommand` already says what an unmatched word means,
1450        // and it costs nothing — so discovery waits behind it unless a mount asks to
1451        // outrank it. Without this, a task runner would spawn its discovery process
1452        // once per task invocation.
1453        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1454            default_accepts_word(&out.cmd, name, &input[idx].word)
1455                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1456        });
1457        if !mounts_resolved
1458            && !out.cmd.mounts.is_empty()
1459            && !default_catches_it
1460            && is_command_word(&input[idx].word)
1461            && !is_negative_number(&input[idx].word)
1462            && out.cmd.find_subcommand(&input[idx].word).is_none()
1463        {
1464            mounts_resolved = true;
1465            let mut mounted = out.cmd.clone();
1466            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1467            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1468            if let Some(last) = out.cmds.last_mut() {
1469                *last = mounted.clone();
1470            }
1471            out.cmd = mounted;
1472        }
1473        if variadic_flag_active
1474            && out.cmd.find_subcommand(&input[idx].word).is_some()
1475            && !out.cmd.subcommand_precedence_over_arg
1476        {
1477            break;
1478        }
1479        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1480            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1481                bail!(
1482                    "subcommand '{}' cannot be used with arguments on its parent command",
1483                    input[idx].word
1484                );
1485            }
1486            let mut subcommand = subcommand.clone();
1487            // Pass prefix words (global flags before this subcommand) to mount
1488            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1489            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1490            // commands are ordinary commands relative to each other.
1491            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1492            merge_subcommand_flags(
1493                &mut out.available_flags,
1494                gather_flags(&subcommand),
1495                crossing_mount,
1496            );
1497            // Remove subcommand from input
1498            let selected = input.remove(idx);
1499            if let Some(selected) = selected {
1500                trace.record(
1501                    selected.argv,
1502                    TokenRole::Command {
1503                        name: subcommand.name.clone(),
1504                    },
1505                );
1506            }
1507            command_has_argv = idx < input.len();
1508            out.cmds.push(subcommand.clone());
1509            out.cmd = subcommand.clone();
1510            // A descent already ran the new command's mounts, above.
1511            mounts_resolved = true;
1512            prefix_flags.clear();
1513            command_arg_found = false;
1514            variadic_flag_active = false;
1515            // Continue from current position (don't reset to 0)
1516            // After remove(), idx now points to the next element
1517        } else if !is_command_word(&input[idx].word)
1518            || declared_numeric_short(&out.available_flags, &input[idx].word)
1519        {
1520            // Check if this is a known flag
1521            let word = input[idx].word.clone();
1522            let flag_key = get_flag_key(&word);
1523
1524            // A short token keys on its first letter, so `-az` would be recorded as
1525            // `-a` and its tail left over. Check the whole token here, where it is
1526            // first read: a token containing an unrecognized letter is not a bundle,
1527            // and recording it as one is what let `-a` be applied from a token that
1528            // never named it.
1529            let is_bundle = word.starts_with("--")
1530                || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1531            if let Some(f) = out
1532                .available_flags
1533                .get(flag_key)
1534                .cloned()
1535                .filter(|_| is_bundle)
1536            {
1537                command_arg_found = true;
1538                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1539                // Skip the flag and keep scanning. Both global and non-global flags may precede
1540                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1541                // stopping at one would hide the subcommand — and any mount on it — from the
1542                // parse, leaving the subcommand name to be mis-read as a positional argument.
1543                //
1544                // Only globals are forwarded to mounts: a non-global flag belongs to the
1545                // command that declared it, not to what is mounted below it.
1546                input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1547                let mut forwarded = f.global.then(|| vec![word.clone()]);
1548                idx += 1;
1549
1550                // Only consume next word if flag takes an argument AND value isn't embedded
1551                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1552                if f.arg.is_some()
1553                    && !word.contains('=')
1554                    && idx < input.len()
1555                    && accepts_detached_flag_value(&f, &input[idx].word)
1556                {
1557                    if let Some(words) = forwarded.as_mut() {
1558                        words.push(input[idx].word.clone());
1559                    }
1560                    idx += 1;
1561                }
1562                if let Some(words) = forwarded {
1563                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1564                    prefix_flags.push((f, words));
1565                }
1566            } else {
1567                // Unknown flag - stop looking for subcommands
1568                // Let the main parsing phase handle the error
1569                break;
1570            }
1571        } else {
1572            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1573                idx += 1;
1574                continue;
1575            }
1576            // Found a word that's not a flag or subcommand
1577            // Check if we should use the default_subcommand (only once, and only at the
1578            // root, which is the only place a spec can declare one — `out.cmds` holds just
1579            // the root until something descends). Without that second condition the one
1580            // declared name is looked up wherever the parser happens to be standing, so an
1581            // unrelated command acquires a default because a name matched one level down:
1582            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1583            // `config ls`.
1584            if !used_default_subcommand && out.cmds.len() == 1 {
1585                if let Some(default_name) = &spec.default_subcommand {
1586                    if let Some(subcommand) = out
1587                        .cmd
1588                        .find_subcommand(default_name)
1589                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1590                    {
1591                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1592                            bail!(
1593                                "subcommand '{}' cannot be used with arguments on its parent command",
1594                                subcommand.name
1595                            );
1596                        }
1597                        let mut subcommand = subcommand.clone();
1598                        // Pass prefix words (global flags before this) to mount
1599                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1600                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1601                        merge_subcommand_flags(
1602                            &mut out.available_flags,
1603                            gather_flags(&subcommand),
1604                            crossing_mount,
1605                        );
1606                        out.cmds.push(subcommand.clone());
1607                        out.cmd = subcommand.clone();
1608                        command_has_argv = true;
1609                        prefix_flags.clear();
1610                        command_arg_found = false;
1611                        variadic_flag_active = false;
1612                        // This descent ran the new command's mounts, so lazy
1613                        // discovery must not run them a second time.
1614                        mounts_resolved = true;
1615                        used_default_subcommand = true;
1616                        // Continue the loop to check if this word is a subcommand of the
1617                        // default subcommand (e.g., a task name added via mount).
1618                        // If it's not a subcommand, the next iteration will break and
1619                        // Phase 2 will handle it as a positional arg.
1620                        continue;
1621                    }
1622                }
1623            }
1624            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1625            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1626            // default subcommand gets first refusal so interpreted and compiled routing agree
1627            // when the root sigil and the default command can both accept this word.
1628            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1629                idx += 1;
1630                continue;
1631            }
1632            // An unmatched word that names no subcommand is forwarded as an external
1633            // command: this word, then every token after it, including flags. Known
1634            // subcommands already won above, and a default_subcommand already caught.
1635            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1636            if out.cmd.external_subcommand {
1637                let rest: Vec<Token> = input.drain(idx..).collect();
1638                for token in &rest {
1639                    trace.record(token.argv, TokenRole::External);
1640                }
1641                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1642                break;
1643            }
1644            // This could be a positional argument, so stop subcommand search
1645            break;
1646        }
1647    }
1648
1649    // Phase 2: Main argument and flag parsing
1650    //
1651    // Now that we've identified all subcommands and executed their mounts,
1652    // we can parse the remaining arguments, flags, and their values.
1653
1654    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
1655    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
1656    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1657    // argument filled?" has to consult `out.args` by key instead of counting.
1658    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1659    let mut enable_flags = true;
1660    let mut grouped_flag = false;
1661    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1662    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1663    // words that come after it — see `report_double_dash_violation`.
1664    let mut seen_double_dash = false;
1665    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1666    // reopen sigil classification for this invocation.
1667    let mut restart_seen = false;
1668    // Args already reported as having been offered a word before the `--` they require, so a
1669    // variadic one does not report the same violation for every word it is offered.
1670    let mut double_dash_violations: HashSet<String> = HashSet::new();
1671    // Scalar occurrences are scoped to the command level where they were written. Inherited
1672    // globals may therefore appear once before and once after a subcommand under clap's strict
1673    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1674    // opposite forms may override one another, while repeating either spelling is an error.
1675    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1676
1677    while !input.is_empty() {
1678        let token = input.pop_front().unwrap();
1679        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1680        let binding = token.binding;
1681        let argv = token.argv;
1682        let mut w = token.word;
1683        // A short's attached value is re-queued with `grouped_flag` set, and that
1684        // continuation is not a following word. `require_equals` refuses only the
1685        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1686        // only when the value is actually missing, so `-cnever` is still `never`.
1687        let attached_continuation = grouped_flag;
1688
1689        // A clause boundary is syntax even after an automatic trailing argument disabled
1690        // flags. Only an explicit `--` protects a literal separator.
1691        if !seen_double_dash {
1692            if let Some(clause) = out.cmd.clause.as_ref() {
1693                if clause.separator.as_deref() == Some(w.as_str()) {
1694                    while try_bind_default_missing(
1695                        &mut out.flags,
1696                        &mut out.flag_awaiting_value,
1697                        custom_env,
1698                        &mut out.flag_origins,
1699                    )? {}
1700                    if let Some(flag) = out.flag_awaiting_value.first() {
1701                        let spelling = flag
1702                            .long
1703                            .first()
1704                            .map(|long| format!("--{long}"))
1705                            .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1706                            .unwrap_or_else(|| flag.name.clone());
1707                        return Err(UsageErr::InvalidFlag {
1708                            token: spelling.clone(),
1709                            reason: "requires an argument".to_string(),
1710                            span: (0, spelling.len()).into(),
1711                            input: spelling,
1712                        }
1713                        .into());
1714                    }
1715                    let name = clause.name.clone();
1716                    finalize_current_clause(&mut out);
1717                    out.arg_origins.clear();
1718                    trace.record(argv, TokenRole::ClauseSeparator { name });
1719                    next_arg_idx = 0;
1720                    out.flag_awaiting_value.clear();
1721                    reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1722                    enable_flags = true;
1723                    seen_double_dash = false;
1724                    continue;
1725                }
1726            }
1727        }
1728
1729        // Check for restart_token - resets argument parsing for multiple command invocations
1730        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1731        if let Some(ref restart_token) = out.cmd.restart_token {
1732            if w == *restart_token {
1733                // Reset argument parsing state for a fresh command invocation, keeping the
1734                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1735                // is not cleared here either, so clearing it would let one arg report the same
1736                // violation once per invocation.
1737                out.args.clear();
1738                // With the values gone, so is where they came from — otherwise the second
1739                // invocation of `run lint ::: test` reports the first one's provenance. The
1740                // token trace is *not* cleared: those words were read, and a report that
1741                // dropped them would show a command line with a hole in it.
1742                out.arg_origins.clear();
1743                trace.record(argv, TokenRole::Restart);
1744                next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1745                restart_seen = true;
1746                out.flag_awaiting_value.clear(); // Clear any pending flag values
1747                enable_flags = true; // Reset -- separator effect
1748                seen_double_dash = false; // The next invocation needs its own `--`
1749                continue;
1750            }
1751        }
1752
1753        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1754        // like, and that has to be asked before the separator arm below rather than
1755        // after it. Asked after, a `--` was consumed as a separator while the flag
1756        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1757        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1758        // itself, which is what clap does with the same declaration — and no flag can
1759        // still be waiting once the separator has done its job, so the starvation rule
1760        // below has no path around it.
1761        if enable_flags
1762            && !attached_continuation
1763            && w.starts_with('-')
1764            && out
1765                .flag_awaiting_value
1766                .last()
1767                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1768        {
1769            // A variadic argument collects here too: which token supplied its first
1770            // value says nothing about how many it takes.
1771            let should_return = bind_pending_flag_value(
1772                spec,
1773                &out.cmd,
1774                &mut out.errors,
1775                &mut out.flags,
1776                &mut out.flag_awaiting_value,
1777                &mut w,
1778                &mut input,
1779                custom_env,
1780                trace,
1781                argv,
1782                // The token a hyphen-valued flag takes is the following word, never attached.
1783                false,
1784            )?;
1785            if should_return {
1786                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1787                return Ok((out, overridden_flags));
1788            }
1789            continue;
1790        }
1791
1792        // A flag whose value may be omitted that cannot take this token as a detached
1793        // value finishes bare and leaves the token for whatever comes next:
1794        // `--color --verbose` colours with the missing value and still sets verbose,
1795        // and `--inspect 9229` with `require_equals` binds the missing value rather
1796        // than treating 9229 as the port.
1797        if enable_flags
1798            && !attached_continuation
1799            && !out.flag_awaiting_value.is_empty()
1800            && out.flag_awaiting_value.last().is_some_and(|flag| {
1801                (flag.default_missing.is_some() || flag.value_optional)
1802                    && !accepts_detached_flag_value(flag, &w)
1803            })
1804        {
1805            try_bind_default_missing(
1806                &mut out.flags,
1807                &mut out.flag_awaiting_value,
1808                custom_env,
1809                &mut out.flag_origins,
1810            )?;
1811        }
1812
1813        // The first explicit `--` is still a separator after an `automatic` argument has
1814        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1815        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1816        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1817        // behavior as the bug it is.
1818        if w == "--" && !seen_double_dash {
1819            enable_flags = false;
1820
1821            // Only preserve the double dash token if we're collecting values for a variadic arg
1822            // in double_dash == `preserve` mode
1823            let should_preserve = active_args(&out.cmd)
1824                .get(next_arg_idx)
1825                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1826                .unwrap_or(false);
1827
1828            if should_preserve {
1829                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1830                // neither counts as one nor unlocks a `double_dash="required"` arg.
1831            } else {
1832                seen_double_dash = true;
1833                trace.record(argv, TokenRole::Separator);
1834
1835                // Everything after an explicit `--` belongs to the arg that requires one, so
1836                // jump the cursor there — past any earlier arg, including a greedy variadic
1837                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1838                // which is what `double_dash="required"` is generated from. Specs without such
1839                // an arg find nothing and keep the cursor where it was.
1840                let target = active_args(&out.cmd).iter().position(|arg| {
1841                    arg.double_dash == SpecDoubleDashChoices::Required
1842                        && !out.args.contains_key(arg)
1843                });
1844                if let Some(target) = target {
1845                    // Forward only. An unfilled required arg declared *before* the cursor is
1846                    // left where it is rather than rewound to — words already assigned to
1847                    // later args would have to be taken back for that to mean anything, and
1848                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1849                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1850                    // one ahead of others is already outside what this models.
1851                    if target > next_arg_idx {
1852                        next_arg_idx = target;
1853                    }
1854                }
1855                continue;
1856            }
1857        }
1858
1859        // long flags
1860        if enable_flags && w.starts_with("--") {
1861            grouped_flag = false;
1862            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1863            // an empty value while `--jobs` supplies none. Collapsing the two lost
1864            // the flag entirely.
1865            let split = w.split_once('=');
1866            let word = split.map(|(word, _)| word).unwrap_or(&w);
1867            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1868            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1869                let command_level = binding
1870                    .as_ref()
1871                    .map(|(_, level)| *level)
1872                    .unwrap_or(out.cmds.len() - 1);
1873                parsed_flag_spellings
1874                    .entry(Arc::as_ptr(f) as usize)
1875                    .or_default()
1876                    .insert(word.to_string());
1877                // Recorded before the action check below: a token that named a flag named it
1878                // whether or not the parse can carry on afterwards.
1879                trace.record(
1880                    argv,
1881                    TokenRole::Flag {
1882                        flag: Arc::clone(f),
1883                        spelling: word.to_string(),
1884                        negated: f.negate.as_deref() == Some(word),
1885                    },
1886                );
1887                if f.action != crate::SpecFlagAction::Set {
1888                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1889                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1890                    return Ok((out, overridden_flags));
1891                }
1892                apply_flag_overrides(
1893                    f,
1894                    &out.available_flags,
1895                    &mut out.flags,
1896                    &mut out.flag_awaiting_value,
1897                    &mut overridden_flags,
1898                    &mut out.overridden_flags,
1899                );
1900                if let Some(pending) = out.flag_awaiting_value.first() {
1901                    out.errors.push(render_missing_flag_value(pending, &w));
1902                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1903                    return Ok((out, overridden_flags));
1904                }
1905                // An attached value only means something to a flag that takes one:
1906                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1907                // give a flag that holds no value. Handing that leftover to the
1908                // positionals would re-split one token into two, so `ex --force=yes`
1909                // would fill an argument the caller never typed a word for.
1910                if f.arg.is_some() {
1911                    record_scalar_flag_occurrence(
1912                        &out.cmds,
1913                        f,
1914                        command_level,
1915                        None,
1916                        &mut scalar_occurrences,
1917                        &mut out.errors,
1918                    );
1919                    let f = Arc::clone(f);
1920                    out.flag_awaiting_value.push(Arc::clone(&f));
1921                    // The `=` has already settled that this text is the value, so it
1922                    // binds here rather than going back on the queue to be read as a
1923                    // token again — where `--jobs=--force` looked like a flag of its
1924                    // own and bound `force`, leaving `jobs` unset.
1925                    if let Some((_, val)) = split {
1926                        // The `=` settles where the *first* value came from and nothing
1927                        // more, so a variadic argument goes on collecting from the words
1928                        // after it exactly as the detached form does.
1929                        let mut val = val.to_string();
1930                        let should_return = bind_pending_flag_value(
1931                            spec,
1932                            &out.cmd,
1933                            &mut out.errors,
1934                            &mut out.flags,
1935                            &mut out.flag_awaiting_value,
1936                            &mut val,
1937                            &mut input,
1938                            custom_env,
1939                            trace,
1940                            argv,
1941                            // The `=` settled that this text is the value, so it rode in on
1942                            // the flag's own token.
1943                            true,
1944                        )?;
1945                        if should_return {
1946                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1947                            return Ok((out, overridden_flags));
1948                        }
1949                    }
1950                } else if f.count {
1951                    let arr = out
1952                        .flags
1953                        .entry(Arc::clone(f))
1954                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1955                        .try_as_multi_bool_mut()
1956                        .unwrap();
1957                    arr.push(true);
1958                } else {
1959                    let negate = f.negate.clone().unwrap_or_default();
1960                    let negated_form = word == negate;
1961                    let value = if f.bool_value {
1962                        match split.map(|(_, value)| value) {
1963                            Some("true") => !negated_form,
1964                            Some("false") => negated_form,
1965                            Some(value) => {
1966                                out.errors.push(UsageErr::InvalidValue {
1967                                    name: f.name.clone(),
1968                                    value: value.to_string(),
1969                                    reason: "expected `true` or `false`".to_string(),
1970                                });
1971                                continue;
1972                            }
1973                            None => !negated_form,
1974                        }
1975                    } else {
1976                        !negated_form
1977                    };
1978                    // Which form was typed is a question about the name, so it is
1979                    // asked of `word` rather than the whole token: the attached value
1980                    // is dropped just above, and comparing `--no-color=yes` against
1981                    // `--no-color` would take the negation down with it.
1982                    record_scalar_flag_occurrence(
1983                        &out.cmds,
1984                        f,
1985                        command_level,
1986                        Some(!negated_form),
1987                        &mut scalar_occurrences,
1988                        &mut out.errors,
1989                    );
1990                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1991                }
1992                continue;
1993            }
1994            if is_help_arg(spec, &out.cmd, &w) {
1995                out.errors
1996                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1997                trace.record(
1998                    argv,
1999                    TokenRole::Builtin {
2000                        spelling: w.clone(),
2001                    },
2002                );
2003                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2004                return Ok((out, overridden_flags));
2005            }
2006            if is_version_arg(spec, &out.cmds, &w) {
2007                out.errors.push(render_version_err(spec, w.len() > 2));
2008                trace.record(
2009                    argv,
2010                    TokenRole::Builtin {
2011                        spelling: w.clone(),
2012                    },
2013                );
2014                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2015                return Ok((out, overridden_flags));
2016            }
2017            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2018                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2019                trace.close(&input);
2020                return Err(refused.into());
2021            }
2022        }
2023
2024        // short flags
2025        //
2026        // A fresh token is checked whole before any of it is applied: `-az` with only
2027        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2028        // discovering that `z` names nothing. A grouped continuation is exempt — its
2029        // token was already checked when it arrived.
2030        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2031        let positional_negative_number = !declared_numeric_short
2032            && is_negative_number(&w)
2033            && active_args(&out.cmd)
2034                .get(next_arg_idx)
2035                .is_some_and(|arg| arg.allow_negative_numbers);
2036        if enable_flags
2037            && !grouped_flag
2038            // A word phase 1 already resolved to a flag needs no re-checking, and
2039            // the flags in scope have changed since, so re-checking would be wrong.
2040            && binding.is_none()
2041            && w.starts_with('-')
2042            && w.len() > 1
2043            && is_flag_like(&w)
2044            && !positional_negative_number
2045            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2046        {
2047            // Refused if this command asked for that; otherwise it carries on below
2048            // as one word, with none of its letters applied.
2049            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2050                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2051                trace.close(&input);
2052                return Err(refused.into());
2053            }
2054        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2055            let short = w.chars().nth(1).unwrap();
2056            if let Some(f) = binding
2057                .as_ref()
2058                .map(|(flag, _)| flag)
2059                .or_else(|| out.available_flags.get(&format!("-{short}")))
2060            {
2061                let command_level = binding
2062                    .as_ref()
2063                    .map(|(_, level)| *level)
2064                    .unwrap_or(out.cmds.len() - 1);
2065                if f.action != crate::SpecFlagAction::Set {
2066                    out.errors
2067                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2068                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2069                    return Ok((out, overridden_flags));
2070                }
2071                parsed_flag_spellings
2072                    .entry(Arc::as_ptr(f) as usize)
2073                    .or_default()
2074                    .insert(format!("-{short}"));
2075                trace.record(
2076                    argv,
2077                    TokenRole::Flag {
2078                        flag: Arc::clone(f),
2079                        spelling: format!("-{short}"),
2080                        // A short spelling is never the negated form: `negate` is a long.
2081                        negated: false,
2082                    },
2083                );
2084                apply_flag_overrides(
2085                    f,
2086                    &out.available_flags,
2087                    &mut out.flags,
2088                    &mut out.flag_awaiting_value,
2089                    &mut overridden_flags,
2090                    &mut out.overridden_flags,
2091                );
2092                if !attached_continuation {
2093                    if let Some(pending) = out.flag_awaiting_value.first() {
2094                        out.errors.push(render_missing_flag_value(pending, &w));
2095                        record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2096                        return Ok((out, overridden_flags));
2097                    }
2098                }
2099                let rest = &w[1 + short.len_utf8()..];
2100                if !rest.is_empty() {
2101                    // `-abc` is one token that names three flags, so the tail is read at the
2102                    // bundle's own position rather than at one of its own.
2103                    input.push_front(Token::new(format!("-{rest}"), argv));
2104                }
2105                // A fully consumed short is no longer a grouped continuation.
2106                // Leaving this set after `-ai` made `-i` skip `require_equals`
2107                // and bind the following word.
2108                grouped_flag = !rest.is_empty();
2109                if f.arg.is_some() {
2110                    record_scalar_flag_occurrence(
2111                        &out.cmds,
2112                        f,
2113                        command_level,
2114                        None,
2115                        &mut scalar_occurrences,
2116                        &mut out.errors,
2117                    );
2118                    out.flag_awaiting_value.push(Arc::clone(f));
2119                } else if f.count {
2120                    let arr = out
2121                        .flags
2122                        .entry(Arc::clone(f))
2123                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2124                        .try_as_multi_bool_mut()
2125                        .unwrap();
2126                    arr.push(true);
2127                } else {
2128                    let negate = f.negate.clone().unwrap_or_default();
2129                    let value = w != negate;
2130                    record_scalar_flag_occurrence(
2131                        &out.cmds,
2132                        f,
2133                        command_level,
2134                        Some(value),
2135                        &mut scalar_occurrences,
2136                        &mut out.errors,
2137                    );
2138                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2139                }
2140                continue;
2141            }
2142            // The letter nothing declared may still be one the parser supplies, and it may
2143            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2144            // neither reaches the whole-token spellings below.
2145            if let Some(err) = supplied_short(spec, &out.cmds, short) {
2146                out.errors.push(err);
2147                trace.record(
2148                    argv,
2149                    TokenRole::Builtin {
2150                        spelling: format!("-{short}"),
2151                    },
2152                );
2153                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2154                return Ok((out, overridden_flags));
2155            }
2156            if is_help_arg(spec, &out.cmd, &w) {
2157                out.errors
2158                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2159                trace.record(
2160                    argv,
2161                    TokenRole::Builtin {
2162                        spelling: w.clone(),
2163                    },
2164                );
2165                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2166                return Ok((out, overridden_flags));
2167            }
2168            if is_version_arg(spec, &out.cmds, &w) {
2169                out.errors.push(render_version_err(spec, w.len() > 2));
2170                trace.record(
2171                    argv,
2172                    TokenRole::Builtin {
2173                        spelling: w.clone(),
2174                    },
2175                );
2176                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2177                return Ok((out, overridden_flags));
2178            }
2179            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2180                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2181                trace.close(&input);
2182                return Err(refused.into());
2183            }
2184            if grouped_flag {
2185                grouped_flag = false;
2186                w.remove(0);
2187                // What is left is a short flag's attached value, and one `=` between
2188                // the letter and the value is a separator: `-j=8` means 8. Only one,
2189                // so `-j==8` still means `=8`.
2190                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2191                    w.remove(0);
2192                }
2193            }
2194        }
2195
2196        // Only while flags are still being read. A flag still waiting when the separator
2197        // was consumed is starved: its value would have to come from after the `--`,
2198        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2199        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2200        // was gone. Left waiting, it is reported as the missing value it is.
2201        // `require_equals` refuses a detached value: `--flag value` is a missing
2202        // value, not a flag of `"value"`. The attached form is still bound above.
2203        // Reported here rather than left waiting until the end of the line: falling
2204        // through would offer `value` to the positionals and call it an unexpected
2205        // word, which is the wrong error and a different one from usage-argv.
2206        if enable_flags
2207            && !attached_continuation
2208            && !out.flag_awaiting_value.is_empty()
2209            && out
2210                .flag_awaiting_value
2211                .last()
2212                .is_some_and(|flag| flag.require_equals)
2213        {
2214            let flag = out.flag_awaiting_value.last().unwrap();
2215            let token = flag
2216                .long
2217                .first()
2218                .map(|l| format!("--{l}"))
2219                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2220                .unwrap_or_else(|| flag.name.clone());
2221            out.errors.push(UsageErr::InvalidFlag {
2222                token: token.clone(),
2223                reason: "requires an argument".to_string(),
2224                span: (0, 0).into(),
2225                input: format!("{token} {w}"),
2226            });
2227            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2228            return Ok((out, overridden_flags));
2229        }
2230        if enable_flags
2231            && !out.flag_awaiting_value.is_empty()
2232            && (attached_continuation
2233                || out
2234                    .flag_awaiting_value
2235                    .last()
2236                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2237        {
2238            // Held before the drain pops it: a flag whose argument is variadic keeps
2239            // taking values after this first one.
2240            let should_return = bind_pending_flag_value(
2241                spec,
2242                &out.cmd,
2243                &mut out.errors,
2244                &mut out.flags,
2245                &mut out.flag_awaiting_value,
2246                &mut w,
2247                &mut input,
2248                custom_env,
2249                trace,
2250                argv,
2251                attached_continuation,
2252            )?;
2253            if should_return {
2254                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2255                return Ok((out, overridden_flags));
2256            }
2257            continue;
2258        }
2259
2260        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2261            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2262            .flatten()
2263        {
2264            if value.is_empty() {
2265                out.errors.push(UsageErr::InvalidValue {
2266                    name: arg.name.clone(),
2267                    value: w.clone(),
2268                    reason: format!("expected a value after sigil {sigil:?}"),
2269                });
2270                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2271                return Ok((out, overridden_flags));
2272            }
2273            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2274            let suppress_trailing_delimiter =
2275                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2276            let delimiter = if suppress_trailing_delimiter && trailing_value {
2277                None
2278            } else {
2279                arg.delimiter
2280            };
2281            let parts = match delimiter {
2282                Some(delimiter) => value
2283                    .split(delimiter)
2284                    .map(str::to_string)
2285                    .collect::<Vec<_>>(),
2286                None => vec![value.to_string()],
2287            };
2288            let mut refused = false;
2289            for part in &parts {
2290                if validate_choices(
2291                    spec,
2292                    &out.cmd,
2293                    &mut out.errors,
2294                    ChoiceTarget::arg(arg),
2295                    part,
2296                    arg.choices.as_ref(),
2297                    custom_env,
2298                )? {
2299                    refused = true;
2300                    break;
2301                }
2302            }
2303            if refused {
2304                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2305                return Ok((out, overridden_flags));
2306            }
2307            trace.record(
2308                argv,
2309                TokenRole::Sigil {
2310                    arg: Arc::new(arg.clone()),
2311                    sigil: sigil.to_string(),
2312                    values: parts.clone(),
2313                },
2314            );
2315            let key = Arc::new(arg.clone());
2316            if arg.var {
2317                let arr = out
2318                    .args
2319                    .entry(key)
2320                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2321                    .try_as_multi_string_mut()
2322                    .unwrap();
2323                arr.extend(parts);
2324            } else {
2325                out.args.insert(key, ParseValue::String(value.to_string()));
2326            }
2327            continue;
2328        }
2329
2330        if out.cmd.allow_missing_positional {
2331            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2332            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2333                if current.required || out.args.contains_key(current) {
2334                    break;
2335                }
2336                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2337                    .iter()
2338                    .filter(|arg| arg.required && arg.sigil.is_none())
2339                    .count();
2340                if required_after == 0 {
2341                    break;
2342                }
2343                let remaining_values = 1 + input
2344                    .iter()
2345                    .filter(|token| {
2346                        (!enable_flags || !is_flag_like(&token.word))
2347                            && (!enable_flags
2348                                || restart_seen
2349                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2350                    })
2351                    .count();
2352                if remaining_values > required_after {
2353                    break;
2354                }
2355                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2356            }
2357        }
2358
2359        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2360            if arg.var
2361                && out.args.contains_key(arg)
2362                && arg.value_terminator.as_deref() == Some(w.as_str())
2363            {
2364                trace.record(
2365                    argv,
2366                    TokenRole::ValueTerminator {
2367                        ends: arg.name.clone(),
2368                    },
2369                );
2370                next_arg_idx += 1;
2371                continue;
2372            }
2373            // Before anything else: an arg that requires `--` accepts nothing until one has been
2374            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2375            // reported as an invalid choice, and from reaching that function's help escape.
2376            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2377                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2378                trace.record(
2379                    argv,
2380                    TokenRole::Refused {
2381                        reason: format!("{} only accepts words after `--`", arg.name),
2382                    },
2383                );
2384                // Drop the word without filling the arg or advancing the cursor: every later
2385                // word hits the same arg and is rejected the same way, so the parse still ends
2386                // in an error rather than in `unexpected word`.
2387                continue;
2388            }
2389            // Split before judging, as the flag path does: after the split the word is
2390            // no longer one value, and `choices` has to be asked about each. Judging
2391            // first rejects `src:docs` against a list that both halves are on, and
2392            // names the whole word rather than the half that was wrong.
2393            let trailing_value =
2394                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2395            let suppress_trailing_delimiter =
2396                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2397            let delimiter = if suppress_trailing_delimiter && trailing_value {
2398                None
2399            } else {
2400                arg.delimiter
2401            };
2402            let parts: Vec<String> = match delimiter {
2403                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2404                None => vec![w.clone()],
2405            };
2406            let mut refused = false;
2407            for part in &parts {
2408                if validate_choices(
2409                    spec,
2410                    &out.cmd,
2411                    &mut out.errors,
2412                    ChoiceTarget::arg(arg),
2413                    part,
2414                    arg.choices.as_ref(),
2415                    custom_env,
2416                )? {
2417                    refused = true;
2418                    break;
2419                }
2420            }
2421            if refused {
2422                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2423                return Ok((out, overridden_flags));
2424            }
2425            // `double_dash="automatic"` means the first value this arg takes is the last
2426            // token read as anything but data: a wrapper declaring it can forward flags
2427            // without its caller typing a `--`. Set before the value is stored, so the
2428            // rest of the command line is already past flag parsing.
2429            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2430                enable_flags = false;
2431            }
2432            // A flag-like word reaching a positional while flags are still being read was
2433            // offered to every declaration and matched none: under the default
2434            // `unknown_flags="value"` it becomes data, and saying so is the difference
2435            // between "you have a typo" and "this argument took your typo".
2436            let unknown_flag = enable_flags
2437                && !positional_negative_number
2438                && is_flag_like(&w)
2439                && binding.is_none();
2440            trace.record(
2441                argv,
2442                if unknown_flag {
2443                    TokenRole::UnknownFlag {
2444                        bound_as: Some(Arc::new(arg.clone())),
2445                    }
2446                } else {
2447                    TokenRole::Arg {
2448                        arg: Arc::new(arg.clone()),
2449                        values: parts.clone(),
2450                    }
2451                },
2452            );
2453            if arg.var {
2454                let arr = out
2455                    .args
2456                    .entry(Arc::new(arg.clone()))
2457                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2458                    .try_as_multi_string_mut()
2459                    .unwrap();
2460                // The values this word carried, split above so that everything
2461                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2462                // counts the values the user meant rather than the words they typed.
2463                arr.extend(parts.iter().cloned());
2464                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2465                    next_arg_idx += 1;
2466                }
2467            } else {
2468                out.args
2469                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2470                next_arg_idx += 1;
2471            }
2472            if out
2473                .cmd
2474                .clause
2475                .as_ref()
2476                .is_some_and(|clause| clause.separator.is_none())
2477                && next_arg_idx >= active_args(&out.cmd).len()
2478            {
2479                while try_bind_default_missing(
2480                    &mut out.flags,
2481                    &mut out.flag_awaiting_value,
2482                    custom_env,
2483                    &mut out.flag_origins,
2484                )? {}
2485                if let Some(flag) = out.flag_awaiting_value.first() {
2486                    let spelling = flag
2487                        .long
2488                        .first()
2489                        .map(|long| format!("--{long}"))
2490                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2491                        .unwrap_or_else(|| flag.name.clone());
2492                    return Err(UsageErr::InvalidFlag {
2493                        token: spelling.clone(),
2494                        reason: "requires an argument".to_string(),
2495                        span: (0, spelling.len()).into(),
2496                        input: spelling,
2497                    }
2498                    .into());
2499                }
2500                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2501                finalize_current_clause(&mut out);
2502                out.arg_origins.clear();
2503                trace.record(argv, TokenRole::ClauseSeparator { name });
2504                next_arg_idx = 0;
2505                out.flag_awaiting_value.clear();
2506                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2507                enable_flags = true;
2508                seen_double_dash = false;
2509            }
2510            continue;
2511        }
2512        if is_help_arg(spec, &out.cmd, &w) {
2513            out.errors
2514                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2515            trace.record(
2516                argv,
2517                TokenRole::Builtin {
2518                    spelling: w.clone(),
2519                },
2520            );
2521            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2522            return Ok((out, overridden_flags));
2523        }
2524        if is_version_arg(spec, &out.cmds, &w) {
2525            out.errors.push(render_version_err(spec, w.len() > 2));
2526            trace.record(
2527                argv,
2528                TokenRole::Builtin {
2529                    spelling: w.clone(),
2530                },
2531            );
2532            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2533            return Ok((out, overridden_flags));
2534        }
2535        trace.record(
2536            argv,
2537            TokenRole::Refused {
2538                reason: "no declaration takes this word".to_string(),
2539            },
2540        );
2541        trace.close(&input);
2542        bail!("unexpected word: {w}");
2543    }
2544
2545    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2546    if validate_clauses {
2547        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2548    }
2549    let clause_flag_names = out
2550        .cmd
2551        .clause
2552        .iter()
2553        .flat_map(|clause| &clause.flags)
2554        .map(|flag| flag.name.clone())
2555        .collect::<HashSet<_>>();
2556
2557    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2558    // declarations with the same canonical name therefore share one public value entry even
2559    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2560    // both, which is what exclusivity needs.
2561    let flag_was_parsed =
2562        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2563
2564    // The spellings the selected command's own declaration speaks for, on this object.
2565    //
2566    // Empty unless that declaration really is this object's: a parent and child may each
2567    // declare `--clean` without merging, leaving two flags that share a name, and the
2568    // ancestor's must not be read as the child's. The test is whether every spelling the child
2569    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2570    // an ancestor whose long form the child took over.
2571    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2572        let declared: HashSet<String> = out
2573            .cmd
2574            .flags
2575            .iter()
2576            .filter(|declared| declared.name == flag.name)
2577            .flat_map(flag_keys)
2578            .collect();
2579        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2580        // some other inherited global already owns — `-c --clean` beside an inherited
2581        // `-c --config` — and that collision is resolved in the other global's favor, so the
2582        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2583        // unrelated collision disown the child from the `--clean` it plainly does own.
2584        //
2585        // Still enough to tell the two-object case apart, which is what this guards: when a
2586        // child re-declares a global as global, the child's own spellings resolve to the
2587        // child's separate flag, so none of them lands on the ancestor's.
2588        let speaks_for_this_flag = declared.iter().any(|spelling| {
2589            out.available_flags
2590                .get(spelling)
2591                .is_some_and(|available| Arc::ptr_eq(available, flag))
2592        });
2593        if speaks_for_this_flag {
2594            declared
2595        } else {
2596            HashSet::new()
2597        }
2598    };
2599
2600    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2601    //
2602    // A child that re-declares an inherited global merges into one object answering to two
2603    // alias sets whose declarations may disagree, so there is no single owner to name: the
2604    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2605    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2606    // child's in one invocation — and each carries its own declaration's answer.
2607    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2608        let child = child_spellings(flag);
2609        let child_exclusive = !child.is_empty()
2610            && out
2611                .cmd
2612                .flags
2613                .iter()
2614                .any(|declared| declared.name == flag.name && declared.exclusive);
2615        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2616            Some(spellings) => (
2617                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2618                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2619            ),
2620            // An environment value has no spelling to attribute it by. The declaration the
2621            // selected command has in scope is the one that answers — which is the child's
2622            // when it re-declared the flag, and the ancestor's when it did not.
2623            None => (child_exclusive, flag.exclusive && child.is_empty()),
2624        }
2625    };
2626
2627    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2628        let (child, ancestor) = exclusivity_in_play(flag);
2629        child || ancestor
2630    };
2631
2632    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2633    // command that otherwise needs an input. Companions are still diagnosed below, but an
2634    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2635    // whether it was alone or not.
2636    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2637        .filter(|flag| !clause_flag_names.contains(&flag.name))
2638        .any(|flag| {
2639            exclusive_occurrence(flag)
2640                && !overridden_flags.contains(&flag.name)
2641                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2642        });
2643    let requirements_apply = |command_index: usize| {
2644        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2645    };
2646
2647    if out.cmd.arg_required_else_help && !command_has_argv {
2648        out.errors.push(render_help_err(spec, &out.cmd, false));
2649    }
2650
2651    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2652    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2653    // the child would be here instead. The spec has carried `subcommand_required` since it was
2654    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2655    // complete invocation while usage-argv and clap both refused it.
2656    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2657        let mut names: Vec<&str> = out
2658            .cmd
2659            .subcommands
2660            .iter()
2661            // Aliases share a map entry with the name they point at; listing both would offer
2662            // the same command twice under two spellings.
2663            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2664            .map(|(name, _)| name.as_str())
2665            .collect();
2666        names.sort_unstable();
2667        out.errors.push(UsageErr::MissingSubcommand(
2668            out.cmd.name.clone(),
2669            names.join(", "),
2670        ));
2671    }
2672
2673    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2674    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2675    if !exclusive_present {
2676        for arg in out
2677            .cmds
2678            .iter()
2679            .enumerate()
2680            .filter(|(index, _)| requirements_apply(*index))
2681            .flat_map(|(_, cmd)| &cmd.args)
2682        {
2683            if out.args.contains_key(arg) {
2684                continue;
2685            }
2686            // Already reported as needing a `--`; one mistake should not yield two messages.
2687            if double_dash_violations.contains(&arg.name) {
2688                continue;
2689            }
2690            let required_if = arg.required_if.iter().any(|selector| {
2691                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2692            });
2693            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2694                selector_explicit_has_value(
2695                    &condition.selector,
2696                    &condition.value,
2697                    &out,
2698                    &overridden_flags,
2699                    custom_env,
2700                )
2701            });
2702            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2703                && arg.required_if_eq_all.iter().all(|condition| {
2704                    selector_explicit_has_value(
2705                        &condition.selector,
2706                        &condition.value,
2707                        &out,
2708                        &overridden_flags,
2709                        custom_env,
2710                    )
2711                });
2712            let unless_any = arg.required_unless.iter().any(|selector| {
2713                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2714            });
2715            let unless_all = !arg.required_unless_all.is_empty()
2716                && arg.required_unless_all.iter().all(|selector| {
2717                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2718                });
2719            let required_unless = !(unless_any
2720                || unless_all
2721                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2722            if (arg.required
2723                || required_if
2724                || required_if_eq
2725                || required_if_eq_all
2726                || required_unless)
2727                && arg.default.is_empty()
2728            {
2729                // Check if there's an env var available (custom env map takes precedence)
2730                let has_env = arg
2731                    .env
2732                    .as_ref()
2733                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2734                if !has_env {
2735                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2736                }
2737            }
2738        }
2739    }
2740
2741    // Conflicts are a question about the invocation as a whole rather than about any one
2742    // token, so they are checked here beside the requirement checks rather than at the
2743    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2744    // Its own loop: the requirement loop below skips the flags that *were* given, which
2745    // is exactly the set this needs.
2746    //
2747    // A value from the environment counts on both sides, matching what
2748    // `selector_is_explicit` says about the other flag: the question is whether a flag
2749    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2750    // would make the same pair of flags a conflict or not depending on which one
2751    // happened to be typed.
2752    for flag in unique_flags(out.available_flags.values())
2753        .filter(|flag| !clause_flag_names.contains(&flag.name))
2754    {
2755        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2756        if !given || overridden_flags.contains(&flag.name) {
2757            continue;
2758        }
2759        for other in &flag.conflicts {
2760            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2761                out.errors.push(UsageErr::InvalidFlag {
2762                    token: format!("--{}", flag.name),
2763                    reason: format!("conflicts with {other}"),
2764                    span: (0, 0).into(),
2765                    input: format!("--{} {other}", flag.name),
2766                });
2767            }
2768        }
2769        // The positive form, checked in the same pass and under the same rule: a value
2770        // from the environment satisfies a requirement, because the question is whether
2771        // the other flag has a value rather than how it got one. A flag that was
2772        // overridden away has not been given, so it cannot satisfy anything either —
2773        // which is what `selector_is_explicit` already accounts for.
2774        //
2775        // Reported as the missing flag rather than as something wrong with the flag that
2776        // named it, which is what clap says too: an unmet `requires` is a required
2777        // argument that was not provided. Named by its own name, resolved through the
2778        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2779        let owner = out
2780            .cmds
2781            .iter()
2782            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2783            .unwrap_or(out.cmds.len() - 1);
2784        if !exclusive_present && requirements_apply(owner) {
2785            for other in &flag.requires {
2786                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2787                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2788                    if other.starts_with('-') {
2789                        out.errors.push(UsageErr::MissingFlag(name));
2790                    } else {
2791                        out.errors.push(UsageErr::MissingArg(name));
2792                    }
2793                }
2794            }
2795            for condition in &flag.requires_if {
2796                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2797                    && !selector_is_satisfied(
2798                        &condition.requires,
2799                        &out,
2800                        &overridden_flags,
2801                        custom_env,
2802                    )
2803                {
2804                    let name = selector_flag_name(&condition.requires, &out)
2805                        .unwrap_or_else(|| condition.requires.clone());
2806                    out.errors.push(UsageErr::MissingFlag(name));
2807                }
2808            }
2809        }
2810    }
2811
2812    // Positionals can declare the same pairwise conflict as flags. Their selector is
2813    // the bare argument name, while a flag keeps its dashed spelling.
2814    for (command_index, arg) in out
2815        .cmds
2816        .iter()
2817        .enumerate()
2818        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2819    {
2820        let given = arg_is_explicit(arg, &out, custom_env);
2821        if !given {
2822            continue;
2823        }
2824        for other in &arg.conflicts {
2825            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2826                out.errors.push(UsageErr::InvalidFlag {
2827                    token: arg.name.clone(),
2828                    reason: format!("conflicts with {other}"),
2829                    span: (0, 0).into(),
2830                    input: format!("{} {other}", arg.name),
2831                });
2832            }
2833        }
2834        if !exclusive_present && requirements_apply(command_index) {
2835            for other in &arg.requires {
2836                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2837                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2838                    if other.starts_with('-') {
2839                        out.errors.push(UsageErr::MissingFlag(name));
2840                    } else {
2841                        out.errors.push(UsageErr::MissingArg(name));
2842                    }
2843                }
2844            }
2845        }
2846    }
2847
2848    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2849    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2850    // positionals included, which is what distinguishes it from being in a group with
2851    // every other flag.
2852    //
2853    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2854    // exclusive one is nobody saying anything, and counting it would make the exclusive
2855    // flag unusable on any command that has a default. Environment values do count, also as
2856    // `conflicts` reads them, so the spec parser and the derive agree.
2857    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
2858        .filter(|flag| !clause_flag_names.contains(&flag.name))
2859    {
2860        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2861        // but re-declared aliases can leave distinct declarations with that same name in
2862        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2863        // compare the parser's `Arc`s by identity here.
2864        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2865        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2866            continue;
2867        }
2868        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2869            .filter(|other| !clause_flag_names.contains(&other.name))
2870            .find(|other| {
2871                !Arc::ptr_eq(other, flag)
2872                    && !overridden_flags.contains(&other.name)
2873                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2874            })
2875            .map(|other| format!("--{}", other.name));
2876        let other_arg = active_args(&out.cmd).iter().find(|arg| {
2877            out.args.keys().any(|given| given.name == arg.name)
2878                || out
2879                    .clauses
2880                    .values()
2881                    .flatten()
2882                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
2883                || arg
2884                    .env
2885                    .as_ref()
2886                    .is_some_and(|env| env_contains(custom_env, env))
2887        });
2888        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2889        // exclusive flag belonging to the child itself does not conflict with the command word
2890        // needed to reach that child — so the question is not who owns the flag but whose
2891        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2892        // separated.
2893        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2894        let selected_subcommand =
2895            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2896        let other = other_flag
2897            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2898            .or(selected_subcommand);
2899        if let Some(other) = other {
2900            out.errors.push(UsageErr::InvalidFlag {
2901                token: format!("--{}", flag.name),
2902                reason: format!("must be given on its own, and {other} was given too"),
2903                span: (0, 0).into(),
2904                input: format!("--{} {other}", flag.name),
2905            });
2906        }
2907    }
2908
2909    // Groups, checked once per group rather than per flag: both questions a group asks —
2910    // how many members were given, and whether that is enough — are about the set, which
2911    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2912    //
2913    // The same "given" rule as everything else here, so a member filled from the
2914    // environment or a default counts.
2915    // Every command in the chain, not only the selected one: a group may name global
2916    // flags, which belong to an ancestor and are declared there.
2917    let mut group_errors: Vec<UsageErr> = Vec::new();
2918    for (command_index, group) in out
2919        .cmds
2920        .iter()
2921        .enumerate()
2922        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2923    {
2924        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2925        // `--file` are two spellings of one flag, and a group naming both — or naming one
2926        // flag twice — would otherwise report that flag as conflicting with itself the
2927        // moment it was given. Deduplicated rather than refused where the group is
2928        // written, because listing both spellings is redundant, not wrong.
2929        let mut given: Vec<&str> = Vec::new();
2930        let mut seen: Vec<String> = Vec::new();
2931        for selector in &group.members {
2932            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2933                continue;
2934            }
2935            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2936            if seen.contains(&name) {
2937                continue;
2938            }
2939            seen.push(name);
2940            given.push(selector.as_str());
2941        }
2942        if !group.multiple && given.len() > 1 {
2943            group_errors.push(UsageErr::InvalidFlag {
2944                token: given[1].to_string(),
2945                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2946                span: (0, 0).into(),
2947                input: format!("{} {}", given[0], given[1]),
2948            });
2949        }
2950        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2951        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2952        // exclusivity must count only what was supplied, or a defaulted member would
2953        // collide with the sibling the user actually typed.
2954        let satisfied = group
2955            .members
2956            .iter()
2957            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2958        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2959            // The members are what a user has to type, so they are in the message; the
2960            // group's name is there too, since a command with several groups would
2961            // otherwise report the same sentence twice with nothing to tell them apart.
2962            group_errors.push(UsageErr::MissingGroup {
2963                group: group.name.clone(),
2964                members: group.members.join(", "),
2965            });
2966        }
2967    }
2968    out.errors.extend(group_errors);
2969
2970    if !exclusive_present {
2971        for flag in unique_flags(out.available_flags.values())
2972            .filter(|flag| !clause_flag_names.contains(&flag.name))
2973        {
2974            let owner = out
2975                .cmds
2976                .iter()
2977                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2978                .unwrap_or(out.cmds.len() - 1);
2979            if !requirements_apply(owner) {
2980                continue;
2981            }
2982            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2983                continue;
2984            }
2985            let has_default =
2986                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2987            let has_env = flag_has_env(flag, custom_env);
2988            let required_if = flag.required_if.iter().any(|selector| {
2989                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2990            });
2991            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2992                selector_explicit_has_value(
2993                    &condition.selector,
2994                    &condition.value,
2995                    &out,
2996                    &overridden_flags,
2997                    custom_env,
2998                )
2999            });
3000            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3001                && flag.required_if_eq_all.iter().all(|condition| {
3002                    selector_explicit_has_value(
3003                        &condition.selector,
3004                        &condition.value,
3005                        &out,
3006                        &overridden_flags,
3007                        custom_env,
3008                    )
3009                });
3010            let unless_any = flag.required_unless.iter().any(|selector| {
3011                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3012            });
3013            let unless_all = !flag.required_unless_all.is_empty()
3014                && flag.required_unless_all.iter().all(|selector| {
3015                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3016                });
3017            let required_unless = !(unless_any
3018                || unless_all
3019                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3020            if (flag.required
3021                || required_if
3022                || required_if_eq
3023                || required_if_eq_all
3024                || required_unless)
3025                && !has_default
3026                && !has_env
3027            {
3028                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3029            }
3030        }
3031    }
3032
3033    // Validate var_min/var_max constraints for variadic args
3034    for (arg, value) in &out.args {
3035        if arg.var {
3036            if let ParseValue::MultiString(values) = value {
3037                if let Some(min) = arg.var_min {
3038                    if values.len() < min {
3039                        out.errors.push(UsageErr::VarArgTooFew {
3040                            name: arg.name.clone(),
3041                            min,
3042                            got: values.len(),
3043                        });
3044                    }
3045                }
3046                if let Some(max) = arg.var_max {
3047                    if values.len() > max {
3048                        out.errors.push(UsageErr::VarArgTooMany {
3049                            name: arg.name.clone(),
3050                            max,
3051                            got: values.len(),
3052                        });
3053                    }
3054                }
3055            }
3056        }
3057    }
3058
3059    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3060    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3061    // by binding once per occurrence, where the per-occurrence count is still available.
3062    for flag in unique_flags(out.available_flags.values())
3063        .filter(|flag| !clause_flag_names.contains(&flag.name))
3064    {
3065        if flag.var {
3066            let bound = match out.flags.get(flag) {
3067                Some(ParseValue::MultiString(values)) => values.len(),
3068                Some(ParseValue::MultiBool(values)) => values.len(),
3069                Some(_) => 1,
3070                None => 0,
3071            };
3072            // A partial parse deliberately leaves the final value-optional flag pending so
3073            // completion can still answer for it. It is nevertheless a real occurrence for
3074            // the repeated flag's bounds; the full parser closes it just after this phase.
3075            let pending = out
3076                .flag_awaiting_value
3077                .iter()
3078                .filter(|pending| {
3079                    Arc::ptr_eq(pending, flag)
3080                        && (pending.value_optional || pending.default_missing.is_some())
3081                })
3082                .count();
3083            let count = bound + pending;
3084            if count == 0 {
3085                continue;
3086            }
3087            if let Some(min) = flag.var_min {
3088                if count < min {
3089                    out.errors.push(UsageErr::VarFlagTooFew {
3090                        name: flag.name.clone(),
3091                        min,
3092                        got: count,
3093                    });
3094                }
3095            }
3096            if let Some(max) = flag.var_max {
3097                if count > max {
3098                    out.errors.push(UsageErr::VarFlagTooMany {
3099                        name: flag.name.clone(),
3100                        max,
3101                        got: count,
3102                    });
3103                }
3104            }
3105        }
3106    }
3107
3108    Ok((out, overridden_flags))
3109}
3110
3111fn validate_expression(
3112    name: &str,
3113    expression: Option<&str>,
3114    message: Option<&str>,
3115    parsed: &ParseValue,
3116    errors: &mut Vec<UsageErr>,
3117) {
3118    let Some(expression) = expression else {
3119        return;
3120    };
3121    #[cfg(not(feature = "validation"))]
3122    let _ = expression;
3123    let values: &[String] = match parsed {
3124        ParseValue::String(value) => std::slice::from_ref(value),
3125        ParseValue::MultiString(values) => values,
3126        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3127    };
3128    #[cfg(feature = "validation")]
3129    for value in values {
3130        let reason = match usage_validation::validate(expression, value) {
3131            Ok(true) => continue,
3132            Ok(false) => message
3133                .unwrap_or("does not satisfy the validation expression")
3134                .to_string(),
3135            Err(error) => format!("validation expression failed: {error}"),
3136        };
3137        errors.push(UsageErr::InvalidValue {
3138            name: name.to_string(),
3139            value: value.clone(),
3140            reason,
3141        });
3142        break;
3143    }
3144    #[cfg(not(feature = "validation"))]
3145    if let Some(value) = values.first() {
3146        let _ = message;
3147        errors.push(UsageErr::InvalidValue {
3148            name: name.to_string(),
3149            value: value.clone(),
3150            reason: "expression validation requires the `validation` feature".to_string(),
3151        });
3152    }
3153}
3154
3155#[cfg(all(test, not(feature = "validation")))]
3156mod optional_validation_tests {
3157    use crate::{parse, Spec};
3158
3159    #[test]
3160    fn validation_declarations_require_the_opt_in_runtime_feature() {
3161        let spec: Spec = r#"
3162name "ex"
3163bin "ex"
3164arg "<port>" validate="int(value) > 0"
3165        "#
3166        .parse()
3167        .unwrap();
3168        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3169        assert!(
3170            error
3171                .to_string()
3172                .contains("requires the `validation` feature"),
3173            "{error:?}"
3174        );
3175    }
3176}
3177
3178fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3179    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3180}
3181
3182fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3183    overrider
3184        .overrides
3185        .iter()
3186        .any(|selector| flag_matches_selector(overridden, selector))
3187}
3188
3189fn apply_prefix_flag_overrides(
3190    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3191    flag: Arc<SpecFlag>,
3192) {
3193    prefix_flags
3194        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3195}
3196
3197fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3198    prefix_flags
3199        .iter()
3200        .flat_map(|(_, words)| words.iter().cloned())
3201        .collect()
3202}
3203
3204fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3205    match custom_env {
3206        Some(env) => env.contains_key(env_var),
3207        None => std::env::var(env_var).is_ok(),
3208    }
3209}
3210
3211fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3212    flag.env_names()
3213        .any(|env_var| env_contains(custom_env, env_var))
3214}
3215
3216fn fallback_is_true(value: &str) -> bool {
3217    matches!(value, "1" | "true" | "True" | "TRUE")
3218}
3219
3220fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3221    match delimiter {
3222        Some(delimiter) => values
3223            .iter()
3224            .flat_map(|value| value.split(delimiter).map(str::to_string))
3225            .collect(),
3226        None => values.to_vec(),
3227    }
3228}
3229
3230fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3231    if let Some(min) = arg.var_min {
3232        if count < min {
3233            errors.push(UsageErr::VarArgTooFew {
3234                name: arg.name.clone(),
3235                min,
3236                got: count,
3237            });
3238        }
3239    }
3240    if let Some(max) = arg.var_max {
3241        if count > max {
3242            errors.push(UsageErr::VarArgTooMany {
3243                name: arg.name.clone(),
3244                max,
3245                got: count,
3246            });
3247        }
3248    }
3249}
3250
3251fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3252    if let Some(min) = flag.var_min {
3253        if count < min {
3254            errors.push(UsageErr::VarFlagTooFew {
3255                name: flag.name.clone(),
3256                min,
3257                got: count,
3258            });
3259        }
3260    }
3261    if let Some(max) = flag.var_max {
3262        if count > max {
3263            errors.push(UsageErr::VarFlagTooMany {
3264                name: flag.name.clone(),
3265                max,
3266                got: count,
3267            });
3268        }
3269    }
3270}
3271
3272fn validate_flag_arg_fallback_count(
3273    flag: &SpecFlag,
3274    arg: &SpecArg,
3275    count: usize,
3276    errors: &mut Vec<UsageErr>,
3277) {
3278    if let Some(min) = arg.var_min {
3279        if count < min {
3280            errors.push(UsageErr::VarFlagTooFew {
3281                name: flag.name.clone(),
3282                min,
3283                got: count,
3284            });
3285        }
3286    }
3287    if let Some(max) = arg.var_max {
3288        if count > max {
3289            errors.push(UsageErr::VarFlagTooMany {
3290                name: flag.name.clone(),
3291                max,
3292                got: count,
3293            });
3294        }
3295    }
3296}
3297
3298/// Bind a fallback the way an unconditional `default` does: one value, or several
3299/// for `var`, and choices checked the same way.
3300fn bind_flag_fallback(
3301    flag: &Arc<SpecFlag>,
3302    values: &[String],
3303    out: &mut ParseOutput,
3304    custom_env: Option<&HashMap<String, String>>,
3305    origin: ValueOrigin,
3306) -> Result<(), miette::Error> {
3307    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3308        return Ok(());
3309    };
3310    out.flags.insert(Arc::clone(flag), value);
3311    out.flag_origins
3312        .entry(Arc::clone(flag))
3313        .or_default()
3314        .push(origin);
3315    Ok(())
3316}
3317
3318fn flag_fallback_value(
3319    flag: &SpecFlag,
3320    values: &[String],
3321    errors: &mut Vec<UsageErr>,
3322    custom_env: Option<&HashMap<String, String>>,
3323) -> Result<Option<ParseValue>, miette::Error> {
3324    if values.is_empty() {
3325        return Ok(None);
3326    }
3327    if let Some(arg) = flag.arg.as_ref() {
3328        let values = split_fallback_values(values, arg.delimiter);
3329        if flag.var || arg.var {
3330            if flag.var {
3331                validate_flag_fallback_count(flag, values.len(), errors);
3332            }
3333            if arg.var {
3334                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3335            }
3336            validate_choice_values(
3337                ChoiceTarget::option(flag),
3338                &values,
3339                arg.choices.as_ref(),
3340                custom_env,
3341            )?;
3342            Ok(Some(ParseValue::MultiString(values)))
3343        } else {
3344            let value = values.into_iter().next().unwrap_or_default();
3345            validate_choice_value(
3346                ChoiceTarget::option(flag),
3347                &value,
3348                arg.choices.as_ref(),
3349                custom_env,
3350            )?;
3351            Ok(Some(ParseValue::String(value)))
3352        }
3353    } else if flag.var {
3354        validate_flag_fallback_count(flag, values.len(), errors);
3355        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3356        Ok(Some(ParseValue::MultiBool(bools)))
3357    } else {
3358        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3359    }
3360}
3361
3362/// Fill scoped flags inside the clause instances argv created.
3363///
3364/// The returned sets contain argv and environment sources only. Relationship
3365/// validation uses them to keep defaults from becoming explicit conflicts while
3366/// still allowing a fallback value to satisfy a required scoped flag.
3367fn apply_clause_flag_fallbacks(
3368    out: &mut ParseOutput,
3369    overridden_flags: &HashSet<String>,
3370    custom_env: Option<&HashMap<String, String>>,
3371) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3372    let Some(clause) = out.cmd.clause.clone() else {
3373        return Ok(None);
3374    };
3375    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3376        return Ok(None);
3377    };
3378    let positional_instances = positional_instances.clone();
3379    let mut flag_instances = out
3380        .clause_flags
3381        .shift_remove(&clause.name)
3382        .unwrap_or_default();
3383    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3384    let get_env = |key: &str| -> Option<String> {
3385        custom_env
3386            .and_then(|values| values.get(key).cloned())
3387            .or_else(|| {
3388                custom_env
3389                    .is_none()
3390                    .then(|| std::env::var(key).ok())
3391                    .flatten()
3392            })
3393    };
3394    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3395
3396    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3397        let positional = &positional_instances[instance_index];
3398        let mut explicit = scoped
3399            .keys()
3400            .map(|flag| flag.name.clone())
3401            .collect::<HashSet<_>>();
3402
3403        // Environment first so every default_if sees all explicit sources,
3404        // independent of declaration order.
3405        for declared in &clause.flags {
3406            let flag = Arc::new(declared.clone());
3407            if scoped.contains_key(&flag) {
3408                continue;
3409            }
3410            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3411                continue;
3412            };
3413            if let Some(warning) = flag_deprecation(declared) {
3414                out.warnings.push(warning);
3415            }
3416            if flag_env_is_deprecated(declared, env_name) {
3417                out.warnings
3418                    .push(Warning::env(env_name, flag_current_env(declared)));
3419            }
3420            if let Some(value) = flag_fallback_value(
3421                declared,
3422                std::slice::from_ref(&env_value),
3423                &mut out.errors,
3424                custom_env,
3425            )? {
3426                scoped.insert(flag, value);
3427                explicit.insert(declared.name.clone());
3428            }
3429        }
3430
3431        let condition_matches =
3432            |condition: &crate::SpecDefaultIf| {
3433                if let Some(flag) = clause
3434                    .flags
3435                    .iter()
3436                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3437                {
3438                    if !explicit.contains(&flag.name) {
3439                        return false;
3440                    }
3441                    return condition.when.as_ref().is_none_or(|expected| {
3442                        scoped
3443                            .iter()
3444                            .find(|(present, _)| present.name == flag.name)
3445                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3446                    });
3447                }
3448                if let Some(arg) = clause.args.iter().find(|arg| {
3449                    !condition.selector.starts_with('-') && arg.name == condition.selector
3450                }) {
3451                    return positional.get(arg).is_some_and(|value| {
3452                        condition
3453                            .when
3454                            .as_ref()
3455                            .is_none_or(|expected| parse_value_has(value, expected))
3456                    });
3457                }
3458                let command_flag = out
3459                    .available_flags
3460                    .values()
3461                    .chain(out.flags.keys())
3462                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3463                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3464                command_flag.is_some_and(|flag| {
3465                    !overridden_flags.contains(&flag.name)
3466                        && command_flag_has_explicit_source(flag, out)
3467                        && condition.when.as_ref().is_none_or(|expected| {
3468                            out.flags
3469                                .get(flag)
3470                                .is_some_and(|value| parse_value_has(value, expected))
3471                        })
3472                })
3473            };
3474
3475        let conditional = clause
3476            .flags
3477            .iter()
3478            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3479            .filter_map(|flag| {
3480                flag.default_if
3481                    .iter()
3482                    .find(|condition| condition_matches(condition))
3483                    .map(|condition| (flag.clone(), condition.value.clone()))
3484            })
3485            .collect::<Vec<_>>();
3486        for (declared, value) in conditional {
3487            if let Some(value) = flag_fallback_value(
3488                &declared,
3489                std::slice::from_ref(&value),
3490                &mut out.errors,
3491                custom_env,
3492            )? {
3493                scoped.insert(Arc::new(declared), value);
3494            }
3495        }
3496
3497        for declared in &clause.flags {
3498            let flag = Arc::new(declared.clone());
3499            if scoped.contains_key(&flag) {
3500                continue;
3501            }
3502            let values = if !declared.default.is_empty() {
3503                &declared.default
3504            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3505                &arg.default
3506            } else {
3507                continue;
3508            };
3509            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3510            {
3511                scoped.insert(flag, value);
3512            }
3513        }
3514        explicit_instances.push(explicit);
3515    }
3516
3517    out.clause_flags.insert(clause.name, flag_instances);
3518    Ok(Some(explicit_instances))
3519}
3520
3521fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3522    out.flags.contains_key(flag)
3523        && out.flag_origins.get(flag).is_none_or(|origins| {
3524            origins
3525                .iter()
3526                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3527        })
3528}
3529
3530fn default_if_condition_matches(
3531    condition: &crate::SpecDefaultIf,
3532    out: &ParseOutput,
3533    overridden_flags: &HashSet<String>,
3534    custom_env: Option<&HashMap<String, String>>,
3535) -> bool {
3536    match &condition.when {
3537        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3538        Some(when) => {
3539            let Some(flag) = out
3540                .available_flags
3541                .values()
3542                .chain(out.flags.keys())
3543                .find(|flag| flag_matches_selector(flag, &condition.selector))
3544            else {
3545                return false;
3546            };
3547            if overridden_flags.contains(&flag.name) {
3548                return false;
3549            }
3550            explicit_flag_has_value(flag, when, out, custom_env)
3551        }
3552    }
3553}
3554
3555/// Whether an explicitly supplied value of `flag` equals `expected`.
3556///
3557/// clap treats command-line and environment values as explicit for `requires_if`, but
3558/// not defaults. Keep that source distinction here instead of consulting the flag's
3559/// defaults through `selector_is_satisfied`.
3560fn explicit_flag_has_value(
3561    flag: &SpecFlag,
3562    expected: &str,
3563    out: &ParseOutput,
3564    custom_env: Option<&HashMap<String, String>>,
3565) -> bool {
3566    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3567        ParseValue::Bool(value) => value.to_string() == expected,
3568        ParseValue::String(value) => value == expected,
3569        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3570        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3571    });
3572    if out.flags.contains_key(flag) {
3573        return parsed_matches;
3574    }
3575
3576    let value = flag.env_names().find_map(|env| match custom_env {
3577        Some(values) => values.get(env).cloned(),
3578        None => std::env::var(env).ok(),
3579    });
3580    value.is_some_and(
3581        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3582            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3583            None if flag.arg.is_none() => {
3584                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3585            }
3586            None => value == expected,
3587        },
3588    )
3589}
3590
3591fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3592    match value {
3593        ParseValue::Bool(value) => value.to_string() == expected,
3594        ParseValue::String(value) => value == expected,
3595        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3596        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3597    }
3598}
3599
3600/// Clause flags share the command's spelling table so they can be recognized while parsing,
3601/// but their values and validation belong to one clause instance rather than the command.
3602fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3603    out.cmd
3604        .clause
3605        .as_ref()
3606        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3607}
3608
3609fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3610    out.cmd
3611        .clause
3612        .as_ref()?
3613        .flags
3614        .iter()
3615        .find(|flag| flag_matches_selector(flag, selector))
3616}
3617
3618fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3619    out.clause_flags
3620        .values()
3621        .flatten()
3622        .any(|instance| instance.keys().any(|present| present.name == flag.name))
3623        || out.flags.keys().any(|present| present.name == flag.name)
3624}
3625
3626fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3627    out.clause_flags.values().flatten().any(|instance| {
3628        instance
3629            .iter()
3630            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3631    }) || out
3632        .flags
3633        .iter()
3634        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3635}
3636
3637fn validate_clause_relationships(
3638    out: &mut ParseOutput,
3639    overridden_flags: &HashSet<String>,
3640    custom_env: Option<&HashMap<String, String>>,
3641    explicit_instances: Option<&[HashSet<String>]>,
3642) {
3643    let Some(clause) = out.cmd.clause.as_ref() else {
3644        return;
3645    };
3646    let Some(instances) = out.clauses.get(&clause.name) else {
3647        return;
3648    };
3649    let command_flag_is_explicit = |selector: &str| {
3650        out.available_flags
3651            .values()
3652            .chain(out.flags.keys())
3653            .filter(|flag| !is_clause_scoped_flag(out, flag))
3654            .any(|flag| {
3655                flag_matches_selector(flag, selector)
3656                    && !overridden_flags.contains(&flag.name)
3657                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3658            })
3659    };
3660    let command_flag_matches_value = |selector: &str, expected: &str| {
3661        out.available_flags
3662            .values()
3663            .chain(out.flags.keys())
3664            .filter(|flag| !is_clause_scoped_flag(out, flag))
3665            .find(|flag| flag_matches_selector(flag, selector))
3666            .is_some_and(|flag| {
3667                !overridden_flags.contains(&flag.name)
3668                    && explicit_flag_has_value(flag, expected, out, custom_env)
3669            })
3670    };
3671    let command_flag_is_satisfied = |selector: &str| {
3672        out.available_flags
3673            .values()
3674            .chain(out.flags.keys())
3675            .filter(|flag| !is_clause_scoped_flag(out, flag))
3676            .any(|flag| flag_matches_selector(flag, selector))
3677            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3678    };
3679    let mut errors = Vec::new();
3680    for (instance_index, instance) in instances.iter().enumerate() {
3681        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
3682        let scoped = out
3683            .clause_flags
3684            .get(&clause.name)
3685            .and_then(|instances| instances.get(instance_index));
3686        let scoped_flag = |selector: &str| {
3687            clause
3688                .flags
3689                .iter()
3690                .find(|flag| flag_matches_selector(flag, selector))
3691        };
3692        let scoped_value = |flag: &SpecFlag| {
3693            scoped.and_then(|values| {
3694                values
3695                    .iter()
3696                    .find(|(present, _)| present.name == flag.name)
3697                    .map(|(_, value)| value)
3698            })
3699        };
3700        let arg_is_explicit = |selector: &str| {
3701            instance
3702                .keys()
3703                .any(|arg| !selector.starts_with('-') && arg.name == selector)
3704        };
3705        let selector_is_explicit = |selector: &str| {
3706            scoped_flag(selector).is_some_and(|flag| {
3707                explicit
3708                    .map(|given| given.contains(&flag.name))
3709                    .unwrap_or_else(|| scoped_value(flag).is_some())
3710            }) || command_flag_is_explicit(selector)
3711                || arg_is_explicit(selector)
3712        };
3713        let selector_has_value = |selector: &str, expected: &str| {
3714            scoped_flag(selector)
3715                .filter(|flag| {
3716                    explicit
3717                        .map(|given| given.contains(&flag.name))
3718                        .unwrap_or_else(|| scoped_value(flag).is_some())
3719                })
3720                .and_then(scoped_value)
3721                .is_some_and(|value| parse_value_has(value, expected))
3722                || command_flag_matches_value(selector, expected)
3723                || instance.iter().any(|(arg, value)| {
3724                    !selector.starts_with('-')
3725                        && arg.name == selector
3726                        && parse_value_has(value, expected)
3727                })
3728        };
3729        let selector_is_satisfied = |selector: &str| {
3730            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
3731                || command_flag_is_satisfied(selector)
3732                || arg_is_explicit(selector)
3733        };
3734        let selector_name = |selector: &str| {
3735            scoped_flag(selector)
3736                .map(|flag| flag.name.clone())
3737                .or_else(|| selector_flag_name(selector, out))
3738                .unwrap_or_else(|| selector.to_string())
3739        };
3740
3741        for flag in &clause.flags {
3742            let value = scoped_value(flag);
3743            if value.is_none() {
3744                let required_if = flag
3745                    .required_if
3746                    .iter()
3747                    .any(|selector| selector_is_explicit(selector));
3748                let required_if_eq = flag
3749                    .required_if_eq
3750                    .iter()
3751                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
3752                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3753                    && flag
3754                        .required_if_eq_all
3755                        .iter()
3756                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
3757                let unless_any = flag
3758                    .required_unless
3759                    .iter()
3760                    .any(|selector| selector_is_explicit(selector));
3761                let unless_all = !flag.required_unless_all.is_empty()
3762                    && flag
3763                        .required_unless_all
3764                        .iter()
3765                        .all(|selector| selector_is_explicit(selector));
3766                let required_unless = !(unless_any
3767                    || unless_all
3768                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3769                if flag.required
3770                    || required_if
3771                    || required_if_eq
3772                    || required_if_eq_all
3773                    || required_unless
3774                {
3775                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
3776                }
3777                continue;
3778            }
3779
3780            let explicitly_provided = explicit
3781                .map(|given| given.contains(&flag.name))
3782                .unwrap_or(true);
3783
3784            if explicitly_provided {
3785                for other in &flag.conflicts {
3786                    if selector_is_explicit(other) {
3787                        errors.push(UsageErr::InvalidFlag {
3788                            token: format!("--{}", flag.name),
3789                            reason: format!("conflicts with {other}"),
3790                            span: (0, 0).into(),
3791                            input: format!("--{} {other}", flag.name),
3792                        });
3793                    }
3794                }
3795                for other in &flag.requires {
3796                    if !selector_is_satisfied(other) {
3797                        errors.push(UsageErr::MissingFlag(selector_name(other)));
3798                    }
3799                }
3800                for condition in &flag.requires_if {
3801                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
3802                        && !selector_is_satisfied(&condition.requires)
3803                    {
3804                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
3805                    }
3806                }
3807            }
3808            if let (true, Some(value)) = (flag.var, value) {
3809                let count = match value {
3810                    ParseValue::MultiString(values) => values.len(),
3811                    ParseValue::MultiBool(values) => values.len(),
3812                    _ => 1,
3813                };
3814                if let Some(min) = flag.var_min.filter(|min| count < *min) {
3815                    errors.push(UsageErr::VarFlagTooFew {
3816                        name: flag.name.clone(),
3817                        min,
3818                        got: count,
3819                    });
3820                }
3821                if let Some(max) = flag.var_max.filter(|max| count > *max) {
3822                    errors.push(UsageErr::VarFlagTooMany {
3823                        name: flag.name.clone(),
3824                        max,
3825                        got: count,
3826                    });
3827                }
3828            }
3829            if let (Some(arg), Some(value)) = (&flag.arg, value) {
3830                validate_expression(
3831                    &flag.name,
3832                    arg.validate.as_deref(),
3833                    arg.validate_error.as_deref(),
3834                    value,
3835                    &mut errors,
3836                );
3837            }
3838        }
3839        for arg in &clause.args {
3840            let given = instance.keys().any(|present| present.name == arg.name);
3841            if !given {
3842                let required_if = arg
3843                    .required_if
3844                    .iter()
3845                    .any(|selector| selector_is_explicit(selector));
3846                let required_if_eq = arg
3847                    .required_if_eq
3848                    .iter()
3849                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
3850                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3851                    && arg
3852                        .required_if_eq_all
3853                        .iter()
3854                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
3855                let unless_any = arg
3856                    .required_unless
3857                    .iter()
3858                    .any(|selector| selector_is_explicit(selector));
3859                let unless_all = !arg.required_unless_all.is_empty()
3860                    && arg
3861                        .required_unless_all
3862                        .iter()
3863                        .all(|selector| selector_is_explicit(selector));
3864                let required_unless = !(unless_any
3865                    || unless_all
3866                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3867                if required_if || required_if_eq || required_if_eq_all || required_unless {
3868                    errors.push(UsageErr::MissingClauseArg {
3869                        clause: clause.name.clone(),
3870                        instance: instance_index + 1,
3871                        arg: arg.name.clone(),
3872                    });
3873                }
3874                continue;
3875            }
3876            for other in &arg.conflicts {
3877                if selector_is_explicit(other) {
3878                    errors.push(UsageErr::InvalidFlag {
3879                        token: arg.name.clone(),
3880                        reason: format!("conflicts with {other}"),
3881                        span: (0, 0).into(),
3882                        input: format!("{} {other}", arg.name),
3883                    });
3884                }
3885            }
3886            for other in &arg.requires {
3887                if selector_is_satisfied(other) {
3888                    continue;
3889                }
3890                if other.starts_with('-') {
3891                    errors.push(UsageErr::MissingFlag(selector_name(other)));
3892                } else {
3893                    errors.push(UsageErr::MissingClauseArg {
3894                        clause: clause.name.clone(),
3895                        instance: instance_index + 1,
3896                        arg: other.clone(),
3897                    });
3898                }
3899            }
3900        }
3901    }
3902    out.errors.extend(errors);
3903}
3904
3905fn selector_explicit_has_value(
3906    selector: &str,
3907    expected: &str,
3908    out: &ParseOutput,
3909    overridden_flags: &HashSet<String>,
3910    custom_env: Option<&HashMap<String, String>>,
3911) -> bool {
3912    if let Some(flag) = selected_clause_flag(out, selector) {
3913        return clause_flag_has_value(out, flag, expected);
3914    }
3915    if let Some(flag) = out
3916        .available_flags
3917        .values()
3918        .chain(out.flags.keys())
3919        .filter(|flag| !is_clause_scoped_flag(out, flag))
3920        .find(|flag| flag_matches_selector(flag, selector))
3921    {
3922        return !overridden_flags.contains(&flag.name)
3923            && explicit_flag_has_value(flag, expected, out, custom_env);
3924    }
3925    let Some(arg) = selector_arg(selector, out) else {
3926        return false;
3927    };
3928    let parsed = out
3929        .args
3930        .iter()
3931        .find(|(given, _)| given.name == arg.name)
3932        .map(|(_, value)| value)
3933        .or_else(|| {
3934            out.clauses.values().flatten().find_map(|instance| {
3935                instance
3936                    .iter()
3937                    .find(|(given, _)| given.name == arg.name)
3938                    .map(|(_, value)| value)
3939            })
3940        });
3941    if let Some(value) = parsed {
3942        return match value {
3943            ParseValue::String(value) => value == expected,
3944            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3945            ParseValue::Bool(value) => value.to_string() == expected,
3946            ParseValue::MultiBool(values) => {
3947                values.iter().any(|value| value.to_string() == expected)
3948            }
3949        };
3950    }
3951    let value = arg.env_names().find_map(|env| match custom_env {
3952        Some(values) => values.get(env).cloned(),
3953        None => std::env::var(env).ok(),
3954    });
3955    value.is_some_and(|value| match arg.delimiter {
3956        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3957        None => value == expected,
3958    })
3959}
3960
3961fn selector_is_explicit(
3962    selector: &str,
3963    out: &ParseOutput,
3964    overridden_flags: &HashSet<String>,
3965    custom_env: Option<&HashMap<String, String>>,
3966) -> bool {
3967    let scoped_flag_is_explicit =
3968        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
3969    let flag_is_explicit = out
3970        .available_flags
3971        .values()
3972        .chain(out.flags.keys())
3973        .filter(|flag| !is_clause_scoped_flag(out, flag))
3974        .any(|flag| {
3975            flag_matches_selector(flag, selector)
3976                && !overridden_flags.contains(&flag.name)
3977                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3978        });
3979    scoped_flag_is_explicit
3980        || flag_is_explicit
3981        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3982}
3983
3984/// The name of the flag a selector points at, for an error that has to name it.
3985///
3986/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3987/// about a flag that is *missing* has to say which one, and the selector may be a short
3988/// form or an alias rather than the name.
3989fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3990    if let Some(flag) = selected_clause_flag(out, selector) {
3991        return Some(flag.name.clone());
3992    }
3993    out.available_flags
3994        .values()
3995        .chain(out.flags.keys())
3996        .filter(|flag| !is_clause_scoped_flag(out, flag))
3997        .find(|flag| flag_matches_selector(flag, selector))
3998        .map(|flag| flag.name.clone())
3999        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4000}
4001
4002/// Whether a selector's flag ended up with a value, however it got one.
4003///
4004/// The rule for a *positive* relationship, and the difference from
4005/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4006/// exclusivity — has to count only what was given, or a flag with a default would
4007/// conflict with everything and no command line would parse. A positive one asks whether
4008/// the flag it names has a value, and a default is a value: that is already how plain
4009/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4010/// otherwise would have made the same flag missing here and present ten lines below.
4011fn selector_is_satisfied(
4012    selector: &str,
4013    out: &ParseOutput,
4014    overridden_flags: &HashSet<String>,
4015    custom_env: Option<&HashMap<String, String>>,
4016) -> bool {
4017    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4018        return true;
4019    }
4020    let flag_is_satisfied = out
4021        .available_flags
4022        .values()
4023        .chain(out.flags.keys())
4024        .filter(|flag| !is_clause_scoped_flag(out, flag))
4025        .filter(|flag| flag_matches_selector(flag, selector))
4026        .any(|flag| {
4027            !overridden_flags.contains(&flag.name)
4028                && (!flag.default.is_empty()
4029                    || flag.arg.iter().any(|a| !a.default.is_empty())
4030                    || flag.default_if.iter().any(|condition| {
4031                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4032                    }))
4033        });
4034    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4035}
4036
4037fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4038    // Bare words are positional selectors. Keep accepting a flag's internal name above
4039    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4040    if selector.starts_with('-') {
4041        return None;
4042    }
4043    out.cmds
4044        .iter()
4045        .flat_map(active_args)
4046        .find(|arg| arg.name == selector)
4047}
4048
4049fn arg_is_explicit(
4050    arg: &SpecArg,
4051    out: &ParseOutput,
4052    custom_env: Option<&HashMap<String, String>>,
4053) -> bool {
4054    out.args.keys().any(|given| given.name == arg.name)
4055        || out
4056            .clauses
4057            .values()
4058            .flatten()
4059            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4060        || arg
4061            .env
4062            .as_ref()
4063            .is_some_and(|env| env_contains(custom_env, env))
4064}
4065
4066fn apply_flag_overrides(
4067    flag: &Arc<SpecFlag>,
4068    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4069    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4070    pending_flags: &mut Vec<Arc<SpecFlag>>,
4071    overridden_flags: &mut HashSet<String>,
4072    // The reportable half of the same fact: which flag did the overriding. The set above
4073    // only stops a default or an environment value restoring what was overridden, and
4074    // "`--quiet` is unset despite its default" has no answer without the name.
4075    attributed: &mut BTreeMap<String, String>,
4076) {
4077    let overridden_names: HashSet<String> = available_flags
4078        .values()
4079        .chain(parsed_flags.keys())
4080        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4081        .map(|other| other.name.clone())
4082        .collect();
4083
4084    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4085    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4086    for name in &overridden_names {
4087        attributed.insert(name.clone(), flag.name.clone());
4088    }
4089    overridden_flags.extend(overridden_names);
4090    // An explicit occurrence always restores this flag, including self-overrides.
4091    overridden_flags.remove(&flag.name);
4092    attributed.remove(&flag.name);
4093}
4094
4095#[cfg(feature = "cli-help")]
4096fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
4097    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
4098}
4099
4100#[cfg(feature = "cli-help")]
4101fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
4102    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
4103        if !out.is_empty() {
4104            out.push('\n');
4105        }
4106        out.push_str(&docs::cli::render_help(spec, cmd, true));
4107        let mut children: Vec<_> = cmd
4108            .subcommands
4109            .values()
4110            .filter(|child| !child.hide)
4111            .collect();
4112        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4113        for child in children {
4114            append(out, spec, child);
4115        }
4116    }
4117
4118    let mut out = String::new();
4119    append(&mut out, spec, cmd);
4120    UsageErr::Help(out)
4121}
4122
4123#[cfg(not(feature = "cli-help"))]
4124fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
4125    UsageErr::Help("help".to_string())
4126}
4127
4128#[cfg(not(feature = "cli-help"))]
4129fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
4130    UsageErr::Help("help".to_string())
4131}
4132
4133/// The version to answer with. `--version` prefers the long text and `-V` the concise
4134/// one, each falling back to the other when only one is declared.
4135fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4136    let value = if long {
4137        spec.long_version.as_ref().or(spec.version.as_ref())
4138    } else {
4139        spec.version.as_ref().or(spec.long_version.as_ref())
4140    };
4141    UsageErr::Version(value.cloned().unwrap_or_default())
4142}
4143
4144fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
4145    use crate::SpecFlagAction;
4146    match flag.action {
4147        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
4148        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
4149        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
4150        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
4151        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4152        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4153    }
4154}
4155
4156/// Report a required flag value that was displaced by a later option.
4157fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4158    let token = flag
4159        .long
4160        .first()
4161        .map(|long| format!("--{long}"))
4162        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4163        .unwrap_or_else(|| flag.name.clone());
4164    UsageErr::InvalidFlag {
4165        token: token.clone(),
4166        reason: "requires an argument".to_string(),
4167        span: (0, 0).into(),
4168        input: format!("{token} {following}"),
4169    }
4170}
4171
4172#[derive(Copy, Clone)]
4173struct ChoiceTarget<'a> {
4174    kind: &'a str,
4175    name: &'a str,
4176}
4177
4178impl<'a> ChoiceTarget<'a> {
4179    fn arg(arg: &'a SpecArg) -> Self {
4180        Self {
4181            kind: "arg",
4182            name: &arg.name,
4183        }
4184    }
4185
4186    fn option(flag: &'a SpecFlag) -> Self {
4187        Self {
4188            kind: "option",
4189            name: &flag.name,
4190        }
4191    }
4192}
4193
4194/// Whether every letter of a short token names a flag in scope.
4195///
4196/// Scanning stops at the first letter whose flag takes a value, because everything
4197/// after it is that value rather than more letters.
4198fn short_bundle_is_known(
4199    spec: &Spec,
4200    cmds: &[SpecCommand],
4201    available: &BTreeMap<String, Arc<SpecFlag>>,
4202    token: &str,
4203) -> bool {
4204    for c in token.chars().skip(1) {
4205        match available.get(&format!("-{c}")) {
4206            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4207            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4208            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4209            // clap all answer it with help.
4210            None if supplied_short(spec, cmds, c).is_some() => {}
4211            None => return false,
4212            Some(f) if f.arg.is_some() => return true,
4213            Some(_) => {}
4214        }
4215    }
4216    true
4217}
4218
4219/// The response `-h` or `-V` produces where nothing declares that letter.
4220///
4221/// The letter form of the flags the parser supplies rather than a spec declaring them,
4222/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4223/// about here one letter at a time, because a bundle is read one letter at a time.
4224///
4225/// Always the short response: `-h` is short help however many letters share its token,
4226/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4227/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4228fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
4229    let cmd = cmds.last()?;
4230    match letter {
4231        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
4232        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4233        _ => None,
4234    }
4235}
4236
4237/// Refuse a flag-like token that named nothing, if this command asked for that.
4238///
4239/// Called from the flag branches, where the lookup has just failed and nothing from
4240/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4241/// than after setting `-a`.
4242fn reject_unknown_flag_if_asked(
4243    spec: &Spec,
4244    path: &[SpecCommand],
4245    token: &str,
4246) -> Result<(), UsageErr> {
4247    // A lone `-` is a value by convention. A negative number reaches this only
4248    // when no pending value opted into the narrower exception.
4249    if !is_flag_like(token) {
4250        return Ok(());
4251    }
4252    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4253        return Ok(());
4254    }
4255    Err(UsageErr::InvalidFlag {
4256        token: token.to_string(),
4257        reason: "no such flag".to_string(),
4258        span: (0, 0).into(),
4259        input: token.to_string(),
4260    })
4261}
4262
4263/// Whether a flag-like token that matches nothing is a value or an error, here.
4264///
4265/// The nearest enclosing command that stated a preference wins, then the spec,
4266/// then the default. Inherited, unlike `effect`: it describes how a command line
4267/// is read, and a CLI that forwards options tends to forward them at every level.
4268fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4269    path.iter()
4270        .rev()
4271        .find_map(|cmd| cmd.unknown_flags)
4272        .or(spec.unknown_flags)
4273        .unwrap_or_default()
4274}
4275
4276/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4277/// ones.
4278///
4279/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4280/// a field may make the narrower negative-number exception.
4281fn is_flag_like(token: &str) -> bool {
4282    match token.strip_prefix('-') {
4283        None | Some("") => false,
4284        Some(_) => true,
4285    }
4286}
4287
4288fn is_negative_number(token: &str) -> bool {
4289    token.strip_prefix('-').is_some_and(is_number)
4290}
4291
4292/// Whether a flag may claim its following token as a detached value.
4293///
4294/// A required value keeps the historical negative-number exception. A value
4295/// that may be omitted needs an explicit opt-in to distinguish a negative value
4296/// from the flag's bare form.
4297fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4298    !flag.require_equals
4299        && (!is_flag_like(token)
4300            || flag.allow_hyphen_values()
4301            || (is_negative_number(token)
4302                && (flag
4303                    .arg
4304                    .as_ref()
4305                    .is_some_and(|arg| arg.allow_negative_numbers)
4306                    || (flag.default_missing.is_none() && !flag.value_optional))))
4307}
4308
4309fn record_scalar_flag_occurrence(
4310    cmds: &[SpecCommand],
4311    flag: &Arc<SpecFlag>,
4312    command_level: usize,
4313    bool_value: Option<bool>,
4314    occurrences: &mut HashMap<(usize, usize), u8>,
4315    errors: &mut Vec<UsageErr>,
4316) {
4317    let strict = cmds.get(command_level).is_some_and(|cmd| {
4318        !cmd.args_override_self
4319            || cmd
4320                .clause
4321                .as_ref()
4322                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4323    });
4324    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4325    if !strict || flag.count || collects_values {
4326        return;
4327    }
4328
4329    let bit = match bool_value {
4330        Some(false) if flag.negate.is_some() => 0b10,
4331        _ => 0b01,
4332    };
4333    let key = (Arc::as_ptr(flag) as usize, command_level);
4334    let seen = occurrences.entry(key).or_default();
4335    if *seen & bit != 0 {
4336        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4337    }
4338    *seen |= bit;
4339}
4340
4341/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4342/// external command.
4343///
4344/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4345/// so it was never a candidate to *select* anything either. usage-argv uses the
4346/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4347/// treated every token that `starts_with('-')` as a flag.
4348fn is_command_word(token: &str) -> bool {
4349    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4350}
4351
4352/// Whether an otherwise numeric-looking token is an exact declared short flag.
4353///
4354/// This check belongs in both parse phases: phase 1 must skip the flag while it
4355/// searches for a later subcommand, and phase 2 must bind it instead of offering
4356/// it to an `allow_negative_numbers` positional.
4357fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4358    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4359}
4360
4361/// Whether an unmatched word belongs to the root's default command.
4362///
4363/// Ordinary words always do. A negative number only does when the default command's
4364/// first positional explicitly accepts one; otherwise it stays at the root, matching
4365/// usage-argv and generated Go.
4366fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4367    !is_negative_number(token)
4368        || cmd
4369            .find_subcommand(default_name)
4370            .and_then(|default| default.args.first())
4371            .is_some_and(|arg| arg.allow_negative_numbers)
4372}
4373
4374/// Digits, at most one `.`, and an optional exponent.
4375///
4376/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4377/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4378/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4379/// two disagreed about `-1e5` when one used a float parse and the other did not.
4380fn is_number(rest: &str) -> bool {
4381    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4382        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4383        None => (rest, None),
4384    };
4385
4386    let mut seen_digit = false;
4387    let mut seen_dot = false;
4388    for c in mantissa.chars() {
4389        match c {
4390            '0'..='9' => seen_digit = true,
4391            '.' if !seen_dot => seen_dot = true,
4392            _ => return false,
4393        }
4394    }
4395    if !seen_digit {
4396        return false;
4397    }
4398
4399    match exponent {
4400        None => true,
4401        Some(exp) => {
4402            let digits = exp
4403                .strip_prefix('+')
4404                .or_else(|| exp.strip_prefix('-'))
4405                .unwrap_or(exp);
4406            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4407        }
4408    }
4409}
4410
4411/// Bind one value to the flag waiting for it, and let a variadic argument go on
4412/// collecting from the words that follow.
4413///
4414/// Every route to a flag's value comes through here — the following word, the text
4415/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4416/// like — so that all three agree on how many values the flag ends up with.
4417#[allow(clippy::too_many_arguments)]
4418fn bind_pending_flag_value(
4419    spec: &Spec,
4420    cmd: &SpecCommand,
4421    errors: &mut Vec<UsageErr>,
4422    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4423    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4424    word: &mut String,
4425    input: &mut VecDeque<Token>,
4426    custom_env: Option<&HashMap<String, String>>,
4427    trace: &mut Trace,
4428    // Which token supplied `word`, and whether it rode along on the flag's own token
4429    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4430    // their own positions and are recorded where they are read.
4431    argv: usize,
4432    attached: bool,
4433) -> miette::Result<bool> {
4434    // Held before the drain pops it, along with what the flag is already carrying: a
4435    // `var_max` bounds the values this occurrence takes, not the list they are appended
4436    // to, so a second `--include` starts counting again.
4437    let collecting = flag_awaiting_value
4438        .last()
4439        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4440        .cloned()
4441        .map(|flag| {
4442            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4443            (flag, carried)
4444        });
4445    let mut bound = vec![];
4446    let refused = drain_pending_flag_values(
4447        spec,
4448        cmd,
4449        errors,
4450        flags,
4451        flag_awaiting_value,
4452        word,
4453        custom_env,
4454        &mut bound,
4455    )?;
4456    for (flag, values) in bound {
4457        trace.record(
4458            argv,
4459            TokenRole::Value {
4460                flag,
4461                values,
4462                attached,
4463            },
4464        );
4465    }
4466    if refused {
4467        return Ok(true);
4468    }
4469    let Some((flag, carried)) = collecting else {
4470        return Ok(false);
4471    };
4472    collect_variadic_flag_values(
4473        spec,
4474        cmd,
4475        errors,
4476        flags,
4477        flag_awaiting_value,
4478        &flag,
4479        carried,
4480        input,
4481        custom_env,
4482        trace,
4483    )
4484}
4485
4486/// Keep feeding a flag whose argument is variadic from the words that follow it.
4487///
4488/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4489/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4490/// This is greedy by design — a command declaring both such a flag and positionals will
4491/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4492///
4493/// `carried` is what the flag already held when this occurrence began, so the bound
4494/// counts this run rather than everything the flag has collected across the command
4495/// line. Each value goes through the same drain as the first, so choices are checked
4496/// and the value lands in the same list rather than by a second route that could
4497/// disagree.
4498#[allow(clippy::too_many_arguments)]
4499fn collect_variadic_flag_values(
4500    spec: &Spec,
4501    cmd: &SpecCommand,
4502    errors: &mut Vec<UsageErr>,
4503    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4504    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4505    flag: &Arc<SpecFlag>,
4506    carried: usize,
4507    input: &mut VecDeque<Token>,
4508    custom_env: Option<&HashMap<String, String>>,
4509    trace: &mut Trace,
4510) -> miette::Result<bool> {
4511    let max = flag
4512        .arg
4513        .as_ref()
4514        .and_then(|arg| arg.var_max)
4515        .unwrap_or(usize::MAX);
4516    while flags
4517        .get(flag)
4518        .map(value_count)
4519        .unwrap_or(0)
4520        .saturating_sub(carried)
4521        < max
4522    {
4523        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4524            break;
4525        };
4526        if flag
4527            .arg
4528            .as_ref()
4529            .and_then(|arg| arg.value_terminator.as_deref())
4530            == Some(next)
4531        {
4532            let terminator = input.pop_front().unwrap();
4533            trace.record(
4534                terminator.argv,
4535                TokenRole::ValueTerminator {
4536                    ends: flag.name.clone(),
4537                },
4538            );
4539            break;
4540        }
4541        // The separator is left where it is: stopping here hands it to the arm that
4542        // knows what it means, rather than reading it as one more value.
4543        if next == "--"
4544            || (is_flag_like(next)
4545                && !(flag
4546                    .arg
4547                    .as_ref()
4548                    .is_some_and(|arg| arg.allow_negative_numbers)
4549                    && is_negative_number(next)))
4550        {
4551            break;
4552        }
4553        let taken = input.pop_front().unwrap();
4554        let argv = taken.argv;
4555        let mut word = taken.word;
4556        flag_awaiting_value.push(Arc::clone(flag));
4557        let mut bound = vec![];
4558        let refused = drain_pending_flag_values(
4559            spec,
4560            cmd,
4561            errors,
4562            flags,
4563            flag_awaiting_value,
4564            &mut word,
4565            custom_env,
4566            &mut bound,
4567        )?;
4568        for (flag, values) in bound {
4569            // A later word of the same occurrence is its own token, and never attached:
4570            // only the first value can ride along on the flag.
4571            trace.record(
4572                argv,
4573                TokenRole::Value {
4574                    flag,
4575                    values,
4576                    attached: false,
4577                },
4578            );
4579        }
4580        if refused {
4581            return Ok(true);
4582        }
4583    }
4584    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4585    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4586    // several values, so the run can end up *past* the bound rather than on it, and stopping
4587    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4588    // case — three values out of the one word the loop was entitled to take.
4589    //
4590    // Counted against `carried` like the loop itself, so this stays a statement about the
4591    // occurrence rather than about the list the occurrences build up.
4592    let taken = flags
4593        .get(flag)
4594        .map(value_count)
4595        .unwrap_or(0)
4596        .saturating_sub(carried);
4597    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4598        if taken < min {
4599            errors.push(UsageErr::VarFlagTooFew {
4600                name: flag.name.clone(),
4601                min,
4602                got: taken,
4603            });
4604        }
4605    }
4606    if taken > max {
4607        errors.push(UsageErr::VarFlagTooMany {
4608            name: flag.name.clone(),
4609            max,
4610            got: taken,
4611        });
4612    }
4613    Ok(false)
4614}
4615
4616/// How many values a flag is holding, for a bound that counts them.
4617fn value_count(value: &ParseValue) -> usize {
4618    match value {
4619        ParseValue::MultiString(values) => values.len(),
4620        ParseValue::MultiBool(values) => values.len(),
4621        _ => 1,
4622    }
4623}
4624
4625/// Finish a value-optional flag that was given with no value.
4626///
4627/// Returns whether anything was bound. Completions keep the flag waiting — a
4628/// half-typed `--color ` is a question about the value — so this is asked only
4629/// once a full parse has decided the value is not coming, or once the next token
4630/// has made that decision.
4631///
4632/// The missing string is a real value: if the flag names `choices`, it has to
4633/// be one of them, the same way an env var or a `default` is checked. Binding
4634/// first and failing later would leave the flag set to a value the spec forbids.
4635fn try_bind_default_missing(
4636    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4637    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4638    custom_env: Option<&HashMap<String, String>>,
4639    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4640) -> miette::Result<bool> {
4641    let Some(flag) = flag_awaiting_value.last() else {
4642        return Ok(false);
4643    };
4644    let value = match flag.default_missing.clone() {
4645        Some(value) => value,
4646        None if flag.value_optional => {
4647            let flag = flag_awaiting_value.pop().unwrap();
4648            // Presence in the map distinguishes this from an absent flag; an
4649            // empty collection distinguishes it from an explicitly empty
4650            // `--flag=` string without inventing a sentinel value.
4651            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4652            origins
4653                .entry(Arc::clone(&flag))
4654                .or_default()
4655                .push(ValueOrigin::DefaultMissing);
4656            if flag.var {
4657                // A repeated bare occurrence is still an occurrence. The string collection
4658                // uses an empty value for it, just as the concrete `default_missing` path
4659                // pushes one value per occurrence; otherwise bounds and consumers silently
4660                // lose every bare repeat after the first.
4661                flags
4662                    .entry(flag)
4663                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4664                    .try_as_multi_string_mut()
4665                    .unwrap()
4666                    .push(String::new());
4667            } else if variadic_value {
4668                // A variadic occurrence stays pending after each value. Reaching the next
4669                // flag (or EOF) closes that same occurrence; it must not erase what it took.
4670                flags
4671                    .entry(flag)
4672                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4673            } else {
4674                // A scalar pending here is a new bare occurrence. The normal permissive
4675                // repeat policy makes the later occurrence a correction, including a
4676                // correction from an explicit value back to the bare tri-state.
4677                flags.insert(flag, ParseValue::MultiString(Vec::new()));
4678            }
4679            return Ok(true);
4680        }
4681        None => return Ok(false),
4682    };
4683    if let Some(arg) = flag.arg.as_ref() {
4684        validate_choice_value(
4685            ChoiceTarget::option(flag),
4686            &value,
4687            arg.choices.as_ref(),
4688            custom_env,
4689        )?;
4690    }
4691    let flag = flag_awaiting_value.pop().unwrap();
4692    origins
4693        .entry(Arc::clone(&flag))
4694        .or_default()
4695        .push(ValueOrigin::DefaultMissing);
4696    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4697    if collecting {
4698        let arr = flags
4699            .entry(flag)
4700            .or_insert_with(|| ParseValue::MultiString(vec![]))
4701            .try_as_multi_string_mut()
4702            .unwrap();
4703        arr.push(value);
4704    } else {
4705        flags.insert(flag, ParseValue::String(value));
4706    }
4707    Ok(true)
4708}
4709
4710/// `bound` collects what each drained flag took, in the order it took it. The values are
4711/// the word after any `delimiter` split, which is the only place that split is known: by the
4712/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
4713/// second occurrence has appended to the same list.
4714#[allow(clippy::too_many_arguments)]
4715fn drain_pending_flag_values(
4716    spec: &Spec,
4717    cmd: &SpecCommand,
4718    errors: &mut Vec<UsageErr>,
4719    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4720    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4721    word: &mut String,
4722    custom_env: Option<&HashMap<String, String>>,
4723    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
4724) -> miette::Result<bool> {
4725    while let Some(flag) = flag_awaiting_value.pop() {
4726        let arg = flag.arg.as_ref().unwrap();
4727        // Split before anything judges the word, because after the split it is no longer
4728        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
4729        // Judging first would reject the whole word against a list neither half is on.
4730        let parts: Vec<String> = match arg.delimiter {
4731            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
4732            None => vec![std::mem::take(word)],
4733        };
4734        for part in &parts {
4735            if validate_choices(
4736                spec,
4737                cmd,
4738                errors,
4739                ChoiceTarget::option(&flag),
4740                part,
4741                arg.choices.as_ref(),
4742                custom_env,
4743            )? {
4744                return Ok(true);
4745            }
4746        }
4747        word.clear();
4748        bound.push((Arc::clone(&flag), parts.clone()));
4749        // Two ways to hold several values, and both record a list: a `var` flag
4750        // collects one per occurrence, a variadic argument collects several from one.
4751        if flag.var || arg.var {
4752            let arr = flags
4753                .entry(flag)
4754                .or_insert_with(|| ParseValue::MultiString(vec![]))
4755                .try_as_multi_string_mut()
4756                .unwrap();
4757            arr.extend(parts);
4758        } else {
4759            // Nowhere for a second value to go, so the word stands as it was typed. A
4760            // delimiter on a flag that takes one value is refused where it is written.
4761            flags.insert(
4762                flag,
4763                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
4764            );
4765        }
4766    }
4767    Ok(false)
4768}
4769
4770fn choice_error(
4771    target: ChoiceTarget<'_>,
4772    value: &str,
4773    choices: Option<&SpecChoices>,
4774    custom_env: Option<&HashMap<String, String>>,
4775) -> Option<String> {
4776    let choices = choices?;
4777    if !choices.strict {
4778        return None;
4779    }
4780    let values = choices.values_with_env(custom_env);
4781    if choices.matches_with_env(value, custom_env) {
4782        return None;
4783    }
4784    if let Some(env) = choices.env() {
4785        if values.is_empty() {
4786            return Some(format!(
4787                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
4788                target.kind, target.name,
4789            ));
4790        }
4791    }
4792    Some(format!(
4793        "Invalid choice for {} {}: {value}, expected one of {}",
4794        target.kind,
4795        target.name,
4796        values.join(", ")
4797    ))
4798}
4799
4800fn validate_choices(
4801    spec: &Spec,
4802    cmd: &SpecCommand,
4803    errors: &mut Vec<UsageErr>,
4804    target: ChoiceTarget<'_>,
4805    value: &str,
4806    choices: Option<&SpecChoices>,
4807    custom_env: Option<&HashMap<String, String>>,
4808) -> miette::Result<bool> {
4809    if is_help_arg(spec, cmd, value)
4810        && choices
4811            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
4812    {
4813        errors.push(render_help_err(spec, cmd, value.len() > 2));
4814        return Ok(true);
4815    }
4816
4817    if let Some(err) = choice_error(target, value, choices, custom_env) {
4818        bail!("{err}");
4819    }
4820    Ok(false)
4821}
4822
4823fn validate_choice_value(
4824    target: ChoiceTarget<'_>,
4825    value: &str,
4826    choices: Option<&SpecChoices>,
4827    custom_env: Option<&HashMap<String, String>>,
4828) -> miette::Result<()> {
4829    if let Some(err) = choice_error(target, value, choices, custom_env) {
4830        bail!("{err}");
4831    }
4832    Ok(())
4833}
4834
4835fn validate_choice_values(
4836    target: ChoiceTarget<'_>,
4837    values: &[String],
4838    choices: Option<&SpecChoices>,
4839    custom_env: Option<&HashMap<String, String>>,
4840) -> miette::Result<()> {
4841    for value in values {
4842        validate_choice_value(target, value, choices, custom_env)?;
4843    }
4844    Ok(())
4845}
4846
4847/// Everything a parse records about where it stopped: the positional cursor, so callers that
4848/// do not re-run the parse — completions, above all — agree with it, and the token trace.
4849///
4850/// Every exit from the binding phase comes through here, which is what makes it the right
4851/// place to close the trace: whatever is still queued was never read, and saying so is more
4852/// useful than leaving those words out of the report entirely.
4853fn record_stop(
4854    out: &mut ParseOutput,
4855    next_arg_idx: usize,
4856    seen_double_dash: bool,
4857    trace: &mut Trace,
4858    unread: &VecDeque<Token>,
4859) {
4860    out.next_arg = out
4861        .cmd
4862        .args
4863        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
4864        .cloned()
4865        .map(Arc::new);
4866    out.double_dash_seen = seen_double_dash;
4867    finalize_current_clause(out);
4868    trace.close(unread);
4869    out.tokens = std::mem::take(&mut trace.tokens);
4870}
4871
4872fn finalize_current_clause(out: &mut ParseOutput) {
4873    let Some(clause) = out.cmd.clause.as_ref() else {
4874        return;
4875    };
4876    let has_scoped_flags = clause
4877        .flags
4878        .iter()
4879        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
4880    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
4881        return;
4882    }
4883    out.clauses
4884        .entry(clause.name.clone())
4885        .or_default()
4886        .push(std::mem::take(&mut out.args));
4887    let mut flags = IndexMap::new();
4888    for clause_flag in &clause.flags {
4889        let key = Arc::new(clause_flag.clone());
4890        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
4891            flags.insert(flag, value);
4892        }
4893    }
4894    out.clause_flags
4895        .entry(clause.name.clone())
4896        .or_default()
4897        .push(flags);
4898}
4899
4900fn restore_current_clause(out: &mut ParseOutput) {
4901    let Some(clause) = out.cmd.clause.as_ref() else {
4902        return;
4903    };
4904    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
4905        out.args = current;
4906    }
4907    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
4908        out.flags.extend(current);
4909    }
4910}
4911
4912fn reset_clause_scalar_occurrences(
4913    out: &ParseOutput,
4914    occurrences: &mut HashMap<(usize, usize), u8>,
4915) {
4916    let Some(clause) = out.cmd.clause.as_ref() else {
4917        return;
4918    };
4919    let names = clause
4920        .flags
4921        .iter()
4922        .map(|flag| flag.name.as_str())
4923        .collect::<HashSet<_>>();
4924    let pointers = unique_flags(out.available_flags.values())
4925        .filter(|flag| names.contains(flag.name.as_str()))
4926        .map(|flag| Arc::as_ptr(flag) as usize)
4927        .collect::<HashSet<_>>();
4928    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
4929}
4930
4931fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
4932    while active_args(cmd)
4933        .get(idx)
4934        .is_some_and(|arg| arg.sigil.is_some())
4935    {
4936        idx += 1;
4937    }
4938    idx
4939}
4940
4941fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
4942    cmd.clause
4943        .as_ref()
4944        .map(|clause| clause.args.as_slice())
4945        .unwrap_or(cmd.args.as_slice())
4946}
4947
4948fn match_sigil_arg<'a>(
4949    cmd: &'a SpecCommand,
4950    word: &'a str,
4951) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4952    cmd.args
4953        .iter()
4954        .filter_map(|arg| {
4955            let sigil = arg.sigil.as_deref()?;
4956            let value = word.strip_prefix(sigil)?;
4957            Some((arg, sigil, value))
4958        })
4959        .max_by_key(|(_, sigil, _)| sigil.len())
4960}
4961
4962fn match_sigil_arg_chain<'a>(
4963    cmds: &'a [SpecCommand],
4964    word: &'a str,
4965) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4966    cmds.iter()
4967        .filter_map(|cmd| match_sigil_arg(cmd, word))
4968        .max_by_key(|(_, sigil, _)| sigil.len())
4969}
4970
4971/// Record that `arg` was handed a word before the `--` it requires.
4972///
4973/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
4974/// message is emitted only the first time each arg is seen. The set is also what suppresses the
4975/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
4976/// end of the parse.
4977fn report_double_dash_violation(
4978    arg: &SpecArg,
4979    errors: &mut Vec<UsageErr>,
4980    violations: &mut HashSet<String>,
4981) {
4982    if violations.insert(arg.name.clone()) {
4983        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4984    }
4985}
4986
4987/// `--version` and `-V`, which the parser supplies where the spec declares a version.
4988///
4989/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
4990/// command tables — both of which accepted these spellings while this parser called them
4991/// unknown words. The help page has always listed `-V, --version` under the same
4992/// condition, and said in as many words that it did so "only where a version is
4993/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
4994/// page advertising a flag the parse refused.
4995///
4996/// The root only, because that is where the page lists it: `version` is a property of
4997/// the program, and a subcommand answering with the program's version is a claim no spec
4998/// made. A declared flag wins by arriving first — every scan consults this only after
4999/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
5000/// keeps `--version` supplied beside it.
5001fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5002    (spec.version.is_some() || spec.long_version.is_some())
5003        && cmds.len() == 1
5004        && !spec.cmd.disable_version_flag
5005        && (w == "--version" || w == "-V")
5006}
5007
5008fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5009    spec.disable_help != Some(true)
5010        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5011            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5012}
5013
5014impl ParseOutput {
5015    pub fn as_env(&self) -> BTreeMap<String, String> {
5016        let mut env = BTreeMap::new();
5017        for (flag, val) in &self.flags {
5018            let key = format!("usage_{}", crate::case::snake(&flag.name));
5019            let val = match val {
5020                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5021                ParseValue::String(s) => s.clone(),
5022                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5023                ParseValue::MultiString(s) => crate::shell_words::join(s),
5024            };
5025            env.insert(key, val);
5026        }
5027        for (arg, val) in &self.args {
5028            let key = format!("usage_{}", crate::case::snake(&arg.name));
5029            env.insert(key, val.to_string());
5030        }
5031        env
5032    }
5033}
5034
5035impl Display for ParseValue {
5036    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5037        match self {
5038            ParseValue::Bool(b) => write!(f, "{b}"),
5039            ParseValue::String(s) => write!(f, "{s}"),
5040            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5041            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5042        }
5043    }
5044}
5045
5046/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5047fn render_token(token: &TokenBinding) -> String {
5048    let roles = token.roles.iter().map(render_role).join(", ");
5049    let synthesized = if token.synthesized { " (read as)" } else { "" };
5050    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5051}
5052
5053fn render_role(role: &TokenRole) -> String {
5054    match role {
5055        TokenRole::Program => "program".to_string(),
5056        TokenRole::Command { name } => format!("subcommand {name}"),
5057        TokenRole::Flag {
5058            flag,
5059            spelling,
5060            negated,
5061        } => {
5062            let negated = if *negated { ", negated" } else { "" };
5063            format!("flag {} as {spelling}{negated}", flag.name)
5064        }
5065        TokenRole::Value {
5066            flag,
5067            values,
5068            attached,
5069        } => {
5070            let attached = if *attached { ", attached" } else { "" };
5071            format!("value of {} = {values:?}{attached}", flag.name)
5072        }
5073        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5074        TokenRole::Sigil { arg, sigil, values } => {
5075            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5076        }
5077        TokenRole::Separator => "separator".to_string(),
5078        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5079        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5080        TokenRole::Restart => "restart".to_string(),
5081        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5082        TokenRole::UnknownFlag { bound_as } => match bound_as {
5083            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5084            None => "unknown flag".to_string(),
5085        },
5086        TokenRole::Refused { reason } => format!("refused: {reason}"),
5087        TokenRole::External => "external".to_string(),
5088        TokenRole::Unread => "unread".to_string(),
5089    }
5090}
5091
5092impl Debug for ParseOutput {
5093    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5094        f.debug_struct("ParseOutput")
5095            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5096            .field(
5097                "args",
5098                &self
5099                    .args
5100                    .iter()
5101                    .map(|(a, w)| format!("{}: {w}", a.name))
5102                    .collect_vec(),
5103            )
5104            .field("clauses", &self.clauses)
5105            .field(
5106                "available_flags",
5107                &self
5108                    .available_flags
5109                    .iter()
5110                    .map(|(f, w)| format!("{f}: {w}"))
5111                    .collect_vec(),
5112            )
5113            .field(
5114                "flags",
5115                &self
5116                    .flags
5117                    .iter()
5118                    .map(|(f, w)| format!("{}: {w}", f.name))
5119                    .collect_vec(),
5120            )
5121            .field("flag_awaiting_value", &self.flag_awaiting_value)
5122            .field("errors", &self.errors)
5123            .field("external", &self.external)
5124            // Provenance, one line per token and one per fallback. This is the parser's
5125            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5126            // first — `usage explain` renders the same facts for a reader.
5127            .field(
5128                "tokens",
5129                &self.tokens.iter().map(render_token).collect_vec(),
5130            )
5131            .field(
5132                "origins",
5133                &self
5134                    .flag_origins
5135                    .iter()
5136                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5137                    .chain(
5138                        self.arg_origins
5139                            .iter()
5140                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5141                    )
5142                    .collect_vec(),
5143            )
5144            .field("overridden_flags", &self.overridden_flags)
5145            .finish()
5146    }
5147}
5148
5149#[cfg(test)]
5150mod tests {
5151    use super::*;
5152    use crate::SpecFlagAction;
5153
5154    fn input(words: &[&str]) -> Vec<String> {
5155        words.iter().map(|word| (*word).to_string()).collect()
5156    }
5157
5158    #[test]
5159    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5160        // The page has always listed `-V, --version` wherever a `version` is declared,
5161        // and usage-argv and usage-go have always accepted both. This parser called them
5162        // unknown words, so the one implementation the corpus measures the others against
5163        // was the one that disagreed.
5164        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5165            .parse()
5166            .unwrap();
5167
5168        for spelling in ["--version", "-V"] {
5169            let err = parse(&spec, &input(&["ex", spelling]))
5170                .expect_err("answering with a version ends the parse");
5171            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5172        }
5173    }
5174
5175    #[test]
5176    fn the_supplied_version_flag_is_the_roots_alone() {
5177        // `version` describes the program, and the page lists the entry on the program's
5178        // own page only. A subcommand answering with it would be a claim no spec made.
5179        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5180            .parse()
5181            .unwrap();
5182
5183        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5184        assert_eq!(err.to_string(), "unexpected word: --version");
5185    }
5186
5187    #[test]
5188    fn no_declared_version_supplies_nothing() {
5189        // A `--version` answering with nothing is worse than one that is not there, which
5190        // is why the entry is conditional on the page and the spelling on the parse.
5191        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5192
5193        for spelling in ["--version", "-V"] {
5194            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5195            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5196        }
5197    }
5198
5199    #[test]
5200    fn disable_version_flag_removes_the_supplied_spellings() {
5201        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5202            .parse()
5203            .unwrap();
5204
5205        for spelling in ["--version", "-V"] {
5206            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5207            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5208        }
5209    }
5210
5211    #[test]
5212    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5213        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5214        // parse agrees without being told, because a declared flag is matched first.
5215        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5216            .parse()
5217            .unwrap();
5218
5219        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5220        assert_eq!(out.flags.len(), 1);
5221
5222        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5223        assert_eq!(err.to_string(), "1.2.3");
5224    }
5225
5226    #[test]
5227    fn the_supplied_spellings_split_the_two_version_texts() {
5228        // The same split `render_action_err` gives a declared version flag: the long
5229        // spelling prefers `long_version`, the short prefers the concise one.
5230        let spec: Spec =
5231            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5232                .parse()
5233                .unwrap();
5234
5235        assert_eq!(
5236            parse(&spec, &input(&["ex", "--version"]))
5237                .unwrap_err()
5238                .to_string(),
5239            "1.2.3 (abcdef)"
5240        );
5241        assert_eq!(
5242            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5243            "1.2.3"
5244        );
5245    }
5246
5247    #[test]
5248    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5249        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5250        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5251        // naming nothing: usage-argv and usage-go both resolve the letter through the
5252        // same lookup that finds a declared short, and clap prints help for it too.
5253        let spec: Spec =
5254            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5255                .parse()
5256                .unwrap();
5257
5258        for token in ["-vh", "-hv"] {
5259            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5260            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5261        }
5262        for token in ["-vV", "-Vv"] {
5263            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5264            assert_eq!(err.to_string(), "1.2.3", "{token}");
5265        }
5266    }
5267
5268    #[test]
5269    fn a_bundled_help_letter_asks_for_the_short_page() {
5270        // Whatever else shares the token: `-h` is the short spelling, and the letters
5271        // beside it say nothing about which page was asked for.
5272        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"
5273            .parse()
5274            .unwrap();
5275
5276        let short = parse(&spec, &input(&["ex", "-vh"]))
5277            .unwrap_err()
5278            .to_string();
5279        let long = parse(&spec, &input(&["ex", "--help"]))
5280            .unwrap_err()
5281            .to_string();
5282        assert!(short.contains("Be loud"), "{short}");
5283        assert!(!short.contains("at length"), "{short}");
5284        assert!(long.contains("at length"), "{long}");
5285    }
5286
5287    #[test]
5288    fn the_bundled_version_letter_is_the_roots_alone() {
5289        // The same rule the whole-token spelling follows, asked one letter at a time.
5290        let spec: Spec =
5291            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5292                .parse()
5293                .unwrap();
5294
5295        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5296        assert_eq!(err.to_string(), "unexpected word: -vV");
5297    }
5298
5299    #[test]
5300    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5301        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5302        // this spec's own `-h`, taking its value from the rest of the token.
5303        let spec: Spec =
5304            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5305                .parse()
5306                .unwrap();
5307
5308        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5309        assert_eq!(out.flags.len(), 2);
5310        assert!(out
5311            .flags
5312            .iter()
5313            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
5314    }
5315
5316    #[test]
5317    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5318        let spec: Spec =
5319            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5320                .parse()
5321                .unwrap();
5322
5323        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5324        assert_eq!(err.to_string(), "unexpected word: -vh");
5325    }
5326
5327    #[test]
5328    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5329        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5330        // on the way to discovering that `z` names nothing.
5331        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5332            .parse()
5333            .unwrap();
5334
5335        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5336        assert!(out.flags.is_empty(), "{:?}", out.flags);
5337        assert_eq!(out.args.len(), 1);
5338    }
5339
5340    fn spec_with_arg(arg: SpecArg) -> Spec {
5341        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5342        Spec {
5343            name: "test".to_string(),
5344            bin: "test".to_string(),
5345            cmd,
5346            ..Default::default()
5347        }
5348    }
5349
5350    fn spec_with_flag(flag: SpecFlag) -> Spec {
5351        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5352        Spec {
5353            name: "test".to_string(),
5354            bin: "test".to_string(),
5355            cmd,
5356            ..Default::default()
5357        }
5358    }
5359
5360    fn parse_with_env(
5361        spec: &Spec,
5362        words: &[&str],
5363        env: &[(&str, &str)],
5364    ) -> Result<ParseOutput, miette::Error> {
5365        let env = env
5366            .iter()
5367            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5368            .collect();
5369        Parser::new(spec).with_env(env).parse(&input(words))
5370    }
5371
5372    fn first_string_value(parsed: &ParseOutput) -> &str {
5373        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5374            return value;
5375        }
5376        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5377            return value;
5378        }
5379        panic!("expected first parsed value to be ParseValue::String");
5380    }
5381
5382    #[test]
5383    fn custom_environment_parser_dispatches_executable_views() {
5384        let spec: Spec = r#"
5385bin "ex"
5386view "runner" root="run"
5387cmd "run" {
5388    flag "--token <token>" env="TOKEN"
5389}
5390        "#
5391        .parse()
5392        .unwrap();
5393        let parsed = Parser::new(&spec)
5394            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5395            .parse(&input(&["runner"]))
5396            .unwrap();
5397
5398        assert_eq!(parsed.cmd.name, "runner");
5399        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5400            && matches!(value, ParseValue::String(value) if value == "secret")));
5401    }
5402
5403    #[test]
5404    fn an_executable_view_keeps_the_hosts_version_action() {
5405        let spec: Spec = r#"
5406bin "ex"
5407version "1.2.3"
5408flag "-V --version" action="version"
5409flag "--verbose" global=#true
5410view "runner" root="run" globals=#true
5411cmd "run"
5412        "#
5413        .parse()
5414        .unwrap();
5415
5416        let error = Parser::new(&spec)
5417            .parse(&input(&["runner", "--version"]))
5418            .expect_err("the host version action should answer before view projection");
5419        assert_eq!(error.to_string(), "1.2.3");
5420
5421        let error = Parser::new(&spec)
5422            .parse(&input(&["runner", "--verbose", "--version"]))
5423            .expect_err("the host version action should remain after a carried global");
5424        assert_eq!(error.to_string(), "1.2.3");
5425    }
5426
5427    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5428        let flag = parsed
5429            .flags
5430            .keys()
5431            .find(|flag| flag.name == name)
5432            .unwrap_or_else(|| panic!("expected flag {name}"));
5433        let value = parsed
5434            .flags
5435            .get(flag)
5436            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5437        match value {
5438            ParseValue::String(value) => value,
5439            _ => panic!("expected flag {name} to be ParseValue::String"),
5440        }
5441    }
5442
5443    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5444        let err = result.expect_err("expected parser error");
5445        assert_eq!(format!("{err}"), expected);
5446    }
5447
5448    #[test]
5449    fn a_short_version_action_falls_back_to_the_long_version() {
5450        let flag = SpecFlag::builder()
5451            .short('R')
5452            .action(SpecFlagAction::Version)
5453            .build();
5454        let spec = Spec {
5455            name: "test".to_string(),
5456            bin: "test".to_string(),
5457            long_version: Some("1.2.3\ncommit abc123".to_string()),
5458            ..Default::default()
5459        };
5460        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
5461            panic!("expected version action")
5462        };
5463        assert_eq!(version, "1.2.3\ncommit abc123");
5464    }
5465
5466    #[cfg(feature = "unstable_choices_env")]
5467    fn spec_arg_choices_env(key: &str) -> Spec {
5468        spec_with_arg(
5469            SpecArg::builder()
5470                .name("env")
5471                .choices_env(key)
5472                .required(false)
5473                .build(),
5474        )
5475    }
5476
5477    #[cfg(feature = "unstable_choices_env")]
5478    fn spec_flag_choices_env(key: &str) -> Spec {
5479        spec_with_flag(
5480            SpecFlag::builder()
5481                .long("env")
5482                .arg(SpecArg::builder().name("env").choices_env(key).build())
5483                .build(),
5484        )
5485    }
5486
5487    #[test]
5488    fn test_parse() {
5489        let cmd = SpecCommand::builder()
5490            .name("test")
5491            .arg(SpecArg::builder().name("arg").build())
5492            .flag(SpecFlag::builder().long("flag").build())
5493            .build();
5494        let spec = Spec {
5495            name: "test".to_string(),
5496            bin: "test".to_string(),
5497            cmd,
5498            ..Default::default()
5499        };
5500        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5501        let parsed = parse(&spec, &input).unwrap();
5502        assert_eq!(parsed.cmds.len(), 1);
5503        assert_eq!(parsed.cmds[0].name, "test");
5504        assert_eq!(parsed.args.len(), 1);
5505        assert_eq!(parsed.flags.len(), 1);
5506        assert_eq!(parsed.available_flags.len(), 1);
5507    }
5508
5509    #[test]
5510    fn test_flag_overrides_last_occurrence_wins() {
5511        let spec: Spec = r#"
5512flag "--stdin" default=#true
5513flag "--file <file>" overrides="--stdin"
5514        "#
5515        .parse()
5516        .unwrap();
5517
5518        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5519        assert_eq!(file_wins.flags.len(), 1);
5520        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5521        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5522
5523        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5524        assert_eq!(stdin_wins.flags.len(), 1);
5525        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5526        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5527    }
5528
5529    #[test]
5530    fn test_flag_override_clears_pending_value() {
5531        let spec: Spec = r#"
5532flag "--file <file>" overrides="--stdin"
5533flag "--stdin"
5534arg "[input]"
5535        "#
5536        .parse()
5537        .unwrap();
5538
5539        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5540        assert_eq!(parsed.flags.len(), 1);
5541        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5542        assert_eq!(first_string_value(&parsed), "input.txt");
5543    }
5544
5545    #[cfg(unix)]
5546    #[test]
5547    fn a_mount_on_the_root_discovers_subcommands() {
5548        // The root is a command like any other, so it can find its own subcommands
5549        // by running something. Uses `echo` rather than a fixture because resolving
5550        // a mount is what is being tested.
5551        let spec: Spec = r#"
5552name "ex"
5553bin "ex"
5554cmd "declared"
5555mount run="echo 'cmd \"discovered\"'"
5556"#
5557        .parse()
5558        .unwrap();
5559
5560        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5561        assert_eq!(out.cmd.name, "discovered");
5562    }
5563
5564    #[test]
5565    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5566        let spec: Spec = r#"
5567name "ex"
5568bin "ex"
5569mount run="this command must never run"
5570cmd "declared"
5571"#
5572        .parse()
5573        .unwrap();
5574
5575        Parser::new(&spec)
5576            .with_mount_outputs(HashMap::new())
5577            .parse(&input(&["ex", "declared"]))
5578            .expect("a declared command does not resolve the mount");
5579
5580        let error = Parser::new(&spec)
5581            .with_mount_outputs(HashMap::new())
5582            .parse(&input(&["ex", "discovered"]))
5583            .unwrap_err();
5584        assert!(
5585            error
5586                .to_string()
5587                .contains("No injected output was provided for mount command"),
5588            "{error}"
5589        );
5590    }
5591
5592    #[cfg(unix)]
5593    #[test]
5594    fn completion_sees_root_mounted_commands_with_nothing_typed() {
5595        // The case a root mount exists for. `mycli <tab>` has no word to trigger
5596        // discovery with, so a completion has to resolve up front or the mounted
5597        // commands are never offered.
5598        let spec: Spec = r#"
5599name "ex"
5600bin "ex"
5601cmd "declared"
5602mount run="echo 'cmd \"discovered\"'"
5603"#
5604        .parse()
5605        .unwrap();
5606
5607        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5608        assert!(
5609            out.cmd.subcommands.contains_key("discovered"),
5610            "a completion should see mounted commands; got {:?}",
5611            out.cmd.subcommands.keys().collect::<Vec<_>>()
5612        );
5613    }
5614
5615    #[cfg(unix)]
5616    #[test]
5617    fn completion_and_execution_agree_about_discovery() {
5618        // Offering a command that a real parse would hand to the default instead is
5619        // worse than not offering it, so the gate applies to both paths. The mount
5620        // fails if it runs, which is how both halves are checked at once.
5621        let spec: Spec = r#"
5622name "ex"
5623bin "ex"
5624default_subcommand "run"
5625cmd "run" {
5626  arg "<task>"
5627}
5628mount run="exit 1"
5629"#
5630        .parse()
5631        .unwrap();
5632
5633        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5634        assert!(
5635            !out.cmd.subcommands.contains_key("discovered"),
5636            "a completion must not offer what execution will not route"
5637        );
5638
5639        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5640        assert_eq!(out.cmd.name, "run");
5641    }
5642
5643    #[cfg(unix)]
5644    #[test]
5645    fn a_default_subcommand_outranks_discovery() {
5646        // The default already says what an unmatched word means, and says it for
5647        // free. The mount fails if it runs, so parsing proves discovery was skipped.
5648        let spec: Spec = r#"
5649name "ex"
5650bin "ex"
5651default_subcommand "run"
5652cmd "run" {
5653  arg "<task>"
5654}
5655mount run="exit 1"
5656"#
5657        .parse()
5658        .unwrap();
5659
5660        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5661        assert_eq!(out.cmd.name, "run");
5662    }
5663
5664    #[cfg(unix)]
5665    #[test]
5666    fn a_mount_may_ask_to_outrank_the_default() {
5667        // Opting in, and paying for it: discovery runs first, so a discovered
5668        // command wins over the fallback.
5669        let spec: Spec = r#"
5670name "ex"
5671bin "ex"
5672default_subcommand "run"
5673cmd "run" {
5674  arg "<task>"
5675}
5676mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5677"#
5678        .parse()
5679        .unwrap();
5680
5681        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5682        assert_eq!(out.cmd.name, "discovered");
5683
5684        // A word it does not know still reaches the default.
5685        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5686        assert_eq!(out.cmd.name, "run");
5687    }
5688
5689    #[cfg(unix)]
5690    #[test]
5691    fn a_flag_does_not_run_the_mount() {
5692        // A flag matches no subcommand, which would have been enough to trigger
5693        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
5694        // so parsing at all is the proof that it did not.
5695        let spec: Spec = r#"
5696name "ex"
5697bin "ex"
5698flag "--verbose"
5699cmd "declared"
5700mount run="exit 1"
5701"#
5702        .parse()
5703        .unwrap();
5704
5705        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
5706        assert_eq!(out.cmd.name, "ex");
5707    }
5708
5709    #[cfg(unix)]
5710    #[test]
5711    fn a_declared_subcommand_does_not_run_the_mount() {
5712        // The mount would fail if it ran, so this parsing at all is the proof that
5713        // discovery is skipped when the word is already known. Worth pinning: a root
5714        // mount that resolved eagerly would spawn a process on every invocation.
5715        let spec: Spec = r#"
5716name "ex"
5717bin "ex"
5718cmd "declared"
5719mount run="exit 1"
5720"#
5721        .parse()
5722        .unwrap();
5723
5724        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
5725        assert_eq!(out.cmd.name, "declared");
5726    }
5727
5728    #[test]
5729    fn a_root_mount_survives_being_written_out() {
5730        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
5731            .parse()
5732            .unwrap();
5733        assert_eq!(spec.cmd.mounts.len(), 1);
5734
5735        let reparsed: Spec = spec.to_string().parse().unwrap();
5736        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
5737        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
5738    }
5739
5740    #[test]
5741    fn test_mount_prefix_applies_flag_overrides() {
5742        let stdin = Arc::new(
5743            SpecFlag::builder()
5744                .name("stdin")
5745                .long("stdin")
5746                .global(true)
5747                .build(),
5748        );
5749        let file = Arc::new(
5750            SpecFlag::builder()
5751                .name("file")
5752                .long("file")
5753                .arg(SpecArg::builder().name("file").build())
5754                .global(true)
5755                .overrides_with(vec!["--stdin".to_string()])
5756                .build(),
5757        );
5758        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
5759
5760        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
5761        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
5762
5763        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
5764    }
5765
5766    #[test]
5767    fn test_flag_override_suppresses_env_value() {
5768        let spec: Spec = r#"
5769flag "--stdin" env="USE_STDIN"
5770flag "--file <file>" overrides="--stdin"
5771        "#
5772        .parse()
5773        .unwrap();
5774
5775        let parsed = parse_with_env(
5776            &spec,
5777            &["test", "--file", "input.txt"],
5778            &[("USE_STDIN", "true")],
5779        )
5780        .unwrap();
5781        assert_eq!(parsed.flags.len(), 1);
5782        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5783    }
5784
5785    #[test]
5786    fn test_flag_override_suppresses_required_check() {
5787        let spec: Spec = r#"
5788flag "--stdin" required=#true
5789flag "--file <file>" overrides="--stdin"
5790        "#
5791        .parse()
5792        .unwrap();
5793
5794        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5795        assert_eq!(parsed.flags.len(), 1);
5796        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5797    }
5798
5799    #[test]
5800    fn test_flag_required_if() {
5801        let spec: Spec = r#"
5802flag "--dir <dir>"
5803flag "--file <file>" required_if="--dir"
5804        "#
5805        .parse()
5806        .unwrap();
5807
5808        parse(&spec, &input(&["test"])).unwrap();
5809        assert_parse_err(
5810            parse(&spec, &input(&["test", "--dir", "src"])),
5811            "Missing required flag: --file <file>",
5812        );
5813        parse(
5814            &spec,
5815            &input(&["test", "--dir", "src", "--file", "input.txt"]),
5816        )
5817        .unwrap();
5818    }
5819
5820    #[test]
5821    fn test_flag_required_unless() {
5822        let spec: Spec = r#"
5823flag "--stdin"
5824flag "--file <file>" required_unless="--stdin"
5825        "#
5826        .parse()
5827        .unwrap();
5828
5829        assert_parse_err(
5830            parse(&spec, &input(&["test"])),
5831            "Missing required flag: --file <file>",
5832        );
5833        parse(&spec, &input(&["test", "--stdin"])).unwrap();
5834        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5835    }
5836
5837    #[test]
5838    fn complete_required_relationship_truth_tables() {
5839        let spec: Spec = r#"
5840name "ex"
5841bin "ex"
5842flag "--mode <mode>"
5843flag "--scope <scope>"
5844flag "--token <token>" {
5845    required_if_eq "--mode" "remote"
5846}
5847flag "--approval <approval>" {
5848    required_if_eq_all "--mode" "remote" "--scope" "global"
5849}
5850flag "--input <input>" {
5851    required_unless "--stdin" "--file"
5852}
5853flag "--checksum <checksum>" {
5854    required_unless_all "--stdin" "--file"
5855}
5856flag "--stdin"
5857flag "--file <file>"
5858arg "[request]" {
5859    requires "--mode" "--scope"
5860}
5861"#
5862        .parse()
5863        .unwrap();
5864        let parse_args = |args: &[&str]| {
5865            parse(
5866                &spec,
5867                &args
5868                    .iter()
5869                    .map(|arg| (*arg).to_string())
5870                    .collect::<Vec<_>>(),
5871            )
5872        };
5873
5874        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
5875        assert!(parse_args(&[
5876            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
5877        ])
5878        .is_err());
5879        parse_args(&[
5880            "ex",
5881            "--mode",
5882            "remote",
5883            "--token",
5884            "secret",
5885            "--scope",
5886            "global",
5887            "--approval",
5888            "yes",
5889            "--stdin",
5890            "--file",
5891            "in",
5892        ])
5893        .unwrap();
5894        parse_args(&[
5895            "ex",
5896            "--mode",
5897            "local",
5898            "--scope",
5899            "project",
5900            "--stdin",
5901            "--checksum",
5902            "sum",
5903            "request.json",
5904        ])
5905        .unwrap();
5906
5907        let reparsed: Spec = spec.to_string().parse().unwrap();
5908        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
5909        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
5910        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
5911        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
5912    }
5913
5914    #[test]
5915    fn test_conditional_requirements_treat_env_as_explicit() {
5916        let spec: Spec = r#"
5917flag "--dir <dir>" env="INPUT_DIR"
5918flag "--stdin" env="USE_STDIN"
5919flag "--file <file>" required_if="--dir" required_unless="--stdin"
5920        "#
5921        .parse()
5922        .unwrap();
5923
5924        assert_parse_err(
5925            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5926            "Missing required flag: --file <file>",
5927        );
5928        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5929    }
5930
5931    #[test]
5932    fn test_custom_env_does_not_fall_back_to_process_env() {
5933        assert!(std::env::var("PATH").is_ok());
5934        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5935
5936        assert_parse_err(
5937            parse_with_env(&spec, &["test"], &[]),
5938            "Missing required flag: --file <file>",
5939        );
5940    }
5941
5942    #[test]
5943    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5944        let spec: Spec = r#"
5945flag "--dir <dir>" default="src"
5946flag "--file <file>" required_if="--dir"
5947        "#
5948        .parse()
5949        .unwrap();
5950
5951        parse(&spec, &input(&["test"])).unwrap();
5952    }
5953
5954    #[test]
5955    fn test_conditional_requirements_see_overridden_flags_as_absent() {
5956        let spec: Spec = r#"
5957flag "--stdin"
5958flag "--dir <dir>" overrides="--stdin"
5959flag "--file <file>" required_unless="--stdin"
5960        "#
5961        .parse()
5962        .unwrap();
5963
5964        assert_parse_err(
5965            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5966            "Missing required flag: --file <file>",
5967        );
5968    }
5969
5970    #[test]
5971    fn short_flag_is_one_character_not_one_byte() {
5972        // A short is declared and read by character. Counting bytes instead either
5973        // refuses the declaration or slices the token inside the character, and clap
5974        // — which many specs are generated from — accepts shorts like this one.
5975        let spec = spec_with_flag(
5976            SpecFlag::builder()
5977                .short('磨')
5978                .long("polish")
5979                .arg(SpecArg::builder().name("opt").build())
5980                .build(),
5981        );
5982        let attached = Parser::new(&spec)
5983            .parse(&input(&["test", "-磨VALUE"]))
5984            .unwrap();
5985        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5986        let detached = Parser::new(&spec)
5987            .parse(&input(&["test", "-磨", "V"]))
5988            .unwrap();
5989        assert_eq!(flag_string_value(&detached, "polish"), "V");
5990    }
5991
5992    #[test]
5993    fn test_as_env() {
5994        let cmd = SpecCommand::builder()
5995            .name("test")
5996            .arg(SpecArg::builder().name("arg").build())
5997            .flag(SpecFlag::builder().long("flag").build())
5998            .flag(
5999                SpecFlag::builder()
6000                    .long("force")
6001                    .negate("--no-force")
6002                    .build(),
6003            )
6004            .build();
6005        let spec = Spec {
6006            name: "test".to_string(),
6007            bin: "test".to_string(),
6008            cmd,
6009            ..Default::default()
6010        };
6011        let input = vec![
6012            "test".to_string(),
6013            "--flag".to_string(),
6014            "--no-force".to_string(),
6015        ];
6016        let parsed = parse(&spec, &input).unwrap();
6017        let env = parsed.as_env();
6018        assert_eq!(env.len(), 2);
6019        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6020        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6021    }
6022
6023    #[test]
6024    fn test_arg_env_var() {
6025        let cmd = SpecCommand::builder()
6026            .name("test")
6027            .arg(
6028                SpecArg::builder()
6029                    .name("input")
6030                    .env("TEST_ARG_INPUT")
6031                    .required(true)
6032                    .build(),
6033            )
6034            .build();
6035        let spec = Spec {
6036            name: "test".to_string(),
6037            bin: "test".to_string(),
6038            cmd,
6039            ..Default::default()
6040        };
6041
6042        // Set env var
6043        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
6044
6045        let input = vec!["test".to_string()];
6046        let parsed = parse(&spec, &input).unwrap();
6047
6048        assert_eq!(parsed.args.len(), 1);
6049        let arg = parsed.args.keys().next().unwrap();
6050        assert_eq!(arg.name, "input");
6051        let value = parsed.args.values().next().unwrap();
6052        assert_eq!(value.to_string(), "test_file.txt");
6053
6054        // Clean up
6055        std::env::remove_var("TEST_ARG_INPUT");
6056    }
6057
6058    #[test]
6059    fn test_flag_env_var_with_arg() {
6060        let cmd = SpecCommand::builder()
6061            .name("test")
6062            .flag(
6063                SpecFlag::builder()
6064                    .long("output")
6065                    .env("TEST_FLAG_OUTPUT")
6066                    .arg(SpecArg::builder().name("file").build())
6067                    .build(),
6068            )
6069            .build();
6070        let spec = Spec {
6071            name: "test".to_string(),
6072            bin: "test".to_string(),
6073            cmd,
6074            ..Default::default()
6075        };
6076
6077        // Set env var
6078        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
6079
6080        let input = vec!["test".to_string()];
6081        let parsed = parse(&spec, &input).unwrap();
6082
6083        assert_eq!(parsed.flags.len(), 1);
6084        let flag = parsed.flags.keys().next().unwrap();
6085        assert_eq!(flag.name, "output");
6086        let value = parsed.flags.values().next().unwrap();
6087        assert_eq!(value.to_string(), "output.txt");
6088
6089        // Clean up
6090        std::env::remove_var("TEST_FLAG_OUTPUT");
6091    }
6092
6093    #[test]
6094    fn test_flag_env_var_boolean() {
6095        let cmd = SpecCommand::builder()
6096            .name("test")
6097            .flag(
6098                SpecFlag::builder()
6099                    .long("verbose")
6100                    .env("TEST_FLAG_VERBOSE")
6101                    .build(),
6102            )
6103            .build();
6104        let spec = Spec {
6105            name: "test".to_string(),
6106            bin: "test".to_string(),
6107            cmd,
6108            ..Default::default()
6109        };
6110
6111        // Set env var to true
6112        std::env::set_var("TEST_FLAG_VERBOSE", "true");
6113
6114        let input = vec!["test".to_string()];
6115        let parsed = parse(&spec, &input).unwrap();
6116
6117        assert_eq!(parsed.flags.len(), 1);
6118        let flag = parsed.flags.keys().next().unwrap();
6119        assert_eq!(flag.name, "verbose");
6120        let value = parsed.flags.values().next().unwrap();
6121        assert_eq!(value.to_string(), "true");
6122
6123        // Clean up
6124        std::env::remove_var("TEST_FLAG_VERBOSE");
6125    }
6126
6127    #[test]
6128    fn test_env_var_precedence() {
6129        // CLI args should take precedence over env vars
6130        let cmd = SpecCommand::builder()
6131            .name("test")
6132            .arg(
6133                SpecArg::builder()
6134                    .name("input")
6135                    .env("TEST_PRECEDENCE_INPUT")
6136                    .required(true)
6137                    .build(),
6138            )
6139            .build();
6140        let spec = Spec {
6141            name: "test".to_string(),
6142            bin: "test".to_string(),
6143            cmd,
6144            ..Default::default()
6145        };
6146
6147        // Set env var
6148        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
6149
6150        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6151        let parsed = parse(&spec, &input).unwrap();
6152
6153        assert_eq!(parsed.args.len(), 1);
6154        let value = parsed.args.values().next().unwrap();
6155        // CLI arg should take precedence
6156        assert_eq!(value.to_string(), "cli_file.txt");
6157
6158        // Clean up
6159        std::env::remove_var("TEST_PRECEDENCE_INPUT");
6160    }
6161
6162    #[test]
6163    fn test_flag_var_true_with_single_default() {
6164        // When var=true and default="bar", the default should be MultiString(["bar"])
6165        let cmd = SpecCommand::builder()
6166            .name("test")
6167            .flag(
6168                SpecFlag::builder()
6169                    .long("foo")
6170                    .var(true)
6171                    .arg(SpecArg::builder().name("foo").build())
6172                    .default_value("bar")
6173                    .build(),
6174            )
6175            .build();
6176        let spec = Spec {
6177            name: "test".to_string(),
6178            bin: "test".to_string(),
6179            cmd,
6180            ..Default::default()
6181        };
6182
6183        // User doesn't provide the flag
6184        let input = vec!["test".to_string()];
6185        let parsed = parse(&spec, &input).unwrap();
6186
6187        assert_eq!(parsed.flags.len(), 1);
6188        let flag = parsed.flags.keys().next().unwrap();
6189        assert_eq!(flag.name, "foo");
6190        let value = parsed.flags.values().next().unwrap();
6191        // Should be MultiString, not String
6192        match value {
6193            ParseValue::MultiString(v) => {
6194                assert_eq!(v.len(), 1);
6195                assert_eq!(v[0], "bar");
6196            }
6197            _ => panic!("Expected MultiString, got {:?}", value),
6198        }
6199    }
6200
6201    #[test]
6202    fn test_flag_var_true_with_multiple_defaults() {
6203        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6204        let cmd = SpecCommand::builder()
6205            .name("test")
6206            .flag(
6207                SpecFlag::builder()
6208                    .long("foo")
6209                    .var(true)
6210                    .arg(SpecArg::builder().name("foo").build())
6211                    .default_values(["xyz", "bar"])
6212                    .build(),
6213            )
6214            .build();
6215        let spec = Spec {
6216            name: "test".to_string(),
6217            bin: "test".to_string(),
6218            cmd,
6219            ..Default::default()
6220        };
6221
6222        // User doesn't provide the flag
6223        let input = vec!["test".to_string()];
6224        let parsed = parse(&spec, &input).unwrap();
6225
6226        assert_eq!(parsed.flags.len(), 1);
6227        let value = parsed.flags.values().next().unwrap();
6228        // Should be MultiString with both values
6229        match value {
6230            ParseValue::MultiString(v) => {
6231                assert_eq!(v.len(), 2);
6232                assert_eq!(v[0], "xyz");
6233                assert_eq!(v[1], "bar");
6234            }
6235            _ => panic!("Expected MultiString, got {:?}", value),
6236        }
6237    }
6238
6239    #[test]
6240    fn test_flag_var_false_with_default_remains_string() {
6241        // When var=false (default), the default should still be String("bar")
6242        let cmd = SpecCommand::builder()
6243            .name("test")
6244            .flag(
6245                SpecFlag::builder()
6246                    .long("foo")
6247                    .var(false) // Default behavior
6248                    .arg(SpecArg::builder().name("foo").build())
6249                    .default_value("bar")
6250                    .build(),
6251            )
6252            .build();
6253        let spec = Spec {
6254            name: "test".to_string(),
6255            bin: "test".to_string(),
6256            cmd,
6257            ..Default::default()
6258        };
6259
6260        // User doesn't provide the flag
6261        let input = vec!["test".to_string()];
6262        let parsed = parse(&spec, &input).unwrap();
6263
6264        assert_eq!(parsed.flags.len(), 1);
6265        let value = parsed.flags.values().next().unwrap();
6266        // Should be String, not MultiString
6267        match value {
6268            ParseValue::String(s) => {
6269                assert_eq!(s, "bar");
6270            }
6271            _ => panic!("Expected String, got {:?}", value),
6272        }
6273    }
6274
6275    #[test]
6276    fn test_arg_var_true_with_single_default() {
6277        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6278        let cmd = SpecCommand::builder()
6279            .name("test")
6280            .arg(
6281                SpecArg::builder()
6282                    .name("files")
6283                    .var(true)
6284                    .default_value("default.txt")
6285                    .required(false)
6286                    .build(),
6287            )
6288            .build();
6289        let spec = Spec {
6290            name: "test".to_string(),
6291            bin: "test".to_string(),
6292            cmd,
6293            ..Default::default()
6294        };
6295
6296        // User doesn't provide the arg
6297        let input = vec!["test".to_string()];
6298        let parsed = parse(&spec, &input).unwrap();
6299
6300        assert_eq!(parsed.args.len(), 1);
6301        let value = parsed.args.values().next().unwrap();
6302        // Should be MultiString, not String
6303        match value {
6304            ParseValue::MultiString(v) => {
6305                assert_eq!(v.len(), 1);
6306                assert_eq!(v[0], "default.txt");
6307            }
6308            _ => panic!("Expected MultiString, got {:?}", value),
6309        }
6310    }
6311
6312    #[test]
6313    fn test_arg_var_true_with_multiple_defaults() {
6314        // When arg has var=true and multiple defaults
6315        let cmd = SpecCommand::builder()
6316            .name("test")
6317            .arg(
6318                SpecArg::builder()
6319                    .name("files")
6320                    .var(true)
6321                    .default_values(["file1.txt", "file2.txt"])
6322                    .required(false)
6323                    .build(),
6324            )
6325            .build();
6326        let spec = Spec {
6327            name: "test".to_string(),
6328            bin: "test".to_string(),
6329            cmd,
6330            ..Default::default()
6331        };
6332
6333        // User doesn't provide the arg
6334        let input = vec!["test".to_string()];
6335        let parsed = parse(&spec, &input).unwrap();
6336
6337        assert_eq!(parsed.args.len(), 1);
6338        let value = parsed.args.values().next().unwrap();
6339        // Should be MultiString with both values
6340        match value {
6341            ParseValue::MultiString(v) => {
6342                assert_eq!(v.len(), 2);
6343                assert_eq!(v[0], "file1.txt");
6344                assert_eq!(v[1], "file2.txt");
6345            }
6346            _ => panic!("Expected MultiString, got {:?}", value),
6347        }
6348    }
6349
6350    #[test]
6351    fn test_arg_var_false_with_default_remains_string() {
6352        // When arg has var=false (default), the default should still be String
6353        let cmd = SpecCommand::builder()
6354            .name("test")
6355            .arg(
6356                SpecArg::builder()
6357                    .name("file")
6358                    .var(false)
6359                    .default_value("default.txt")
6360                    .required(false)
6361                    .build(),
6362            )
6363            .build();
6364        let spec = Spec {
6365            name: "test".to_string(),
6366            bin: "test".to_string(),
6367            cmd,
6368            ..Default::default()
6369        };
6370
6371        // User doesn't provide the arg
6372        let input = vec!["test".to_string()];
6373        let parsed = parse(&spec, &input).unwrap();
6374
6375        assert_eq!(parsed.args.len(), 1);
6376        let value = parsed.args.values().next().unwrap();
6377        // Should be String, not MultiString
6378        match value {
6379            ParseValue::String(s) => {
6380                assert_eq!(s, "default.txt");
6381            }
6382            _ => panic!("Expected String, got {:?}", value),
6383        }
6384    }
6385
6386    #[test]
6387    fn test_scalar_defaults_validate_only_first_default_choice() {
6388        let specs = [
6389            spec_with_arg(
6390                SpecArg::builder()
6391                    .name("env")
6392                    .var(false)
6393                    .default_values(["dev", "prod"])
6394                    .choices(["dev"])
6395                    .required(false)
6396                    .build(),
6397            ),
6398            spec_with_flag(
6399                SpecFlag::builder()
6400                    .long("env")
6401                    .arg(
6402                        SpecArg::builder()
6403                            .name("env")
6404                            .default_values(["dev", "prod"])
6405                            .choices(["dev"])
6406                            .build(),
6407                    )
6408                    .build(),
6409            ),
6410        ];
6411
6412        for spec in specs {
6413            let parsed = parse(&spec, &input(&["test"])).unwrap();
6414            assert_eq!(first_string_value(&parsed), "dev");
6415        }
6416    }
6417
6418    #[test]
6419    fn a_delimiter_turns_one_word_into_several_values() {
6420        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6421            .parse()
6422            .unwrap();
6423
6424        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6425        let multi = |value: &ParseValue| match value {
6426            ParseValue::MultiString(values) => values.clone(),
6427            other => panic!("expected several values, got {other:?}"),
6428        };
6429        let tags = parsed
6430            .flags
6431            .iter()
6432            .find(|(f, _)| f.name == "tags")
6433            .map(|(_, v)| v)
6434            .unwrap();
6435        assert_eq!(multi(tags), vec!["a", "b", "c"]);
6436        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6437    }
6438
6439    #[test]
6440    fn a_positional_splits_before_its_choices_are_asked() {
6441        // The flag path did this and the positional path did not, so a word whose parts
6442        // were all choices was rejected as one value, and a bad half was reported as the
6443        // whole word.
6444        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
6445            .parse()
6446            .unwrap();
6447
6448        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6449
6450        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6451        let message = err.to_string();
6452        assert!(message.contains("nowhere"), "{message}");
6453        assert!(
6454            !message.contains("src:nowhere"),
6455            "the bad half should be named, not the whole word: {message}"
6456        );
6457    }
6458
6459    #[test]
6460    fn a_split_value_is_counted_and_judged_as_values() {
6461        // Split during the parse rather than after it, so everything downstream sees the
6462        // values the user meant rather than the words they typed: `choices` judges each
6463        // one, and the bounds count them.
6464        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
6465            .parse()
6466            .unwrap();
6467
6468        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6469        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6470        assert!(err.to_string().contains("staging"), "{err}");
6471        assert!(
6472            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6473            "three values should breach var_max=2"
6474        );
6475    }
6476
6477    #[test]
6478    fn a_split_bound_counts_one_occurrence_at_a_time() {
6479        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
6480        // delimiter the collection simply stops at it, so it could never be exceeded; a word
6481        // carrying several values can carry an occurrence past it in one step, and that is
6482        // the only way this bound is ever breached.
6483        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
6484            .parse()
6485            .unwrap();
6486
6487        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
6488        assert!(
6489            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
6490            "three values out of one word is still three values"
6491        );
6492        // The rule the corpus documents for plain words, on split ones: a second occurrence
6493        // starts counting again rather than adding to the first.
6494        parse(
6495            &spec,
6496            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
6497        )
6498        .expect("two per occurrence, twice, is within the bound");
6499    }
6500
6501    #[test]
6502    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
6503        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
6504            .parse()
6505            .unwrap();
6506
6507        parse(
6508            &spec,
6509            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
6510        )
6511        .expect("each occurrence satisfies the bound independently");
6512
6513        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
6514        assert!(
6515            error
6516                .to_string()
6517                .contains("requires at least 2 value(s), got 1"),
6518            "{error:?}"
6519        );
6520    }
6521
6522    #[test]
6523    fn an_exclusive_flag_has_to_be_alone() {
6524        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6525            .parse()
6526            .unwrap();
6527
6528        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6529
6530        // Any other flag.
6531        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6532        assert!(err.to_string().contains("on its own"), "{err}");
6533
6534        // And a positional, which is what makes this more than a conflict with every
6535        // other flag.
6536        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6537        assert!(err.to_string().contains("on its own"), "{err}");
6538
6539        // Not given, so it imposes nothing.
6540        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6541    }
6542
6543    #[test]
6544    fn an_exclusive_flag_conflicts_with_clause_arguments() {
6545        let spec: Spec = r#"name "ex"
6546bin "ex"
6547flag "--dump" exclusive=#true
6548clause "tasks" separator=":::" {
6549  arg "<task>"
6550}
6551"#
6552        .parse()
6553        .unwrap();
6554
6555        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6556        assert!(err.to_string().contains("on its own"), "{err}");
6557    }
6558
6559    #[test]
6560    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6561        // Only what was supplied counts, as `conflicts` reads it. A default counting as
6562        // company would make an exclusive flag unusable on any command that has one.
6563        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6564            .parse()
6565            .unwrap();
6566
6567        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6568        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6569    }
6570
6571    #[test]
6572    fn an_exclusive_flag_bypasses_required_siblings() {
6573        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6574            .parse()
6575            .unwrap();
6576
6577        parse(&spec, &input(&["ex", "--dump"]))
6578            .expect("exclusive is the command's requiredness escape");
6579        assert!(parse(
6580            &spec,
6581            &input(&["ex", "--dump", "--out", "somewhere", "target"])
6582        )
6583        .is_err());
6584    }
6585
6586    #[test]
6587    fn an_environment_value_counts_for_an_exclusive_flag() {
6588        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6589            .parse()
6590            .unwrap();
6591
6592        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6593        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6594    }
6595
6596    #[test]
6597    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6598        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6599            .parse()
6600            .unwrap();
6601
6602        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6603        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6604    }
6605
6606    #[test]
6607    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6608        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6609            .parse()
6610            .unwrap();
6611
6612        parse(&spec, &input(&["ex", "run", "--clean"]))
6613            .expect("the child flag is alone within the child command");
6614        assert!(
6615            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6616            "the parent flag still conflicts with selecting the child"
6617        );
6618    }
6619
6620    #[test]
6621    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6622        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6623            .parse()
6624            .unwrap();
6625
6626        parse(&spec, &input(&["ex", "run", "--clean"]))
6627            .expect("the child-local exclusive flag is alone inside the child command");
6628        assert!(
6629            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6630            "the ancestor spelling still conflicts with selecting the child"
6631        );
6632    }
6633
6634    #[test]
6635    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6636        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
6637            .parse()
6638            .unwrap();
6639
6640        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6641        assert!(
6642            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6643            "the distinct parent declaration is still company despite sharing a name"
6644        );
6645    }
6646
6647    #[test]
6648    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6649        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6650            .parse()
6651            .unwrap();
6652
6653        parse(&spec, &input(&["ex", "run", "--clean"]))
6654            .expect("the child exclusive flag is valid alone");
6655        assert!(
6656            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6657            "merging with the inherited global must not discard child exclusivity"
6658        );
6659    }
6660
6661    #[test]
6662    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6663        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6664            .parse()
6665            .unwrap();
6666
6667        parse(&spec, &input(&["ex", "run", "--clean"]))
6668            .expect("the typed long form belongs to the child declaration");
6669        assert!(
6670            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6671            "the inherited short form still belongs to the ancestor"
6672        );
6673        assert!(
6674            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6675            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6676        );
6677    }
6678
6679    #[test]
6680    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
6681        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
6682            .parse()
6683            .unwrap();
6684
6685        parse(&spec, &input(&["ex", "run", "--clean"]))
6686            .expect("the child's spelling does not activate the orphan ancestor alias");
6687        assert!(
6688            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6689            "the inherited short alias still belongs to the ancestor exclusive flag"
6690        );
6691    }
6692
6693    #[test]
6694    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
6695        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"
6696            .parse()
6697            .unwrap();
6698
6699        assert!(
6700            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
6701            "the inherited negated alias still belongs to the ancestor exclusive flag"
6702        );
6703    }
6704
6705    #[test]
6706    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
6707        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
6708        // an unrelated inherited global already owns. That collision is resolved in the other
6709        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
6710        // the `--clean` it declared, and its exclusivity holds.
6711        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"
6712            .parse()
6713            .unwrap();
6714
6715        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
6716        assert!(
6717            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6718            "one unrelated alias collision cannot disown the child from its own flag"
6719        );
6720    }
6721
6722    #[test]
6723    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
6724        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
6725        // subcommand word the flag can only be the ancestor's, because that is the only one in
6726        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
6727        // is set. The pair below differ in nothing else, which is what makes this one rule
6728        // rather than two behaviors.
6729        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6730            .parse()
6731            .unwrap();
6732        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
6733            .expect("the ancestor owns this occurrence, and it is not exclusive");
6734        assert!(
6735            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6736            "after the subcommand word the child's declaration is in scope, and it is exclusive"
6737        );
6738
6739        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6740            .parse()
6741            .unwrap();
6742        assert!(
6743            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
6744            "the same rule, with an exclusive ancestor: selecting the child is company for it"
6745        );
6746    }
6747
6748    #[test]
6749    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
6750        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
6751        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
6752        // the ancestor ever declared, so the ancestor's answer still governs it.
6753        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
6754            .parse()
6755            .unwrap();
6756
6757        assert!(
6758            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6759            "the orphan ancestor alias is still the ancestor's exclusive flag"
6760        );
6761        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
6762            .expect("the child's own spelling drops the exclusivity the child did not restate");
6763    }
6764
6765    #[test]
6766    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
6767        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
6768        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
6769        // occurrence to the ancestor because `-c` appeared in it lost that.
6770        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6771            .parse()
6772            .unwrap();
6773
6774        assert!(
6775            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
6776            "the child spelling is exclusive whatever it was typed beside"
6777        );
6778        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
6779            .expect("the ancestor's own spelling was never exclusive");
6780    }
6781
6782    #[test]
6783    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
6784        // An environment value has no spelling to attribute, so the declaration the selected
6785        // command has in scope answers — in both directions. Comparing whole alias sets asked
6786        // the ancestor instead, because the merged flag also carries its orphan `-c`.
6787        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"
6788            .parse()
6789            .unwrap();
6790
6791        assert!(
6792            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
6793            "the child added exclusivity the environment value has to honor"
6794        );
6795
6796        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"
6797            .parse()
6798            .unwrap();
6799
6800        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
6801            .expect("the child dropped the exclusivity, and the environment value follows it");
6802    }
6803
6804    #[test]
6805    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
6806        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
6807        // the same reason the companion check does.
6808        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"
6809            .parse()
6810            .unwrap();
6811
6812        parse(&spec, &input(&["ex", "run", "--clean"]))
6813            .expect("a merged child exclusive flag is still the command's requiredness escape");
6814    }
6815
6816    #[test]
6817    fn a_group_allows_one_member_and_refuses_two() {
6818        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
6819            .parse()
6820            .unwrap();
6821
6822        // One is fine, and so is none: a plain group says "at most one".
6823        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
6824        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
6825
6826        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
6827        assert!(err.to_string().contains("group input"), "{err}");
6828    }
6829
6830    #[test]
6831    fn positional_selectors_work_in_conflicts_and_groups() {
6832        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
6833            .parse()
6834            .unwrap();
6835        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
6836            .expect("the flag alone is valid");
6837        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
6838        assert!(parse(
6839            &conflicts,
6840            &input(&["ex", "--from-file", "vars.env", "literal"])
6841        )
6842        .is_err());
6843
6844        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
6845            .parse()
6846            .unwrap();
6847        assert!(parse(
6848            &positional_source,
6849            &input(&["ex", "--from-file", "vars.env", "literal"])
6850        )
6851        .is_err());
6852
6853        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
6854            .parse()
6855            .unwrap();
6856        assert!(parse(&group, &input(&["ex"])).is_err());
6857        parse(&group, &input(&["ex", "target-name"]))
6858            .expect("a positional satisfies a required group");
6859        assert!(parse(
6860            &group,
6861            &input(&["ex", "--file", "input.txt", "target-name"])
6862        )
6863        .is_err());
6864    }
6865
6866    #[test]
6867    fn a_required_group_needs_one_of_its_members() {
6868        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6869            .parse()
6870            .unwrap();
6871
6872        let err = parse(&spec, &input(&["ex"])).unwrap_err();
6873        // The members, because that is what a user has to type; the name, because a
6874        // command with several groups would otherwise report the same sentence twice.
6875        assert!(err.to_string().contains("--file, --url"), "{err}");
6876        assert!(err.to_string().contains("input"), "{err}");
6877
6878        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
6879    }
6880
6881    #[test]
6882    fn a_multiple_group_only_polices_requiredness() {
6883        // `multiple` with `required` is "at least one of these", so two is fine and
6884        // none is not.
6885        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
6886            .parse()
6887            .unwrap();
6888
6889        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
6890        assert!(parse(&spec, &input(&["ex"])).is_err());
6891    }
6892
6893    #[test]
6894    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
6895        // The two halves of a group are two kinds of rule, and they read a default
6896        // differently on purpose. Requiredness asks whether a member has a value, and a
6897        // default is a value — the rule `requires` follows. Exclusivity asks what the
6898        // user supplied, because a defaulted member counted as supplied would collide
6899        // with the sibling they actually typed and refuse a correct command line.
6900        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6901            .parse()
6902            .unwrap();
6903
6904        parse(&spec, &input(&["ex"])).expect("the default fills the group");
6905        parse(&spec, &input(&["ex", "--url", "u"]))
6906            .expect("the default must not conflict with the flag the user typed");
6907    }
6908
6909    #[test]
6910    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
6911        // `-f` and `--file` are one flag. Counted by selector, giving it once would
6912        // report it as conflicting with itself.
6913        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
6914            .parse()
6915            .unwrap();
6916
6917        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
6918        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
6919
6920        // A genuine collision is still one.
6921        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
6922        assert!(err.to_string().contains("group input"), "{err}");
6923    }
6924
6925    #[test]
6926    fn a_group_reads_the_environment_as_given() {
6927        // The environment does count, which is the same asymmetry `conflicts` has: an
6928        // env var is somebody saying something, a default is nobody saying anything.
6929        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6930            .parse()
6931            .unwrap();
6932
6933        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
6934    }
6935
6936    #[test]
6937    fn a_requirement_names_the_flag_that_is_missing() {
6938        // Reported as the missing flag rather than as something wrong with `--out`,
6939        // which is what clap says for an unmet `requires` and what a user can act on.
6940        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6941            .parse()
6942            .unwrap();
6943
6944        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6945        assert!(
6946            err.to_string().contains("format"),
6947            "the missing flag should be named: {err}"
6948        );
6949
6950        // Satisfied, in either order.
6951        for words in [
6952            &["ex", "--out", "a.txt", "--format", "json"][..],
6953            &["ex", "--format", "json", "--out", "a.txt"][..],
6954        ] {
6955            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6956        }
6957
6958        // Nothing happens when the flag that imposes the rule is absent: a requirement
6959        // is a consequence of using the flag, not a rule about the command line.
6960        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6961    }
6962
6963    #[test]
6964    fn a_value_activates_only_its_conditional_requirement() {
6965        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"
6966            .parse()
6967            .unwrap();
6968
6969        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6970            .expect("an unrelated value requires nothing");
6971
6972        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6973        assert!(key.to_string().contains("key"), "{key}");
6974        parse(
6975            &spec,
6976            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6977        )
6978        .expect("the matching requirement is satisfied");
6979
6980        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6981        assert!(token.to_string().contains("token"), "{token}");
6982    }
6983
6984    #[test]
6985    fn conditional_requirements_read_explicit_env_but_not_defaults() {
6986        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"
6987            .parse()
6988            .unwrap();
6989        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6990        assert!(err.to_string().contains("key"), "{err}");
6991
6992        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"
6993            .parse()
6994            .unwrap();
6995        parse(&from_default, &input(&["ex"]))
6996            .expect("a default is not an explicit conditional value");
6997    }
6998
6999    #[test]
7000    fn command_line_values_override_env_for_conditional_requirements() {
7001        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7002            .parse()
7003            .unwrap();
7004
7005        parse_with_env(
7006            &spec,
7007            &["ex", "--config", "ordinary.toml"],
7008            &[("EX_CONFIG", "special.toml")],
7009        )
7010        .expect("the command-line value takes precedence over the environment");
7011    }
7012
7013    #[test]
7014    fn conditional_requirements_normalize_boolean_env_values() {
7015        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7016            .parse()
7017            .unwrap();
7018
7019        for value in ["1", "true", "True", "TRUE"] {
7020            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7021            assert!(err.to_string().contains("key"), "{value}: {err}");
7022        }
7023        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7024            .expect("a false environment value does not activate a true condition");
7025    }
7026
7027    #[test]
7028    fn a_default_satisfies_a_requirement() {
7029        // The flag it names has a value, which is the question a requirement asks. Read
7030        // any other way, `--format` would be missing here and present ten lines further
7031        // down, where plain required-ness reads the same default as filling it.
7032        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7033            .parse()
7034            .unwrap();
7035
7036        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7037            .expect("a defaulted flag is not a missing one");
7038    }
7039
7040    #[test]
7041    fn a_present_flag_binds_a_conditional_default() {
7042        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7043            .parse()
7044            .unwrap();
7045
7046        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7047        assert_eq!(
7048            with.as_env().get("usage_bin_names").map(String::as_str),
7049            Some("true")
7050        );
7051
7052        let without = parse(&spec, &input(&["ex"])).unwrap();
7053        assert!(
7054            !without.as_env().contains_key("usage_bin_names"),
7055            "IsPresent does nothing when the selector is absent"
7056        );
7057    }
7058
7059    #[test]
7060    fn an_equals_condition_binds_a_conditional_default() {
7061        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7062            .parse()
7063            .unwrap();
7064
7065        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7066        assert_eq!(
7067            json.as_env().get("usage_style").map(String::as_str),
7068            Some("pretty")
7069        );
7070        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7071        assert!(!yaml.as_env().contains_key("usage_style"));
7072    }
7073
7074    #[test]
7075    fn an_equals_condition_reads_a_negated_flag() {
7076        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7077            .parse()
7078            .unwrap();
7079
7080        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7081        assert_eq!(
7082            off.as_env().get("usage_pretty").map(String::as_str),
7083            Some("true")
7084        );
7085        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7086        assert!(
7087            !on.as_env().contains_key("usage_pretty"),
7088            "--json is true, so when=false should miss"
7089        );
7090    }
7091
7092    #[test]
7093    fn the_first_matching_conditional_default_wins() {
7094        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"
7095            .parse()
7096            .unwrap();
7097
7098        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7099        assert_eq!(
7100            out.as_env().get("usage_style").map(String::as_str),
7101            Some("compact")
7102        );
7103    }
7104
7105    #[test]
7106    fn argv_and_env_suppress_a_conditional_default() {
7107        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7108            .parse()
7109            .unwrap();
7110
7111        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7112        assert_eq!(
7113            from_env.as_env().get("usage_bin_names").map(String::as_str),
7114            Some("false"),
7115            "the target's environment wins over default_if"
7116        );
7117    }
7118
7119    #[test]
7120    fn a_sibling_env_activates_a_conditional_default() {
7121        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7122            .parse()
7123            .unwrap();
7124
7125        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7126        assert_eq!(
7127            out.as_env().get("usage_bin_names").map(String::as_str),
7128            Some("true")
7129        );
7130    }
7131
7132    #[test]
7133    fn a_default_does_not_activate_a_conditional_default() {
7134        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7135        // bind would put json's default into `out.flags` and then treat it as
7136        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7137        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7138            .parse()
7139            .unwrap();
7140
7141        let out = parse(&spec, &input(&["ex"])).unwrap();
7142        assert_eq!(
7143            out.as_env().get("usage_json").map(String::as_str),
7144            Some("true")
7145        );
7146        assert!(
7147            !out.as_env().contains_key("usage_pretty"),
7148            "a default is not an explicit value for default_if"
7149        );
7150    }
7151
7152    #[test]
7153    fn a_conditional_default_does_not_activate_requires_if() {
7154        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"
7155            .parse()
7156            .unwrap();
7157
7158        parse(&spec, &input(&["ex", "--json"]))
7159            .expect("a default_if value is not explicit for requires_if");
7160        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7161    }
7162
7163    #[test]
7164    fn a_conditional_default_satisfies_a_requirement() {
7165        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7166            .parse()
7167            .unwrap();
7168
7169        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7170            .expect("default_if fills the required flag");
7171        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7172    }
7173
7174    #[test]
7175    fn an_environment_value_satisfies_a_requirement() {
7176        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7177            .parse()
7178            .unwrap();
7179
7180        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7181        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7182            .expect("the environment supplies it");
7183    }
7184
7185    #[test]
7186    fn a_requirement_is_satisfied_by_a_short_form() {
7187        // The selector may spell the other flag any way it answers to, so the check
7188        // resolves it the way every other selector is resolved rather than matching
7189        // text. The error names the flag, not the selector.
7190        let spec: Spec =
7191            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7192                .parse()
7193                .unwrap();
7194
7195        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7196
7197        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7198        assert!(err.to_string().contains("key"), "{err}");
7199    }
7200
7201    #[test]
7202    fn conflicting_flags_are_rejected_in_either_order() {
7203        // Declared once, on `--file`, which is all clap exposes — so the check has to
7204        // be order-independent by looking at every flag that was given rather than at
7205        // the one that declared the conflict.
7206        let spec: Spec =
7207            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7208                .parse()
7209                .unwrap();
7210
7211        for words in [
7212            &["ex", "--file", "a.txt", "--stdin"][..],
7213            &["ex", "--stdin", "--file", "a.txt"][..],
7214        ] {
7215            let err = parse(&spec, &input(words)).unwrap_err();
7216            assert!(
7217                err.to_string().contains("conflicts with --stdin"),
7218                "{words:?} should be refused: {err}"
7219            );
7220        }
7221
7222        // Either one alone is fine.
7223        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7224        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7225    }
7226
7227    #[test]
7228    fn unknown_flags_are_values_by_default() {
7229        // The default, and the reason it is the default: a spec often parses a
7230        // command line whose flags belong to something else.
7231        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7232            .parse()
7233            .unwrap();
7234        let out = parse(
7235            &spec,
7236            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7237        )
7238        .unwrap();
7239        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7240        assert_eq!(out.args[rest].to_string(), "--wat x");
7241    }
7242
7243    #[test]
7244    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7245        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7246            .parse()
7247            .unwrap();
7248        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7249            .expect("a repeat is a correction by default");
7250        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7251        assert_eq!(out.flags[jobs].to_string(), "2");
7252        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7253            .expect("switches use the same default");
7254
7255        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7256            .parse()
7257            .unwrap();
7258        for words in [
7259            &["ex", "--jobs", "1", "--jobs", "2"][..],
7260            &["ex", "--verbose", "--verbose"][..],
7261        ] {
7262            let err = parse(&strict, &input(words)).unwrap_err();
7263            assert!(
7264                err.to_string().contains("cannot be used multiple times"),
7265                "{err}"
7266            );
7267        }
7268
7269        let reparsed: Spec = strict.to_string().parse().unwrap();
7270        assert!(!reparsed.cmd.args_override_self);
7271    }
7272
7273    #[test]
7274    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7275        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7276            .parse()
7277            .unwrap();
7278
7279        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7280            .expect("opposite forms override each other");
7281        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7282        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7283
7284        for words in [
7285            &["ex", "--color", "--color"][..],
7286            &["ex", "--no-color", "--no-color"][..],
7287        ] {
7288            let err = parse(&spec, &input(words)).unwrap_err();
7289            assert!(err.to_string().contains("cannot be used multiple times"));
7290        }
7291    }
7292
7293    #[test]
7294    fn strict_global_flags_may_repeat_across_command_levels() {
7295        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"
7296            .parse()
7297            .unwrap();
7298
7299        let out = parse(
7300            &spec,
7301            &input(&[
7302                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7303            ]),
7304        )
7305        .expect("an inherited global is allowed once at each command level");
7306        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7307        assert_eq!(out.flags[jobs].to_string(), "2");
7308
7309        for words in [
7310            &["ex", "--color", "--color", "run", "--no-color"][..],
7311            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7312        ] {
7313            let err = parse(&spec, &input(words)).unwrap_err();
7314            assert!(err.to_string().contains("cannot be used multiple times"));
7315        }
7316    }
7317
7318    #[test]
7319    fn a_subcommand_can_negate_only_its_parents_requirements() {
7320        let base = r#"name "ex"
7321bin "ex"
7322flag "--config" required=#true
7323flag "--mode" requires="--config"
7324flag "--other"
7325arg "<input>"
7326group "source" "--config" "--other" required=#true
7327cmd "run" { flag "--child" required=#true }
7328"#;
7329        let strict: Spec = base.parse().unwrap();
7330        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7331        let message = err.to_string();
7332        assert!(
7333            message.contains("input") || message.contains("config"),
7334            "{message}"
7335        );
7336
7337        let negated: Spec = base
7338            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7339            .parse()
7340            .unwrap();
7341        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7342        assert!(
7343            err.to_string().contains("child"),
7344            "the selected command keeps its own requirements: {err}"
7345        );
7346
7347        let mut child_optional = negated.clone();
7348        child_optional.cmd.subcommands["run"].flags[0].required = false;
7349        parse(&child_optional, &input(&["ex", "run"]))
7350            .expect("the child selection satisfies all parent requirements");
7351        parse(&child_optional, &input(&["ex", "--mode", "run"]))
7352            .expect("parent requires relationships are negated too");
7353    }
7354
7355    #[test]
7356    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7357        let spec: Spec = r#"name "ex"
7358bin "ex"
7359args_conflicts_with_subcommands #true
7360flag "--verbose"
7361cmd "run"
7362"#
7363        .parse()
7364        .unwrap();
7365
7366        parse(&spec, &input(&["ex", "run"]))
7367            .expect("the subcommand is valid without a parent argument");
7368        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7369        assert!(
7370            err.to_string().contains("cannot be used with arguments"),
7371            "{err}"
7372        );
7373    }
7374
7375    #[test]
7376    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7377        let base = r#"name "ex"
7378bin "ex"
7379flag "--values <value>..."
7380cmd "run"
7381"#;
7382        let plain: Spec = base.parse().unwrap();
7383        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7384        assert_eq!(out.cmd.name, "ex");
7385
7386        let precedence: Spec = base
7387            .replacen(
7388                "bin \"ex\"",
7389                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7390                1,
7391            )
7392            .parse()
7393            .unwrap();
7394        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7395        assert_eq!(out.cmd.name, "run");
7396    }
7397
7398    #[test]
7399    fn a_required_positional_can_follow_an_unfilled_optional_one() {
7400        let base = r#"name "ex"
7401bin "ex"
7402arg "[optional]"
7403arg "<required>"
7404"#;
7405        let plain: Spec = base.parse().unwrap();
7406        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7407        assert!(err.to_string().contains("required"), "{err}");
7408
7409        let enabled: Spec = base
7410            .replacen(
7411                "bin \"ex\"",
7412                "bin \"ex\"\nallow_missing_positional #true",
7413                1,
7414            )
7415            .parse()
7416            .unwrap();
7417        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7418        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7419        let value = &out
7420            .args
7421            .iter()
7422            .find(|(arg, _)| arg.name == "required")
7423            .unwrap()
7424            .1;
7425        assert!(matches!(value, ParseValue::String(value) if value == "value"));
7426    }
7427
7428    #[test]
7429    fn sigil_args_do_not_block_optional_positional_skipping() {
7430        let spec: Spec = r#"name "ex"
7431bin "ex"
7432allow_missing_positional #true
7433arg "[optional]"
7434arg "[tool]" sigil="@"
7435arg "<required>"
7436"#
7437        .parse()
7438        .unwrap();
7439
7440        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7441        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7442        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7443        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7444        assert_eq!(out.args[tool].to_string(), "node");
7445        assert_eq!(out.args[required].to_string(), "value");
7446    }
7447
7448    #[test]
7449    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7450        let spec: Spec =
7451            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7452                .parse()
7453                .unwrap();
7454        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7455        assert!(
7456            err.to_string().contains("--wat"),
7457            "the message should name the token: {err}"
7458        );
7459
7460        // The positional opts into the narrower negative-number carve-out without
7461        // accepting arbitrary unknown flags.
7462        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
7463        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7464        assert_eq!(out.args[rest].to_string(), "-1");
7465
7466        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
7467        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7468        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7469    }
7470
7471    #[test]
7472    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
7473        let spec: Spec = r#"
7474name "ex"
7475bin "ex"
7476unknown_flags "error"
7477flag "-0 --print0" global=#true
7478cmd "run" {
7479  flag "--force"
7480}
7481"#
7482        .parse()
7483        .unwrap();
7484        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
7485        assert_eq!(out.cmd.name, "run");
7486        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7487        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
7488        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7489        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
7490    }
7491
7492    #[test]
7493    fn a_command_may_override_the_cli_wide_setting() {
7494        // Strict overall, lenient for the one command that forwards options.
7495        let spec: Spec = r#"
7496name "ex"
7497bin "ex"
7498unknown_flags "error"
7499cmd "exec" unknown_flags="value" {
7500  arg "[rest]..."
7501}
7502cmd "build" {
7503  arg "[rest]..."
7504}
7505"#
7506        .parse()
7507        .unwrap();
7508
7509        let out = parse(
7510            &spec,
7511            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
7512        )
7513        .unwrap();
7514        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7515        assert_eq!(out.args[rest].to_string(), "--wat");
7516
7517        assert!(
7518            parse(
7519                &spec,
7520                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
7521            )
7522            .is_err(),
7523            "a command that says nothing inherits the CLI's choice"
7524        );
7525    }
7526
7527    #[test]
7528    fn the_setting_survives_a_round_trip() {
7529        let spec: Spec =
7530            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7531                .parse()
7532                .unwrap();
7533        let reparsed: Spec = spec.to_string().parse().unwrap();
7534        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7535        assert_eq!(
7536            reparsed.cmd.subcommands["x"].unknown_flags,
7537            Some(UnknownFlags::Value)
7538        );
7539    }
7540
7541    #[test]
7542    fn test_default_subcommand() {
7543        // Test that default_subcommand routes to the specified subcommand
7544        let run_cmd = SpecCommand::builder()
7545            .name("run")
7546            .arg(SpecArg::builder().name("task").build())
7547            .build();
7548        let mut cmd = SpecCommand::builder().name("test").build();
7549        cmd.subcommands.insert("run".to_string(), run_cmd);
7550
7551        let spec = Spec {
7552            name: "test".to_string(),
7553            bin: "test".to_string(),
7554            cmd,
7555            default_subcommand: Some("run".to_string()),
7556            ..Default::default()
7557        };
7558
7559        // "test mytask" should be parsed as if it were "test run mytask"
7560        let input = vec!["test".to_string(), "mytask".to_string()];
7561        let parsed = parse(&spec, &input).unwrap();
7562
7563        // Should have two commands: root and "run"
7564        assert_eq!(parsed.cmds.len(), 2);
7565        assert_eq!(parsed.cmds[1].name, "run");
7566
7567        // Should have parsed the task argument
7568        assert_eq!(parsed.args.len(), 1);
7569        let arg = parsed.args.keys().next().unwrap();
7570        assert_eq!(arg.name, "task");
7571        let value = parsed.args.values().next().unwrap();
7572        assert_eq!(value.to_string(), "mytask");
7573    }
7574
7575    #[test]
7576    fn default_subcommand_outranks_root_sigil_arg() {
7577        let spec: Spec = r#"
7578name "test"
7579bin "test"
7580default_subcommand "run"
7581arg "[tools]..." sigil="+"
7582cmd "run" { arg "<task>" }
7583"#
7584        .parse()
7585        .unwrap();
7586
7587        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7588        assert_eq!(parsed.cmd.name, "run");
7589        let value = |name| {
7590            parsed
7591                .args
7592                .iter()
7593                .find(|(arg, _)| arg.name == name)
7594                .map(|(_, value)| value.to_string())
7595                .unwrap()
7596        };
7597        assert_eq!(value("tools"), "node");
7598        assert_eq!(value("task"), "node");
7599    }
7600
7601    #[test]
7602    fn test_default_subcommand_explicit_still_works() {
7603        // Test that explicit subcommand takes precedence
7604        let run_cmd = SpecCommand::builder()
7605            .name("run")
7606            .arg(SpecArg::builder().name("task").build())
7607            .build();
7608        let other_cmd = SpecCommand::builder()
7609            .name("other")
7610            .arg(SpecArg::builder().name("other_arg").build())
7611            .build();
7612        let mut cmd = SpecCommand::builder().name("test").build();
7613        cmd.subcommands.insert("run".to_string(), run_cmd);
7614        cmd.subcommands.insert("other".to_string(), other_cmd);
7615
7616        let spec = Spec {
7617            name: "test".to_string(),
7618            bin: "test".to_string(),
7619            cmd,
7620            default_subcommand: Some("run".to_string()),
7621            ..Default::default()
7622        };
7623
7624        // "test other foo" should use "other" subcommand, not default
7625        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7626        let parsed = parse(&spec, &input).unwrap();
7627
7628        // Should have used "other" subcommand
7629        assert_eq!(parsed.cmds.len(), 2);
7630        assert_eq!(parsed.cmds[1].name, "other");
7631    }
7632
7633    #[test]
7634    fn test_default_subcommand_applies_only_at_the_root() {
7635        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
7636        // was being looked up wherever the parser happened to be standing, so a command with
7637        // an unrelated subcommand of the same name acquired a default of its own: with
7638        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
7639        // `zzz` there. Nothing declared that, and nothing could have.
7640        let mut config_ls = SpecCommand::builder().name("ls").build();
7641        config_ls.args.push(SpecArg::builder().name("what").build());
7642        let mut config_cmd = SpecCommand::builder().name("config").build();
7643        config_cmd.subcommands.insert("ls".to_string(), config_ls);
7644
7645        // The root's own `ls`, which is what its default points at. It takes an argument so
7646        // that a routed word has somewhere to land.
7647        let mut root_ls = SpecCommand::builder().name("ls").build();
7648        root_ls.args.push(SpecArg::builder().name("what").build());
7649        let mut cmd = SpecCommand::builder().name("ex").build();
7650        cmd.subcommands.insert("ls".to_string(), root_ls);
7651        cmd.subcommands.insert("config".to_string(), config_cmd);
7652
7653        let spec = Spec {
7654            name: "ex".to_string(),
7655            bin: "ex".to_string(),
7656            cmd,
7657            default_subcommand: Some("ls".to_string()),
7658            ..Default::default()
7659        };
7660
7661        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
7662        // own business — and `config` takes no argument, so this is an error rather than a
7663        // silent descent.
7664        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7665        assert!(
7666            parse(&spec, &input).is_err(),
7667            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7668        );
7669
7670        // At the root, where it is declared, it still applies.
7671        let input = vec!["ex".to_string(), "zzz".to_string()];
7672        let parsed = parse(&spec, &input).expect("the root's default applies");
7673        assert_eq!(
7674            parsed
7675                .cmds
7676                .iter()
7677                .map(|c| c.name.as_str())
7678                .collect::<Vec<_>>(),
7679            ["ex", "ls"]
7680        );
7681        assert_eq!(
7682            parsed.args.values().next().map(|v| v.to_string()),
7683            Some("zzz".to_string()),
7684            "and the word binds inside the command it reached"
7685        );
7686    }
7687
7688    #[test]
7689    fn test_default_subcommand_with_nested_subcommands() {
7690        // Test that default_subcommand works when the default subcommand has nested subcommands.
7691        // This is the mise use case: "mise say" should be parsed as "mise run say"
7692        // where "say" is a subcommand of "run" (a task).
7693        let say_cmd = SpecCommand::builder()
7694            .name("say")
7695            .arg(SpecArg::builder().name("name").build())
7696            .build();
7697        let mut run_cmd = SpecCommand::builder().name("run").build();
7698        run_cmd.subcommands.insert("say".to_string(), say_cmd);
7699
7700        let mut cmd = SpecCommand::builder().name("test").build();
7701        cmd.subcommands.insert("run".to_string(), run_cmd);
7702
7703        let spec = Spec {
7704            name: "test".to_string(),
7705            bin: "test".to_string(),
7706            cmd,
7707            default_subcommand: Some("run".to_string()),
7708            ..Default::default()
7709        };
7710
7711        // "test say hello" should be parsed as "test run say hello"
7712        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
7713        let parsed = parse(&spec, &input).unwrap();
7714
7715        // Should have three commands: root, "run", and "say"
7716        assert_eq!(parsed.cmds.len(), 3);
7717        assert_eq!(parsed.cmds[0].name, "test");
7718        assert_eq!(parsed.cmds[1].name, "run");
7719        assert_eq!(parsed.cmds[2].name, "say");
7720
7721        // Should have parsed the "name" argument
7722        assert_eq!(parsed.args.len(), 1);
7723        let arg = parsed.args.keys().next().unwrap();
7724        assert_eq!(arg.name, "name");
7725        let value = parsed.args.values().next().unwrap();
7726        assert_eq!(value.to_string(), "hello");
7727    }
7728
7729    /// Build a spec equivalent to the post-mount structure produced by mise's
7730    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
7731    /// subcommand that re-declares the same flag as NON-global, and a mounted task
7732    /// (`sample:run`) carrying a positional arg with `choices`.
7733    ///
7734    /// We construct the merged structure directly instead of executing a real mount so the
7735    /// test stays hermetic and cross-platform while still exercising the parser defect.
7736    fn mounted_global_flag_spec() -> Spec {
7737        let task_cmd = SpecCommand::builder()
7738            .name("sample:run")
7739            .arg(
7740                SpecArg::builder()
7741                    .name("profile")
7742                    .choices(["alpha", "beta", "gamma"])
7743                    .build(),
7744            )
7745            .build();
7746        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
7747        let mut run_cmd = SpecCommand::builder()
7748            .name("run")
7749            .flag(
7750                SpecFlag::builder()
7751                    .name("cd")
7752                    .short('C')
7753                    .long("cd")
7754                    .arg(SpecArg::builder().name("dir").build())
7755                    .global(false)
7756                    .build(),
7757            )
7758            .build();
7759        run_cmd
7760            .subcommands
7761            .insert("sample:run".to_string(), task_cmd);
7762
7763        let mut cmd = SpecCommand::builder()
7764            .name("test")
7765            .flag(
7766                SpecFlag::builder()
7767                    .name("cd")
7768                    .short('C')
7769                    .long("cd")
7770                    .arg(SpecArg::builder().name("dir").build())
7771                    .global(true)
7772                    .build(),
7773            )
7774            .build();
7775        cmd.subcommands.insert("run".to_string(), run_cmd);
7776
7777        Spec {
7778            name: "test".to_string(),
7779            bin: "test".to_string(),
7780            cmd,
7781            ..Default::default()
7782        }
7783    }
7784
7785    #[test]
7786    fn test_prefix_global_flag_does_not_pollute_choices() {
7787        // Regression for the parser-side root cause referenced by jdx/mise#10069.
7788        //
7789        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
7790        // then into the mounted `sample:run`) used to drop the inherited global flag from
7791        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
7792        // mis-validated against the task's `choices` positional arg.
7793        let spec = mounted_global_flag_spec();
7794
7795        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
7796        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
7797        for words in [
7798            &["test", "-C", "/tmp", "run", "sample:run"][..],
7799            // Embedded-value form must behave identically.
7800            &["test", "--cd=/tmp", "run", "sample:run"][..],
7801        ] {
7802            let parsed = parse_partial(&spec, &input(words)).unwrap();
7803            assert_eq!(
7804                parsed
7805                    .cmds
7806                    .iter()
7807                    .map(|c| c.name.as_str())
7808                    .collect::<Vec<_>>(),
7809                vec!["test", "run", "sample:run"],
7810            );
7811            // No positional arg should have been consumed by the leftover global-flag tokens.
7812            assert!(
7813                parsed.args.is_empty(),
7814                "args should be empty, got {:?}",
7815                parsed.args
7816            );
7817
7818            // Fix (B): the inherited global flag survives the descent even though `run`
7819            // re-declares `-C/--cd` as non-global.
7820            let cd = parsed
7821                .available_flags
7822                .get("--cd")
7823                .expect("--cd should remain available after descending into the subcommand");
7824            assert!(cd.global, "--cd must stay global after descent");
7825            assert!(
7826                parsed.available_flags.get("-C").is_some_and(|f| f.global),
7827                "-C must stay global after descent",
7828            );
7829
7830            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
7831            // for normal execution and for the env passed to mount scripts. (Removing the
7832            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
7833            assert_eq!(
7834                parsed.as_env().get("usage_cd").map(String::as_str),
7835                Some("/tmp"),
7836                "global flag value must survive in as_env(), got {:?}",
7837                parsed.as_env(),
7838            );
7839        }
7840
7841        // A real, valid choice still parses through the global flag prefix.
7842        let parsed = parse_partial(
7843            &spec,
7844            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
7845        )
7846        .unwrap();
7847        assert_eq!(parsed.args.len(), 1);
7848        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7849
7850        // And genuinely invalid choices are still rejected (we didn't disable validation).
7851        assert_parse_err(
7852            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
7853            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7854        );
7855    }
7856
7857    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
7858    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
7859    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
7860    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
7861    fn mounted_orphan_short_spec() -> Spec {
7862        let task_cmd = SpecCommand::builder()
7863            .name("sample:run")
7864            .arg(
7865                SpecArg::builder()
7866                    .name("profile")
7867                    .choices(["alpha", "beta", "gamma"])
7868                    .build(),
7869            )
7870            .build();
7871        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
7872        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
7873        let mut run_cmd = SpecCommand::builder()
7874            .name("run")
7875            .flag(
7876                SpecFlag::builder()
7877                    .name("raw")
7878                    .short('r')
7879                    .long("raw")
7880                    .global(false)
7881                    .build(),
7882            )
7883            .flag(
7884                SpecFlag::builder()
7885                    .name("force")
7886                    .short('f')
7887                    .long("force")
7888                    .global(false)
7889                    .build(),
7890            )
7891            .build();
7892        run_cmd
7893            .subcommands
7894            .insert("sample:run".to_string(), task_cmd);
7895
7896        // Root global is LONG-ONLY: `--raw` with no short.
7897        let mut cmd = SpecCommand::builder()
7898            .name("test")
7899            .flag(
7900                SpecFlag::builder()
7901                    .name("raw")
7902                    .long("raw")
7903                    .global(true)
7904                    .build(),
7905            )
7906            .build();
7907        cmd.subcommands.insert("run".to_string(), run_cmd);
7908
7909        Spec {
7910            name: "test".to_string(),
7911            bin: "test".to_string(),
7912            cmd,
7913            ..Default::default()
7914        }
7915    }
7916
7917    #[test]
7918    fn test_orphan_short_alias_survives_merge() {
7919        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
7920        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
7921        // added short `-r` must be unioned onto the surviving inherited global flag instead of
7922        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
7923        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
7924        let spec = mounted_orphan_short_spec();
7925
7926        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
7927        assert_eq!(
7928            parsed
7929                .cmds
7930                .iter()
7931                .map(|c| c.name.as_str())
7932                .collect::<Vec<_>>(),
7933            vec!["test", "run", "sample:run"],
7934        );
7935
7936        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
7937        // and the original long `--raw` is still global too.
7938        assert!(
7939            parsed.available_flags.get("-r").is_some_and(|f| f.global),
7940            "-r must be merged onto the inherited global flag and stay global after descent",
7941        );
7942        assert!(
7943            parsed
7944                .available_flags
7945                .get("--raw")
7946                .is_some_and(|f| f.global),
7947            "--raw must stay global after descent",
7948        );
7949
7950        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
7951        assert!(
7952            parsed.args.is_empty(),
7953            "args should be empty, got {:?}",
7954            parsed.args
7955        );
7956
7957        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
7958        assert_eq!(
7959            parsed.as_env().get("usage_raw").map(String::as_str),
7960            Some("true"),
7961            "merged short's value must survive in as_env(), got {:?}",
7962            parsed.as_env(),
7963        );
7964
7965        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
7966        // promoted/merged — it is correctly dropped when descending into the mount.
7967        assert!(
7968            !parsed.available_flags.contains_key("-f"),
7969            "purely-local -f must not be promoted onto a global",
7970        );
7971        assert!(
7972            !parsed.available_flags.contains_key("--force"),
7973            "purely-local --force must not be promoted onto a global",
7974        );
7975
7976        // A real, valid choice still parses through the merged short prefix.
7977        let parsed =
7978            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
7979        assert_eq!(parsed.args.len(), 1);
7980        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7981
7982        // And genuinely invalid choices are still rejected.
7983        assert_parse_err(
7984            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
7985            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7986        );
7987    }
7988
7989    #[test]
7990    fn test_orphan_short_does_not_clobber_unrelated_global() {
7991        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
7992        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
7993        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
7994        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
7995        let run_cmd = SpecCommand::builder()
7996            .name("run")
7997            .flag(
7998                SpecFlag::builder()
7999                    .name("raw")
8000                    .short('r')
8001                    .long("raw")
8002                    .global(false)
8003                    .build(),
8004            )
8005            .build();
8006        let mut cmd = SpecCommand::builder()
8007            .name("test")
8008            .flag(
8009                SpecFlag::builder()
8010                    .name("raw")
8011                    .long("raw")
8012                    .global(true)
8013                    .build(),
8014            )
8015            .flag(
8016                SpecFlag::builder()
8017                    .name("restrict")
8018                    .short('r')
8019                    .long("restrict")
8020                    .global(true)
8021                    .build(),
8022            )
8023            .build();
8024        cmd.subcommands.insert("run".to_string(), run_cmd);
8025        let spec = Spec {
8026            name: "test".to_string(),
8027            bin: "test".to_string(),
8028            cmd,
8029            ..Default::default()
8030        };
8031
8032        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8033        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8034        assert_eq!(
8035            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8036            Some("restrict"),
8037            "-r must remain owned by the unrelated global it already belonged to",
8038        );
8039        // Both globals are still recognized and global after the descent.
8040        assert!(parsed
8041            .available_flags
8042            .get("--raw")
8043            .is_some_and(|f| f.global));
8044        assert!(parsed
8045            .available_flags
8046            .get("--restrict")
8047            .is_some_and(|f| f.global));
8048    }
8049
8050    #[test]
8051    fn test_redeclared_global_aliases_share_one_flag() {
8052        // A global declared with BOTH a short and a long, re-declared non-globally by a
8053        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8054        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8055        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8056        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8057        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8058        let spec = r#"
8059flag "-y --yes" global=#true effect="write"
8060cmd "run" {
8061    flag "-y --yes --assume-yes"
8062}
8063"#
8064        .parse::<Spec>()
8065        .unwrap();
8066
8067        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8068
8069        for key in ["-y", "--yes", "--assume-yes"] {
8070            let flag = parsed
8071                .available_flags
8072                .get(key)
8073                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8074            assert!(flag.global, "{key} must stay global after the descent");
8075            assert_eq!(
8076                flag.long,
8077                vec!["yes".to_string(), "assume-yes".to_string()],
8078                "{key} must resolve to the flag carrying every alias",
8079            );
8080            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8081        }
8082
8083        // One logical flag means one object: all three keys share a single `Arc`.
8084        assert_eq!(
8085            unique_flags(parsed.available_flags.values()).count(),
8086            1,
8087            "all aliases must point at one flag object, got {:?}",
8088            parsed.available_flags,
8089        );
8090
8091        // The global's effect survives the merge, so `-y` still marks the command as writing.
8092        assert_eq!(
8093            parsed.available_flags["-y"].effect,
8094            Some(crate::SpecCommandEffect::Write),
8095        );
8096    }
8097
8098    #[test]
8099    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8100        let spec = r#"
8101flag "--yes" global=#true {
8102    alias "-q" "--quietly" hide=#true
8103}
8104cmd "run" {
8105    flag "--yes --assume-yes" {
8106        alias "-s" "--secret" hide=#true
8107    }
8108}
8109"#
8110        .parse::<Spec>()
8111        .unwrap();
8112
8113        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8114        let merged = &parsed.available_flags["--yes"];
8115        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8116        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8117        for key in ["-q", "-s", "--quietly", "--secret"] {
8118            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8119        }
8120    }
8121
8122    #[test]
8123    fn test_redeclared_global_can_promote_hidden_aliases() {
8124        let spec = r#"
8125flag "--yes" global=#true {
8126    alias "-q" "--quietly" hide=#true
8127}
8128cmd "run" {
8129    flag "-q --yes --quietly"
8130}
8131"#
8132        .parse::<Spec>()
8133        .unwrap();
8134
8135        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8136        let merged = &parsed.available_flags["--yes"];
8137        assert!(merged.hidden_short_aliases.is_empty());
8138        assert!(merged.hidden_aliases.is_empty());
8139        for key in ["-q", "--quietly"] {
8140            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8141        }
8142    }
8143
8144    #[test]
8145    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8146        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8147        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8148        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8149        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8150        // pre-merge global and miss the added `assume-yes`.
8151        let spec = r#"
8152flag "-y --yes --confirm" global=#true
8153cmd "run" {
8154    flag "--yes --assume-yes"
8155}
8156"#
8157        .parse::<Spec>()
8158        .unwrap();
8159
8160        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8161
8162        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8163            let flag = parsed
8164                .available_flags
8165                .get(key)
8166                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8167            assert!(flag.global, "{key} must stay global after the descent");
8168            assert_eq!(
8169                flag.long,
8170                vec![
8171                    "yes".to_string(),
8172                    "confirm".to_string(),
8173                    "assume-yes".to_string()
8174                ],
8175                "{key} must resolve to the flag carrying every alias",
8176            );
8177        }
8178
8179        assert_eq!(
8180            unique_flags(parsed.available_flags.values()).count(),
8181            1,
8182            "all aliases must point at one flag object, got {:?}",
8183            parsed.available_flags,
8184        );
8185    }
8186
8187    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8188    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8189    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8190    ///
8191    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8192    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8193    fn mounted_task_flag_spec() -> Spec {
8194        let mut task_cmd = SpecCommand::builder()
8195            .name("mytask")
8196            .flag(
8197                SpecFlag::builder()
8198                    .name("env")
8199                    .long("env")
8200                    .arg(
8201                        SpecArg::builder()
8202                            .name("name")
8203                            .choices(["dev", "stage", "prod"])
8204                            .build(),
8205                    )
8206                    .global(false)
8207                    .build(),
8208            )
8209            .flag(
8210                SpecFlag::builder()
8211                    .name("bump")
8212                    .long("bump")
8213                    .arg(
8214                        SpecArg::builder()
8215                            .name("type")
8216                            .choices(["auto", "major"])
8217                            .build(),
8218                    )
8219                    .global(false)
8220                    .build(),
8221            )
8222            .build();
8223        task_cmd.mounted = true;
8224
8225        let mut run_cmd = SpecCommand::builder()
8226            .name("run")
8227            .flag(
8228                SpecFlag::builder()
8229                    .name("force")
8230                    .short('f')
8231                    .long("force")
8232                    .global(false)
8233                    .build(),
8234            )
8235            .build();
8236        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8237
8238        let mut cmd = SpecCommand::builder()
8239            .name("test")
8240            .flag(
8241                SpecFlag::builder()
8242                    .name("env")
8243                    .short('E')
8244                    .long("env")
8245                    .arg(SpecArg::builder().name("ENV").build())
8246                    .global(true)
8247                    .build(),
8248            )
8249            .flag(
8250                SpecFlag::builder()
8251                    .name("silent")
8252                    .long("silent")
8253                    .global(true)
8254                    .build(),
8255            )
8256            .build();
8257        cmd.subcommands.insert("run".to_string(), run_cmd);
8258
8259        Spec {
8260            name: "test".to_string(),
8261            bin: "test".to_string(),
8262            cmd,
8263            ..Default::default()
8264        }
8265    }
8266
8267    #[test]
8268    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8269        // The mounted program's own commands are ordinary commands relative to each other, so
8270        // descending *within* the mounted tree must follow the normal rules — including keeping
8271        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8272        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8273        // global, which the next descent's `retain(global)` then dropped entirely.
8274        let deep = SpecCommand::builder().name("deep").build();
8275        let mut sub = SpecCommand::builder()
8276            .name("sub")
8277            // Re-declares the mounted program's own global as non-global.
8278            .flag(
8279                SpecFlag::builder()
8280                    .name("cd")
8281                    .short('C')
8282                    .long("cd")
8283                    .arg(SpecArg::builder().name("dir").build())
8284                    .global(false)
8285                    .build(),
8286            )
8287            .build();
8288        sub.subcommands.insert("deep".to_string(), deep);
8289        let mut task = SpecCommand::builder()
8290            .name("task")
8291            .flag(
8292                SpecFlag::builder()
8293                    .name("cd")
8294                    .short('C')
8295                    .long("cd")
8296                    .arg(SpecArg::builder().name("dir").build())
8297                    .global(true)
8298                    .build(),
8299            )
8300            .build();
8301        task.subcommands.insert("sub".to_string(), sub);
8302        task.mark_mounted();
8303
8304        let mut run_cmd = SpecCommand::builder().name("run").build();
8305        run_cmd.subcommands.insert("task".to_string(), task);
8306        let mut cmd = SpecCommand::builder().name("test").build();
8307        cmd.subcommands.insert("run".to_string(), run_cmd);
8308        let spec = Spec {
8309            name: "test".to_string(),
8310            bin: "test".to_string(),
8311            cmd,
8312            ..Default::default()
8313        };
8314
8315        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8316        assert!(
8317            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8318            "the mounted program's own global must survive descents inside the mounted tree",
8319        );
8320        assert!(
8321            parsed.completion_flags().contains_key("--cd"),
8322            "and must still be offered there: it belongs to the mounted program",
8323        );
8324        assert!(
8325            parsed.completion_flags().contains_key("-C"),
8326            "including the short the nested command re-declared",
8327        );
8328    }
8329
8330    #[test]
8331    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8332        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
8333        // into the command the mount sits on. They belong to the mounted program, so they must
8334        // be offered inside the mounted commands rather than filtered out with the mounting
8335        // CLI's own flags.
8336        let mut task = SpecCommand::builder()
8337            .name("task")
8338            .flag(
8339                SpecFlag::builder()
8340                    .name("bump")
8341                    .long("bump")
8342                    .global(false)
8343                    .build(),
8344            )
8345            .build();
8346        task.mark_mounted();
8347
8348        let mut run_cmd = SpecCommand::builder().name("run").build();
8349        run_cmd.subcommands.insert("task".to_string(), task);
8350        // What `mount()` leaves behind when the mounted spec's root declares flags.
8351        run_cmd.flags = vec![
8352            SpecFlag::builder()
8353                .name("tglobal")
8354                .long("tglobal")
8355                .global(true)
8356                .build(),
8357            SpecFlag::builder()
8358                .name("tlocal")
8359                .long("tlocal")
8360                .global(false)
8361                .build(),
8362        ];
8363        run_cmd.flags_from_mount = true;
8364
8365        let mut cmd = SpecCommand::builder()
8366            .name("test")
8367            .flag(
8368                SpecFlag::builder()
8369                    .name("silent")
8370                    .long("silent")
8371                    .global(true)
8372                    .build(),
8373            )
8374            .build();
8375        cmd.subcommands.insert("run".to_string(), run_cmd);
8376        let spec = Spec {
8377            name: "test".to_string(),
8378            bin: "test".to_string(),
8379            cmd,
8380            ..Default::default()
8381        };
8382
8383        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8384        assert_eq!(
8385            parsed.completion_flags().keys().collect::<Vec<_>>(),
8386            vec!["--bump", "--tglobal"],
8387            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8388             `--silent` does not, and the mount's non-global root flag is not inherited",
8389        );
8390    }
8391
8392    #[test]
8393    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8394        // Regression for jdx/mise#11282. A mounted command describes another program, which
8395        // does not accept the mounting CLI's globals (mise forwards everything after a task
8396        // name to the task). They must stay recognized — they may appear before the mounted
8397        // command — but must not be offered in completions there.
8398        let spec = mounted_task_flag_spec();
8399        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8400
8401        // Still recognized for parsing...
8402        assert!(parsed.available_flags.contains_key("--silent"));
8403        assert!(parsed.available_flags.contains_key("-E"));
8404        // ...but belonging to a command above the mount, so not offered.
8405        assert_eq!(
8406            parsed.completion_flags().keys().collect::<Vec<_>>(),
8407            vec!["--bump", "--env"],
8408            "only the mounted command's own flags may be offered",
8409        );
8410
8411        // `run`'s own non-global flag is dropped on descent, as it always was.
8412        assert!(!parsed.available_flags.contains_key("--force"));
8413    }
8414
8415    #[test]
8416    fn test_mounted_cmd_flag_wins_over_inherited_global() {
8417        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
8418        // by the root's `--env` global, so completing its value fell back to file completion.
8419        let spec = mounted_task_flag_spec();
8420        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8421
8422        let awaiting = parsed
8423            .flag_awaiting_value
8424            .first()
8425            .expect("--env should await a value");
8426        assert_eq!(
8427            awaiting
8428                .arg
8429                .as_ref()
8430                .and_then(|a| a.choices.as_ref())
8431                .map(|c| c.choices.clone()),
8432            Some(vec![
8433                "dev".to_string(),
8434                "stage".to_string(),
8435                "prod".to_string()
8436            ]),
8437            "the mounted command's own --env must win over the inherited global",
8438        );
8439
8440        // The global's short is not declared by the mounted command, so it keeps pointing at
8441        // the global and a value passed before the mounted command still parses.
8442        let parsed =
8443            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8444        assert!(
8445            parsed.args.is_empty(),
8446            "prefix global tokens must not be consumed as positionals, got {:?}",
8447            parsed.args
8448        );
8449        assert_eq!(
8450            parsed.as_env().get("usage_env").map(String::as_str),
8451            Some("anything"),
8452        );
8453    }
8454
8455    #[test]
8456    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
8457        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
8458        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
8459        // global's value would be validated against the mounted flag's choices and a legitimate
8460        // value would be rejected.
8461        let spec = mounted_task_flag_spec();
8462        let parsed = parse_partial(
8463            &spec,
8464            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
8465        )
8466        .unwrap();
8467        assert!(
8468            parsed.errors.is_empty(),
8469            "prefix global value must not be validated against the mounted flag: {:?}",
8470            parsed
8471                .errors
8472                .iter()
8473                .map(|e| e.to_string())
8474                .collect::<Vec<_>>(),
8475        );
8476        assert_eq!(
8477            parsed.as_env().get("usage_env").map(String::as_str),
8478            Some("not-a-task-choice"),
8479        );
8480
8481        // The embedded-value form binds the same way.
8482        let parsed = parse_partial(
8483            &spec,
8484            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
8485        )
8486        .unwrap();
8487        assert!(parsed.errors.is_empty());
8488        assert_eq!(
8489            parsed.as_env().get("usage_env").map(String::as_str),
8490            Some("not-a-task-choice"),
8491        );
8492
8493        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
8494        // the same name was already used before it.
8495        let parsed = parse_partial(
8496            &spec,
8497            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
8498        )
8499        .unwrap();
8500        let awaiting = parsed
8501            .flag_awaiting_value
8502            .first()
8503            .expect("--env should await a value");
8504        assert_eq!(
8505            awaiting
8506                .arg
8507                .as_ref()
8508                .and_then(|a| a.choices.as_ref())
8509                .map(|c| c.choices.clone()),
8510            Some(vec![
8511                "dev".to_string(),
8512                "stage".to_string(),
8513                "prod".to_string()
8514            ]),
8515            "the mounted command's --env must own the name after the mounted command",
8516        );
8517    }
8518
8519    #[test]
8520    fn test_non_global_flag_does_not_hide_subcommand() {
8521        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
8522        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
8523        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
8524        let spec = mounted_task_flag_spec();
8525
8526        for words in [
8527            // `run` declares `-f/--force` as non-global.
8528            &["test", "run", "--force", "mytask"][..],
8529            &["test", "run", "-f", "mytask"][..],
8530            // Mixed with a global before the subcommand.
8531            &["test", "-E", "prod", "run", "--force", "mytask"][..],
8532        ] {
8533            let parsed = parse_partial(&spec, &input(words)).unwrap();
8534            assert_eq!(
8535                parsed
8536                    .cmds
8537                    .iter()
8538                    .map(|c| c.name.as_str())
8539                    .collect::<Vec<_>>(),
8540                vec!["test", "run", "mytask"],
8541                "{words:?} should descend into the mounted command",
8542            );
8543            assert!(
8544                parsed.args.is_empty(),
8545                "{words:?} should not consume a positional, got {:?}",
8546                parsed.args,
8547            );
8548            assert_eq!(
8549                parsed.as_env().get("usage_force").map(String::as_str),
8550                Some("true"),
8551                "the non-global flag must still be recorded for {words:?}",
8552            );
8553        }
8554
8555        // A non-global flag that takes a value consumes it, rather than reading the value as the
8556        // subcommand.
8557        let mut run_cmd = SpecCommand::builder()
8558            .name("run")
8559            .flag(
8560                SpecFlag::builder()
8561                    .name("output")
8562                    .short('o')
8563                    .long("output")
8564                    .arg(SpecArg::builder().name("mode").build())
8565                    .global(false)
8566                    .build(),
8567            )
8568            .build();
8569        run_cmd.subcommands.insert(
8570            "task".to_string(),
8571            SpecCommand::builder().name("task").build(),
8572        );
8573        let mut cmd = SpecCommand::builder().name("test").build();
8574        cmd.subcommands.insert("run".to_string(), run_cmd);
8575        let spec = Spec {
8576            name: "test".to_string(),
8577            bin: "test".to_string(),
8578            cmd,
8579            ..Default::default()
8580        };
8581
8582        let parsed =
8583            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8584        assert_eq!(
8585            parsed
8586                .cmds
8587                .iter()
8588                .map(|c| c.name.as_str())
8589                .collect::<Vec<_>>(),
8590            vec!["test", "run", "task"],
8591        );
8592        assert_eq!(
8593            parsed.as_env().get("usage_output").map(String::as_str),
8594            Some("quiet"),
8595        );
8596
8597        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
8598        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
8599        assert_parse_err(
8600            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8601            "unexpected word: --nope",
8602        );
8603    }
8604
8605    #[test]
8606    fn test_non_mounted_subcommand_offers_inherited_globals() {
8607        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
8608        // still both recognized and offered.
8609        let mut run_cmd = SpecCommand::builder().name("run").build();
8610        run_cmd.subcommands.insert(
8611            "nested".to_string(),
8612            SpecCommand::builder().name("nested").build(),
8613        );
8614        let mut cmd = SpecCommand::builder()
8615            .name("test")
8616            .flag(
8617                SpecFlag::builder()
8618                    .name("silent")
8619                    .long("silent")
8620                    .global(true)
8621                    .build(),
8622            )
8623            .build();
8624        cmd.subcommands.insert("run".to_string(), run_cmd);
8625        let spec = Spec {
8626            name: "test".to_string(),
8627            bin: "test".to_string(),
8628            cmd,
8629            ..Default::default()
8630        };
8631
8632        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8633        assert_eq!(
8634            parsed.completion_flags().keys().collect::<Vec<_>>(),
8635            parsed.available_flags.keys().collect::<Vec<_>>(),
8636        );
8637        assert!(parsed.completion_flags().contains_key("--silent"));
8638    }
8639
8640    #[test]
8641    fn test_subcommand_alias_collision_keeps_last_owner() {
8642        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
8643        // share an alias are resolved. Historically the flattened flag map gave the shared
8644        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
8645        let run_cmd = SpecCommand::builder()
8646            .name("run")
8647            .flag(
8648                SpecFlag::builder()
8649                    .name("alpha")
8650                    .short('x')
8651                    .long("alpha")
8652                    .global(false)
8653                    .build(),
8654            )
8655            .flag(
8656                SpecFlag::builder()
8657                    .name("beta")
8658                    .short('x')
8659                    .long("beta")
8660                    .global(false)
8661                    .build(),
8662            )
8663            .build();
8664        let mut cmd = SpecCommand::builder().name("test").build();
8665        cmd.subcommands.insert("run".to_string(), run_cmd);
8666        let spec = Spec {
8667            name: "test".to_string(),
8668            bin: "test".to_string(),
8669            cmd,
8670            ..Default::default()
8671        };
8672
8673        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8674        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
8675        assert_eq!(
8676            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8677            Some("beta"),
8678            "the last-declared flag must keep a shared short alias",
8679        );
8680        // Both distinct long aliases remain recognized and point to their own flag.
8681        assert_eq!(
8682            parsed
8683                .available_flags
8684                .get("--alpha")
8685                .map(|f| f.name.as_str()),
8686            Some("alpha"),
8687        );
8688        assert_eq!(
8689            parsed
8690                .available_flags
8691                .get("--beta")
8692                .map(|f| f.name.as_str()),
8693            Some("beta"),
8694        );
8695    }
8696
8697    #[test]
8698    fn test_default_subcommand_same_name_child() {
8699        // Test that default_subcommand doesn't cause issues when the default subcommand
8700        // has a child with the same name (e.g., "run" has a task named "run").
8701        // This verifies we don't switch multiple times or get stuck in a loop.
8702        let run_task = SpecCommand::builder()
8703            .name("run")
8704            .arg(SpecArg::builder().name("args").build())
8705            .build();
8706        let mut run_cmd = SpecCommand::builder().name("run").build();
8707        run_cmd.subcommands.insert("run".to_string(), run_task);
8708
8709        let mut cmd = SpecCommand::builder().name("test").build();
8710        cmd.subcommands.insert("run".to_string(), run_cmd);
8711
8712        let spec = Spec {
8713            name: "test".to_string(),
8714            bin: "test".to_string(),
8715            cmd,
8716            default_subcommand: Some("run".to_string()),
8717            ..Default::default()
8718        };
8719
8720        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
8721        let input = vec!["test".to_string(), "run".to_string()];
8722        let parsed = parse(&spec, &input).unwrap();
8723
8724        // Should have two commands: root and "run"
8725        assert_eq!(parsed.cmds.len(), 2);
8726        assert_eq!(parsed.cmds[0].name, "test");
8727        assert_eq!(parsed.cmds[1].name, "run");
8728
8729        // "test run run" should descend into the "run" task (child of "run" subcommand)
8730        let input = vec![
8731            "test".to_string(),
8732            "run".to_string(),
8733            "run".to_string(),
8734            "hello".to_string(),
8735        ];
8736        let parsed = parse(&spec, &input).unwrap();
8737
8738        assert_eq!(parsed.cmds.len(), 3);
8739        assert_eq!(parsed.cmds[0].name, "test");
8740        assert_eq!(parsed.cmds[1].name, "run");
8741        assert_eq!(parsed.cmds[2].name, "run");
8742        assert_eq!(parsed.args.len(), 1);
8743        let value = parsed.args.values().next().unwrap();
8744        assert_eq!(value.to_string(), "hello");
8745
8746        // Key test case: "test other" should switch to default subcommand "run"
8747        // and treat "other" as a positional arg (not try to switch again because
8748        // "run" also has a "run" child).
8749        let mut run_cmd = SpecCommand::builder()
8750            .name("run")
8751            .arg(SpecArg::builder().name("task").build())
8752            .build();
8753        let run_task = SpecCommand::builder().name("run").build();
8754        run_cmd.subcommands.insert("run".to_string(), run_task);
8755
8756        let mut cmd = SpecCommand::builder().name("test").build();
8757        cmd.subcommands.insert("run".to_string(), run_cmd);
8758
8759        let spec = Spec {
8760            name: "test".to_string(),
8761            bin: "test".to_string(),
8762            cmd,
8763            default_subcommand: Some("run".to_string()),
8764            ..Default::default()
8765        };
8766
8767        let input = vec!["test".to_string(), "other".to_string()];
8768        let parsed = parse(&spec, &input).unwrap();
8769
8770        // Should have two commands: root and "run" (the default)
8771        // We should NOT have switched again to the "run" task child
8772        assert_eq!(parsed.cmds.len(), 2);
8773        assert_eq!(parsed.cmds[0].name, "test");
8774        assert_eq!(parsed.cmds[1].name, "run");
8775
8776        // "other" should be parsed as a positional arg
8777        assert_eq!(parsed.args.len(), 1);
8778        let value = parsed.args.values().next().unwrap();
8779        assert_eq!(value.to_string(), "other");
8780    }
8781
8782    #[test]
8783    fn test_restart_token() {
8784        // Test that restart_token resets argument parsing
8785        let run_cmd = SpecCommand::builder()
8786            .name("run")
8787            .arg(SpecArg::builder().name("task").build())
8788            .restart_token(":::".to_string())
8789            .build();
8790        let mut cmd = SpecCommand::builder().name("test").build();
8791        cmd.subcommands.insert("run".to_string(), run_cmd);
8792
8793        let spec = Spec {
8794            name: "test".to_string(),
8795            bin: "test".to_string(),
8796            cmd,
8797            ..Default::default()
8798        };
8799
8800        // "test run task1 ::: task2" - should end up with task2 as the arg
8801        let input = vec![
8802            "test".to_string(),
8803            "run".to_string(),
8804            "task1".to_string(),
8805            ":::".to_string(),
8806            "task2".to_string(),
8807        ];
8808        let parsed = parse(&spec, &input).unwrap();
8809
8810        // After restart, args were cleared and task2 was parsed
8811        assert_eq!(parsed.args.len(), 1);
8812        let value = parsed.args.values().next().unwrap();
8813        assert_eq!(value.to_string(), "task2");
8814    }
8815
8816    #[test]
8817    fn test_restart_token_multiple() {
8818        // Test multiple restart tokens
8819        let run_cmd = SpecCommand::builder()
8820            .name("run")
8821            .arg(SpecArg::builder().name("task").build())
8822            .restart_token(":::".to_string())
8823            .build();
8824        let mut cmd = SpecCommand::builder().name("test").build();
8825        cmd.subcommands.insert("run".to_string(), run_cmd);
8826
8827        let spec = Spec {
8828            name: "test".to_string(),
8829            bin: "test".to_string(),
8830            cmd,
8831            ..Default::default()
8832        };
8833
8834        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
8835        let input = vec![
8836            "test".to_string(),
8837            "run".to_string(),
8838            "task1".to_string(),
8839            ":::".to_string(),
8840            "task2".to_string(),
8841            ":::".to_string(),
8842            "task3".to_string(),
8843        ];
8844        let parsed = parse(&spec, &input).unwrap();
8845
8846        // After multiple restarts, args were cleared and task3 was parsed
8847        assert_eq!(parsed.args.len(), 1);
8848        let value = parsed.args.values().next().unwrap();
8849        assert_eq!(value.to_string(), "task3");
8850    }
8851
8852    #[test]
8853    fn test_restart_token_clears_flag_awaiting_value() {
8854        // Test that restart_token clears pending flag values
8855        let run_cmd = SpecCommand::builder()
8856            .name("run")
8857            .arg(SpecArg::builder().name("task").build())
8858            .flag(
8859                SpecFlag::builder()
8860                    .name("jobs")
8861                    .long("jobs")
8862                    .arg(SpecArg::builder().name("count").build())
8863                    .build(),
8864            )
8865            .restart_token(":::".to_string())
8866            .build();
8867        let mut cmd = SpecCommand::builder().name("test").build();
8868        cmd.subcommands.insert("run".to_string(), run_cmd);
8869
8870        let spec = Spec {
8871            name: "test".to_string(),
8872            bin: "test".to_string(),
8873            cmd,
8874            ..Default::default()
8875        };
8876
8877        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
8878        let input = vec![
8879            "test".to_string(),
8880            "run".to_string(),
8881            "task1".to_string(),
8882            "--jobs".to_string(),
8883            ":::".to_string(),
8884            "task2".to_string(),
8885        ];
8886        let parsed = parse(&spec, &input).unwrap();
8887
8888        // task2 should be parsed as the task arg, not as --jobs value
8889        assert_eq!(parsed.args.len(), 1);
8890        let value = parsed.args.values().next().unwrap();
8891        assert_eq!(value.to_string(), "task2");
8892        // --jobs should not have a value
8893        assert!(parsed.flag_awaiting_value.is_empty());
8894    }
8895
8896    #[test]
8897    fn test_restart_token_resets_double_dash() {
8898        // Test that restart_token resets the -- separator effect
8899        let run_cmd = SpecCommand::builder()
8900            .name("run")
8901            .arg(SpecArg::builder().name("task").build())
8902            .arg(SpecArg::builder().name("extra_args").var(true).build())
8903            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
8904            .restart_token(":::".to_string())
8905            .build();
8906        let mut cmd = SpecCommand::builder().name("test").build();
8907        cmd.subcommands.insert("run".to_string(), run_cmd);
8908
8909        let spec = Spec {
8910            name: "test".to_string(),
8911            bin: "test".to_string(),
8912            cmd,
8913            ..Default::default()
8914        };
8915
8916        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
8917        let input = vec![
8918            "test".to_string(),
8919            "run".to_string(),
8920            "task1".to_string(),
8921            "--".to_string(),
8922            "extra".to_string(),
8923            ":::".to_string(),
8924            "--verbose".to_string(),
8925            "task2".to_string(),
8926        ];
8927        let parsed = parse(&spec, &input).unwrap();
8928
8929        // --verbose should be parsed as a flag (not an arg) after the restart
8930        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
8931        // task2 should be the arg after restart
8932        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8933        let value = parsed.args.get(task_arg).unwrap();
8934        assert_eq!(value.to_string(), "task2");
8935    }
8936
8937    #[test]
8938    fn test_double_dashes_without_preserve() {
8939        // Only the first `--` is a separator; a later one is a value, because flag
8940        // parsing has already stopped and there is nothing left for it to do.
8941        // `preserve` is about the *first* one — see the test below, where none is
8942        // consumed at all.
8943        let run_cmd = SpecCommand::builder()
8944            .name("run")
8945            .arg(SpecArg::builder().name("args").var(true).build())
8946            .build();
8947        let mut cmd = SpecCommand::builder().name("test").build();
8948        cmd.subcommands.insert("run".to_string(), run_cmd);
8949
8950        let spec = Spec {
8951            name: "test".to_string(),
8952            bin: "test".to_string(),
8953            cmd,
8954            ..Default::default()
8955        };
8956
8957        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
8958        let input = vec![
8959            "test".to_string(),
8960            "run".to_string(),
8961            "arg1".to_string(),
8962            "--".to_string(),
8963            "arg2".to_string(),
8964            "--".to_string(),
8965            "arg3".to_string(),
8966        ];
8967        let parsed = parse(&spec, &input).unwrap();
8968
8969        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8970        let value = parsed.args.get(args_arg).unwrap();
8971        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
8972    }
8973
8974    #[test]
8975    fn test_double_dashes_with_preserve() {
8976        // Test that variadic args WITH `preserve` keep all double dashes
8977        let run_cmd = SpecCommand::builder()
8978            .name("run")
8979            .arg(
8980                SpecArg::builder()
8981                    .name("args")
8982                    .var(true)
8983                    .double_dash(SpecDoubleDashChoices::Preserve)
8984                    .build(),
8985            )
8986            .build();
8987        let mut cmd = SpecCommand::builder().name("test").build();
8988        cmd.subcommands.insert("run".to_string(), run_cmd);
8989
8990        let spec = Spec {
8991            name: "test".to_string(),
8992            bin: "test".to_string(),
8993            cmd,
8994            ..Default::default()
8995        };
8996
8997        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
8998        let input = vec![
8999            "test".to_string(),
9000            "run".to_string(),
9001            "arg1".to_string(),
9002            "--".to_string(),
9003            "arg2".to_string(),
9004            "--".to_string(),
9005            "arg3".to_string(),
9006        ];
9007        let parsed = parse(&spec, &input).unwrap();
9008
9009        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9010        let value = parsed.args.get(args_arg).unwrap();
9011        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9012    }
9013
9014    #[test]
9015    fn test_double_dashes_with_preserve_only_dashes() {
9016        // Test that variadic args WITH `preserve` keep all double dashes even
9017        // if the values are just double dashes
9018        let run_cmd = SpecCommand::builder()
9019            .name("run")
9020            .arg(
9021                SpecArg::builder()
9022                    .name("args")
9023                    .var(true)
9024                    .double_dash(SpecDoubleDashChoices::Preserve)
9025                    .build(),
9026            )
9027            .build();
9028        let mut cmd = SpecCommand::builder().name("test").build();
9029        cmd.subcommands.insert("run".to_string(), run_cmd);
9030
9031        let spec = Spec {
9032            name: "test".to_string(),
9033            bin: "test".to_string(),
9034            cmd,
9035            ..Default::default()
9036        };
9037
9038        // "test run -- --" - all double dashes should be preserved
9039        let input = vec![
9040            "test".to_string(),
9041            "run".to_string(),
9042            "--".to_string(),
9043            "--".to_string(),
9044        ];
9045        let parsed = parse(&spec, &input).unwrap();
9046
9047        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9048        let value = parsed.args.get(args_arg).unwrap();
9049        assert_eq!(value.to_string(), "-- --");
9050    }
9051
9052    #[test]
9053    fn test_double_dashes_with_preserve_multiple_args() {
9054        // Test with multiple args where only the second has has `preserve`
9055        let run_cmd = SpecCommand::builder()
9056            .name("run")
9057            .arg(SpecArg::builder().name("task").build())
9058            .arg(
9059                SpecArg::builder()
9060                    .name("extra_args")
9061                    .var(true)
9062                    .double_dash(SpecDoubleDashChoices::Preserve)
9063                    .build(),
9064            )
9065            .build();
9066        let mut cmd = SpecCommand::builder().name("test").build();
9067        cmd.subcommands.insert("run".to_string(), run_cmd);
9068
9069        let spec = Spec {
9070            name: "test".to_string(),
9071            bin: "test".to_string(),
9072            cmd,
9073            ..Default::default()
9074        };
9075
9076        // The first arg "task1" is captured normally
9077        // Then extra_args with `preserve` captures everything, including the "--" tokens
9078        let input = vec![
9079            "test".to_string(),
9080            "run".to_string(),
9081            "task1".to_string(),
9082            "--".to_string(),
9083            "arg1".to_string(),
9084            "--".to_string(),
9085            "--foo".to_string(),
9086        ];
9087        let parsed = parse(&spec, &input).unwrap();
9088
9089        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9090        let task_value = parsed.args.get(task_arg).unwrap();
9091        assert_eq!(task_value.to_string(), "task1");
9092
9093        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9094        let extra_value = parsed.args.get(extra_arg).unwrap();
9095        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9096    }
9097
9098    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9099        let cmd = SpecCommand::builder().name("test").args(args).build();
9100        Spec {
9101            name: "test".to_string(),
9102            bin: "test".to_string(),
9103            cmd,
9104            ..Default::default()
9105        }
9106    }
9107
9108    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9109        let arg = parsed
9110            .args
9111            .keys()
9112            .find(|a| a.name == name)
9113            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9114        parsed.args.get(arg).unwrap().to_string()
9115    }
9116
9117    fn required_arg(name: &str) -> SpecArg {
9118        SpecArg::builder()
9119            .name(name)
9120            .var(true)
9121            .required(false)
9122            .double_dash(SpecDoubleDashChoices::Required)
9123            .build()
9124    }
9125
9126    #[test]
9127    fn test_double_dash_required_reports_error_once_for_variadic() {
9128        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9129        let spec = spec_with_args([required_arg("files")]);
9130
9131        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9132
9133        assert!(parsed.args.is_empty());
9134        assert_eq!(parsed.errors.len(), 1);
9135        assert!(
9136            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9137        );
9138    }
9139
9140    #[test]
9141    fn test_double_dash_required_suppresses_missing_arg() {
9142        // The arg is never filled, so the end-of-parse check would also call it missing.
9143        let spec = spec_with_args([SpecArg::builder()
9144            .name("file")
9145            .required(true)
9146            .double_dash(SpecDoubleDashChoices::Required)
9147            .build()]);
9148
9149        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9150
9151        assert_eq!(parsed.errors.len(), 1);
9152        assert!(matches!(
9153            &parsed.errors[0],
9154            UsageErr::ArgRequiresDoubleDash(_)
9155        ));
9156        // The cursor stays put, so a completion keeps offering the same arg.
9157        assert_eq!(
9158            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9159            Some("file")
9160        );
9161        assert!(!parsed.double_dash_seen);
9162    }
9163
9164    #[test]
9165    fn test_double_dash_routes_to_required_arg() {
9166        // Everything after `--` belongs to the arg that requires it, even though the greedy
9167        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9168        let spec = spec_with_args([
9169            SpecArg::builder()
9170                .name("tool")
9171                .var(true)
9172                .required(false)
9173                .build(),
9174            required_arg("command"),
9175        ]);
9176
9177        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9178
9179        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9180        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9181        assert!(parsed.double_dash_seen);
9182    }
9183
9184    #[test]
9185    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9186        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9187        // "is it filled?" check cannot be a count of how many args were filled.
9188        let spec = spec_with_args([
9189            SpecArg::builder()
9190                .name("tool")
9191                .var(true)
9192                .required(true)
9193                .build(),
9194            required_arg("command"),
9195        ]);
9196
9197        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9198
9199        assert_eq!(arg_value(&parsed, "command"), "ls");
9200        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9201        assert!(parsed
9202            .errors
9203            .iter()
9204            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
9205    }
9206
9207    #[test]
9208    fn test_double_dash_gap_applies_defaults() {
9209        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9210        let spec = spec_with_args([
9211            SpecArg::builder()
9212                .name("tool")
9213                .var(true)
9214                .required(false)
9215                .default_value("node@20")
9216                .build(),
9217            required_arg("command"),
9218        ]);
9219
9220        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9221
9222        assert_eq!(arg_value(&parsed, "command"), "ls");
9223        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9224    }
9225
9226    fn spec_with_restart_token_and_required_arg() -> Spec {
9227        let run_cmd = SpecCommand::builder()
9228            .name("run")
9229            .arg(SpecArg::builder().name("task").build())
9230            .arg(required_arg("run_args"))
9231            .restart_token(":::".to_string())
9232            .build();
9233        let mut cmd = SpecCommand::builder().name("test").build();
9234        cmd.subcommands.insert("run".to_string(), run_cmd);
9235        Spec {
9236            name: "test".to_string(),
9237            bin: "test".to_string(),
9238            cmd,
9239            ..Default::default()
9240        }
9241    }
9242
9243    #[test]
9244    fn test_double_dash_required_restart_token_resets_separator() {
9245        // The `--` before `:::` belongs to the previous invocation only.
9246        let spec = spec_with_restart_token_and_required_arg();
9247
9248        let parsed = parse_partial(
9249            &spec,
9250            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9251        )
9252        .unwrap();
9253
9254        assert_eq!(arg_value(&parsed, "task"), "task2");
9255        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9256        // Reported once even though the arg was violated after already succeeding once.
9257        assert_eq!(
9258            parsed
9259                .errors
9260                .iter()
9261                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9262                .count(),
9263            1
9264        );
9265    }
9266
9267    #[test]
9268    fn test_double_dash_required_restart_token_accepts_new_separator() {
9269        let spec = spec_with_restart_token_and_required_arg();
9270
9271        let parsed = parse(
9272            &spec,
9273            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9274        )
9275        .unwrap();
9276
9277        assert_eq!(arg_value(&parsed, "task"), "task2");
9278        assert_eq!(arg_value(&parsed, "run_args"), "c");
9279    }
9280
9281    #[test]
9282    fn test_double_dash_preserve_is_not_a_separator() {
9283        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9284        // arg that requires a separator. Deliberate: one token cannot be both.
9285        let spec = spec_with_args([
9286            SpecArg::builder()
9287                .name("kept")
9288                .var(true)
9289                .var_max(1)
9290                .required(false)
9291                .double_dash(SpecDoubleDashChoices::Preserve)
9292                .build(),
9293            required_arg("rest"),
9294        ]);
9295
9296        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9297
9298        assert_eq!(arg_value(&parsed, "kept"), "--");
9299        assert!(parsed.args.keys().all(|a| a.name != "rest"));
9300        assert!(!parsed.double_dash_seen);
9301        assert_eq!(parsed.errors.len(), 1);
9302    }
9303
9304    #[test]
9305    fn test_double_dash_required_does_not_bail_in_parse_partial() {
9306        // Completions parse half-typed command lines; they must still get a result.
9307        let spec = spec_with_args([required_arg("file")]);
9308
9309        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9310        assert!(parse(&spec, &input(&["test", "x"])).is_err());
9311    }
9312
9313    #[test]
9314    fn test_double_dash_without_required_arg_does_not_move_cursor() {
9315        // Specs with no `double_dash="required"` arg are untouched by the jump.
9316        let spec = spec_with_args([
9317            SpecArg::builder().name("first").required(false).build(),
9318            SpecArg::builder().name("second").required(false).build(),
9319        ]);
9320
9321        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9322
9323        assert_eq!(arg_value(&parsed, "first"), "a");
9324        assert_eq!(arg_value(&parsed, "second"), "b");
9325        assert!(parsed.next_arg.is_none());
9326    }
9327
9328    #[test]
9329    fn test_parser_with_custom_env_for_required_arg() {
9330        let spec = spec_with_arg(
9331            SpecArg::builder()
9332                .name("name")
9333                .env("NAME")
9334                .required(true)
9335                .build(),
9336        );
9337        std::env::remove_var("NAME");
9338
9339        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9340            .expect("parse should succeed with custom env");
9341        assert_eq!(parsed.args.len(), 1);
9342        assert_eq!(first_string_value(&parsed), "john");
9343    }
9344
9345    #[test]
9346    fn test_parser_with_custom_env_for_required_flag() {
9347        let spec = spec_with_flag(
9348            SpecFlag::builder()
9349                .long("name")
9350                .env("NAME")
9351                .required(true)
9352                .arg(SpecArg::builder().name("name").build())
9353                .build(),
9354        );
9355        std::env::remove_var("NAME");
9356
9357        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9358            .expect("parse should succeed with custom env");
9359        assert_eq!(parsed.flags.len(), 1);
9360        assert_eq!(first_string_value(&parsed), "jane");
9361    }
9362
9363    #[test]
9364    fn test_flag_environment_fallbacks_preserve_declaration_order() {
9365        let spec = spec_with_flag(
9366            SpecFlag::builder()
9367                .long("name")
9368                .env("NAME")
9369                .env_fallback("OLD_NAME")
9370                .env_fallback("OLDER_NAME")
9371                .deprecated_env("DEPRECATED_NAME")
9372                .arg(SpecArg::builder().name("name").build())
9373                .build(),
9374        );
9375
9376        let parsed = parse_with_env(
9377            &spec,
9378            &["test"],
9379            &[
9380                ("NAME", "canonical"),
9381                ("OLD_NAME", "fallback"),
9382                ("DEPRECATED_NAME", "deprecated"),
9383            ],
9384        )
9385        .unwrap();
9386        assert_eq!(first_string_value(&parsed), "canonical");
9387
9388        let parsed = parse_with_env(
9389            &spec,
9390            &["test"],
9391            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9392        )
9393        .unwrap();
9394        assert_eq!(first_string_value(&parsed), "old");
9395
9396        let parsed =
9397            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9398        assert_eq!(first_string_value(&parsed), "deprecated");
9399    }
9400
9401    #[test]
9402    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9403        let spec = spec_with_flag(
9404            SpecFlag::builder()
9405                .long("name")
9406                .env("NAME")
9407                .deprecated_env("DEPRECATED_NAME")
9408                .arg(SpecArg::builder().name("name").build())
9409                .build(),
9410        );
9411
9412        // The current name is not a deprecated one, and says nothing.
9413        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9414        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9415
9416        let parsed =
9417            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9418        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9419        assert_eq!(
9420            parsed.warnings[0].kind,
9421            crate::warn::WarningKind::DeprecatedEnv
9422        );
9423        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9424        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9425        // Reported, not printed, and the value still arrives.
9426        assert_eq!(first_string_value(&parsed), "deprecated");
9427    }
9428
9429    #[test]
9430    fn a_deprecated_flag_reports_only_when_it_was_used() {
9431        let spec = spec_with_flag(
9432            SpecFlag::builder()
9433                .long("output")
9434                .deprecated("use --out")
9435                .deprecated_remove_at("3.0.0")
9436                .arg(SpecArg::builder().name("output").build())
9437                .build(),
9438        );
9439
9440        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9441        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9442
9443        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9444        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9445        assert_eq!(
9446            parsed.warnings[0].kind,
9447            crate::warn::WarningKind::DeprecatedFlag
9448        );
9449        // Named the way it was typed, dashes and all.
9450        assert_eq!(parsed.warnings[0].name, "--output");
9451        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
9452        assert_eq!(
9453            parsed.warnings[0].render(),
9454            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
9455        );
9456    }
9457
9458    #[test]
9459    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
9460        let flag = SpecFlag::builder()
9461            .long("output")
9462            .deprecated("use --out")
9463            .deprecated_warn_at("9.0.0")
9464            .arg(SpecArg::builder().name("output").build())
9465            .build();
9466        let mut spec = spec_with_flag(flag);
9467        spec.version = Some("2.0.0".to_string());
9468
9469        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9470        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9471
9472        // And once the CLI is the release that was named, it speaks up.
9473        spec.version = Some("9.0.0".to_string());
9474        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9475        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9476    }
9477
9478    #[test]
9479    fn test_parser_with_custom_env_still_fails_when_missing() {
9480        let spec = spec_with_arg(
9481            SpecArg::builder()
9482                .name("name")
9483                .env("NAME")
9484                .required(true)
9485                .build(),
9486        );
9487        std::env::remove_var("NAME");
9488        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
9489    }
9490
9491    #[test]
9492    fn test_parser_does_not_treat_env_choice_value_as_help() {
9493        let spec = spec_with_arg(
9494            SpecArg::builder()
9495                .name("env")
9496                .env("CURRENT_ENV")
9497                .choices(["dev", "staging"])
9498                .required(false)
9499                .build(),
9500        );
9501
9502        assert_parse_err(
9503            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
9504            "Invalid choice for arg env: --help, expected one of dev, staging",
9505        );
9506    }
9507
9508    #[test]
9509    fn test_parser_does_not_treat_default_choice_value_as_help() {
9510        let spec = spec_with_flag(
9511            SpecFlag::builder()
9512                .long("env")
9513                .arg(
9514                    SpecArg::builder()
9515                        .name("env")
9516                        .choices(["dev", "staging"])
9517                        .build(),
9518                )
9519                .default_value("--help")
9520                .build(),
9521        );
9522
9523        assert_parse_err(
9524            parse_with_env(&spec, &["test"], &[]),
9525            "Invalid choice for option env: --help, expected one of dev, staging",
9526        );
9527    }
9528
9529    /// argv as `parse` wants it, program name included.
9530    fn words(of: &[&str]) -> Vec<String> {
9531        of.iter().map(|s| s.to_string()).collect()
9532    }
9533
9534    #[test]
9535    fn a_command_that_needs_a_subcommand_says_so() {
9536        // The spec has carried `subcommand_required` since the derive needed it, and this parser
9537        // never read it — so `mise generate`, which declares it, parsed as a complete
9538        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
9539        let spec: Spec = r#"
9540name "ex"
9541bin "ex"
9542cmd "gen" subcommand_required=#true {
9543    cmd "two" {}
9544    cmd "one" {}
9545    cmd "secret" hide=#true {}
9546    alias "g"
9547}
9548cmd "open" {
9549    cmd "sub" {}
9550}
9551"#
9552        .parse()
9553        .unwrap();
9554
9555        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9556        // Sorted, so the message does not depend on map order; hidden commands left out,
9557        // because a message telling someone to type a hidden name is worse than a vague one;
9558        // and the alias not listed beside the name it points at.
9559        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9560
9561        // Reached through its alias, and still about the command rather than the spelling.
9562        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9563        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9564
9565        // Given one: fine.
9566        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9567
9568        // And a command that has subcommands without declaring them required is untouched —
9569        // this is the half that keeps the check from being "any command with children".
9570        parse(&spec, &words(&["ex", "open"])).unwrap();
9571        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9572    }
9573
9574    #[test]
9575    fn arg_required_else_help_observes_the_selected_commands_argv() {
9576        let spec: Spec = r#"
9577name "ex"
9578bin "ex"
9579flag "--verbose" global=#true
9580cmd "run" arg_required_else_help=#true {
9581    flag "--all"
9582}
9583"#
9584        .parse()
9585        .unwrap();
9586        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9587
9588        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9589        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9590
9591        // A global before the command belongs to the ancestor. It selected `run`, but did not
9592        // give `run` an argument of its own.
9593        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9594        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9595
9596        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9597    }
9598
9599    #[test]
9600    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9601        let spec: Spec = r#"
9602name "ex"
9603bin "ex"
9604unknown_flags "error"
9605external_subcommand #true
9606cmd "install"
9607flag "-v --verbose" global=#true
9608"#
9609        .parse()
9610        .unwrap();
9611
9612        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9613        assert_eq!(
9614            parsed.external,
9615            Some(vec!["foo".into(), "--help".into(), "bar".into()])
9616        );
9617        assert!(parsed.flags.is_empty());
9618
9619        // Known subcommands still win.
9620        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9621        assert_eq!(parsed.cmd.name, "install");
9622        assert!(parsed.external.is_none());
9623
9624        // A global flag before the unmatched word still binds on the parent.
9625        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9626        assert_eq!(
9627            parsed.external,
9628            Some(vec!["foo".into(), "--verbose".into()])
9629        );
9630        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9631
9632        // An unknown flag on the parent is still an error, which is what clap does.
9633        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9634
9635        // A negative number is a value, not a flag, so it can be the unmatched word.
9636        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
9637        // token as a flag and never reach the catch-all.
9638        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9639        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9640    }
9641
9642    #[test]
9643    fn an_external_subcommand_satisfies_subcommand_required() {
9644        let mut spec: Spec = r#"
9645name "ex"
9646bin "ex"
9647external_subcommand #true
9648cmd "install"
9649"#
9650        .parse()
9651        .unwrap();
9652        spec.cmd.subcommand_required = true;
9653
9654        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9655        assert!(parse(&spec, &input(&["ex"])).is_err());
9656    }
9657
9658    #[test]
9659    fn a_default_subcommand_outranks_an_external_one() {
9660        let spec: Spec = r#"
9661name "ex"
9662bin "ex"
9663default_subcommand "run"
9664external_subcommand #true
9665cmd "run" {
9666    arg "[task]"
9667}
9668"#
9669        .parse()
9670        .unwrap();
9671
9672        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
9673        assert_eq!(parsed.cmd.name, "run");
9674        assert!(parsed.external.is_none());
9675        assert_eq!(first_string_value(&parsed), "build");
9676    }
9677
9678    #[test]
9679    fn multicall_basename_strips_a_path_and_exe() {
9680        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
9681        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
9682        assert_eq!(multicall_basename("LS.EXE"), "LS");
9683        assert_eq!(multicall_basename("busybox"), "busybox");
9684    }
9685
9686    #[test]
9687    fn a_multicall_applet_is_the_first_word() {
9688        let spec: Spec = r#"
9689name "busybox"
9690bin "busybox"
9691multicall #true
9692cmd "ls" {
9693    arg "[ARGS]" var=#true
9694}
9695cmd "cat"
9696"#
9697        .parse()
9698        .unwrap();
9699
9700        // A symlink: argv[0] is the applet.
9701        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
9702        assert_eq!(parsed.cmd.name, "ls");
9703        match parsed.args.values().next() {
9704            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
9705            other => panic!("expected ARGS to collect -l, got {other:?}"),
9706        }
9707
9708        // A dispatcher invocation still skips argv[0].
9709        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
9710        assert_eq!(parsed.cmd.name, "ls");
9711
9712        // Configured dispatcher values receive the same path and extension normalization.
9713        let mut configured = spec.clone();
9714        configured.name = "BusyBox".to_string();
9715        configured.bin = "/opt/bin/busybox.exe".to_string();
9716        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
9717        assert_eq!(parsed.cmd.name, "ls");
9718
9719        // `.exe` is stripped so Windows and Unix agree.
9720        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
9721        assert_eq!(parsed.cmd.name, "ls");
9722
9723        // Without the property, argv[0] is discarded as usual.
9724        let mut plain = spec.clone();
9725        plain.multicall = false;
9726        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
9727        assert_eq!(parsed.cmd.name, "ls");
9728    }
9729
9730    #[test]
9731    fn a_multicall_unknown_applet_can_be_external() {
9732        let spec: Spec = r#"
9733name "busybox"
9734bin "busybox"
9735multicall #true
9736unknown_flags "error"
9737external_subcommand #true
9738cmd "ls"
9739"#
9740        .parse()
9741        .unwrap();
9742
9743        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
9744        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
9745
9746        let mut closed = spec.clone();
9747        closed.cmd.external_subcommand = false;
9748        assert!(parse(&closed, &input(&["wat"])).is_err());
9749    }
9750
9751    #[cfg(feature = "unstable_choices_env")]
9752    #[test]
9753    fn test_parser_arg_choices_from_custom_env() {
9754        let spec = spec_arg_choices_env("DEPLOY_ENVS");
9755
9756        let parsed =
9757            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
9758        assert_eq!(first_string_value(&parsed), "bar");
9759
9760        assert_parse_err(
9761            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
9762            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
9763        );
9764        assert_parse_err(
9765            parse_with_env(&spec, &["test", "prod"], &[]),
9766            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
9767        );
9768    }
9769
9770    #[cfg(feature = "unstable_choices_env")]
9771    #[test]
9772    fn test_parser_validates_flag_choices_from_custom_env() {
9773        let spec = spec_flag_choices_env("DEPLOY_ENVS");
9774        let parsed = parse_with_env(
9775            &spec,
9776            &["test", "--env", "baz"],
9777            &[("DEPLOY_ENVS", "foo,bar baz")],
9778        )
9779        .unwrap();
9780        assert_eq!(first_string_value(&parsed), "baz");
9781    }
9782
9783    #[cfg(feature = "unstable_choices_env")]
9784    #[test]
9785    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
9786        let arg_env_spec = spec_with_arg(
9787            SpecArg::builder()
9788                .name("env")
9789                .env("CURRENT_ENV")
9790                .choices_env("DEPLOY_ENVS")
9791                .build(),
9792        );
9793        assert_parse_err(
9794            parse_with_env(
9795                &arg_env_spec,
9796                &["test"],
9797                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
9798            ),
9799            "Invalid choice for arg env: prod, expected one of dev, staging",
9800        );
9801
9802        let flag_default_spec = spec_with_flag(
9803            SpecFlag::builder()
9804                .long("env")
9805                .arg(
9806                    SpecArg::builder()
9807                        .name("env")
9808                        .choices_env("DEPLOY_ENVS")
9809                        .build(),
9810                )
9811                .default_value("prod")
9812                .build(),
9813        );
9814        assert_parse_err(
9815            parse_with_env(
9816                &flag_default_spec,
9817                &["test"],
9818                &[("DEPLOY_ENVS", "dev,staging")],
9819            ),
9820            "Invalid choice for option env: prod, expected one of dev, staging",
9821        );
9822    }
9823
9824    #[test]
9825    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
9826        let spec: Spec = r#"
9827            flag "-v --verbose" var=#true
9828            arg "[database]" default="myapp_dev"
9829            arg "[args...]"
9830        "#
9831        .parse()
9832        .unwrap();
9833        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9834            .into_iter()
9835            .map(String::from)
9836            .collect();
9837        let parsed = parse(&spec, &input).unwrap();
9838        let env = parsed.as_env();
9839        assert_eq!(env.get("usage_database").unwrap(), "mydb");
9840        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
9841    }
9842
9843    #[test]
9844    fn test_variadic_arg_captures_unknown_flags() {
9845        let cmd = SpecCommand::builder()
9846            .name("test")
9847            .flag(SpecFlag::builder().short('v').long("verbose").build())
9848            .arg(SpecArg::builder().name("database").required(false).build())
9849            .arg(
9850                SpecArg::builder()
9851                    .name("args")
9852                    .required(false)
9853                    .var(true)
9854                    .build(),
9855            )
9856            .build();
9857        let spec = Spec {
9858            name: "test".to_string(),
9859            bin: "test".to_string(),
9860            cmd,
9861            ..Default::default()
9862        };
9863
9864        // Unknown --host flag and its value should be captured by [args...]
9865        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9866            .into_iter()
9867            .map(String::from)
9868            .collect();
9869        let parsed = parse(&spec, &input).unwrap();
9870        assert_eq!(parsed.args.len(), 2);
9871        let args_val = parsed
9872            .args
9873            .iter()
9874            .find(|(a, _)| a.name == "args")
9875            .unwrap()
9876            .1;
9877        match args_val {
9878            ParseValue::MultiString(v) => {
9879                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9880            }
9881            _ => panic!("Expected MultiString, got {:?}", args_val),
9882        }
9883    }
9884
9885    #[test]
9886    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
9887        let cmd = SpecCommand::builder()
9888            .name("test")
9889            .flag(SpecFlag::builder().short('v').long("verbose").build())
9890            .arg(SpecArg::builder().name("database").required(false).build())
9891            .arg(
9892                SpecArg::builder()
9893                    .name("args")
9894                    .required(false)
9895                    .var(true)
9896                    .build(),
9897            )
9898            .build();
9899        let spec = Spec {
9900            name: "test".to_string(),
9901            bin: "test".to_string(),
9902            cmd,
9903            ..Default::default()
9904        };
9905
9906        // With explicit -- separator
9907        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
9908            .into_iter()
9909            .map(String::from)
9910            .collect();
9911        let parsed = parse(&spec, &input).unwrap();
9912        assert_eq!(parsed.args.len(), 2);
9913        let args_val = parsed
9914            .args
9915            .iter()
9916            .find(|(a, _)| a.name == "args")
9917            .unwrap()
9918            .1;
9919        match args_val {
9920            ParseValue::MultiString(v) => {
9921                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9922            }
9923            _ => panic!("Expected MultiString, got {:?}", args_val),
9924        }
9925    }
9926
9927    #[test]
9928    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
9929        // Regression: --flag=value should be treated as a single positional token when
9930        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
9931        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
9932
9933        // Single unknown --flag=value: must not produce a stray "3" positional.
9934        // as_env() shell-joins values, so "=" gets quoted.
9935        let input: Vec<String> = vec!["test", "--option=3"]
9936            .into_iter()
9937            .map(String::from)
9938            .collect();
9939        let parsed = parse(&spec, &input).unwrap();
9940        let env = parsed.as_env();
9941        assert_eq!(
9942            env.get("usage_other_args").map(String::as_str),
9943            Some("'--option=3'"),
9944            "expected a single --option=3 token, got {:?}",
9945            env.get("usage_other_args"),
9946        );
9947
9948        // Multiple unknown --flag=value args should each be kept intact
9949        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
9950            .into_iter()
9951            .map(String::from)
9952            .collect();
9953        let parsed2 = parse(&spec, &input2).unwrap();
9954        let env2 = parsed2.as_env();
9955        assert_eq!(
9956            env2.get("usage_other_args").map(String::as_str),
9957            Some("'--foo=bar' '--baz=qux'"),
9958            "expected two intact tokens, got {:?}",
9959            env2.get("usage_other_args"),
9960        );
9961
9962        // Mix of plain positional args and unknown --flag=value tokens
9963        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
9964            .into_iter()
9965            .map(String::from)
9966            .collect();
9967        let parsed3 = parse(&spec, &input3).unwrap();
9968        let env3 = parsed3.as_env();
9969        assert_eq!(
9970            env3.get("usage_other_args").map(String::as_str),
9971            Some("positional1 '--option=3' positional2"),
9972            "expected positional args and intact flag token, got {:?}",
9973            env3.get("usage_other_args"),
9974        );
9975    }
9976
9977    #[test]
9978    fn test_allow_hyphen_values_consumes_short_flag_collision() {
9979        let spec = r#"
9980flag "-d --working-dir <DIR>"
9981flag "-a --args <ARGS>" allow_hyphen_values=#true
9982"#
9983        .parse::<Spec>()
9984        .unwrap();
9985
9986        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9987
9988        assert_eq!(parsed.flags.len(), 1);
9989        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9990    }
9991
9992    #[test]
9993    fn test_allow_hyphen_values_consumes_embedded_long_value() {
9994        let spec = r#"
9995flag "-d --working-dir <DIR>"
9996flag "-a --args <ARGS>" allow_hyphen_values=#true
9997"#
9998        .parse::<Spec>()
9999        .unwrap();
10000
10001        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10002
10003        assert_eq!(parsed.flags.len(), 1);
10004        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10005    }
10006
10007    #[test]
10008    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10009        // The flag is declared to accept a token that looks like a flag, and `--` looks
10010        // like one, so it binds — which is what clap does with the same declaration.
10011        // Letting the separator arm run first consumed it and left the flag hungry, and
10012        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10013        let spec = r#"
10014flag "-a --args <ARGS>" allow_hyphen_values=#true
10015arg "[rest]..."
10016"#
10017        .parse::<Spec>()
10018        .unwrap();
10019
10020        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10021
10022        assert_eq!(flag_string_value(&parsed, "args"), "--");
10023        let rest = parsed
10024            .args
10025            .values()
10026            .next()
10027            .expect("expected the word after the separator to reach the argument");
10028        assert_eq!(rest.to_string(), "-x");
10029    }
10030
10031    #[test]
10032    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10033        // Which token supplied the first value says nothing about how many the argument
10034        // takes, so collection carries on from a hyphenated one exactly as from a plain
10035        // one. It still stops at the next flag-like token, which is what keeps a second
10036        // occurrence of the flag from being eaten as a value.
10037        let spec = r#"
10038flag "-a --args <ARGS>..." allow_hyphen_values=#true
10039"#
10040        .parse::<Spec>()
10041        .unwrap();
10042
10043        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10044
10045        let flag = parsed
10046            .flags
10047            .keys()
10048            .find(|flag| flag.name == "args")
10049            .expect("expected args flag");
10050        match parsed.flags.get(flag).expect("expected args value") {
10051            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10052            other => panic!("expected a list of values, got {other:?}"),
10053        }
10054    }
10055
10056    #[test]
10057    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10058        let spec = r#"
10059flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10060"#
10061        .parse::<Spec>()
10062        .unwrap();
10063
10064        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10065
10066        let flag = parsed
10067            .flags
10068            .keys()
10069            .find(|flag| flag.name == "args")
10070            .expect("expected args flag");
10071        let value = parsed.flags.get(flag).expect("expected args value");
10072        match value {
10073            ParseValue::MultiString(values) => {
10074                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10075            }
10076            _ => panic!("expected MultiString, got {value:?}"),
10077        }
10078    }
10079
10080    #[test]
10081    fn test_require_equals_accepts_attached_and_refuses_detached() {
10082        let spec = r#"
10083flag "--inspect <PORT>" require_equals=#true
10084"#
10085        .parse::<Spec>()
10086        .unwrap();
10087
10088        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10089        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10090
10091        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10092        let msg = format!("{err}");
10093        assert!(
10094            msg.contains("requires an argument") || msg.contains("inspect"),
10095            "detached value must be refused: {msg}"
10096        );
10097    }
10098
10099    #[test]
10100    fn boolean_flags_can_accept_attached_values_when_enabled() {
10101        let spec: Spec = r#"
10102name "ex"
10103bin "ex"
10104flag "--color" negate="--no-color" bool_value=#true
10105arg "[rest]"
10106"#
10107        .parse()
10108        .unwrap();
10109
10110        for (token, expected) in [
10111            ("--color", true),
10112            ("--color=true", true),
10113            ("--color=false", false),
10114            ("--no-color", false),
10115            ("--no-color=false", true),
10116        ] {
10117            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10118            assert!(
10119                matches!(
10120                    parsed.flags.get(&spec.cmd.flags[0]),
10121                    Some(ParseValue::Bool(value)) if *value == expected
10122                ),
10123                "{token}"
10124            );
10125        }
10126
10127        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10128        assert!(matches!(
10129            parsed.args.get(&spec.cmd.args[0]),
10130            Some(ParseValue::String(value)) if value == "word"
10131        ));
10132        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10133        assert!(err.to_string().contains("expected `true` or `false`"));
10134
10135        let strict: Spec = r#"
10136name "ex"
10137bin "ex"
10138args_override_self #false
10139flag "--color" negate="--no-color" bool_value=#true
10140"#
10141        .parse()
10142        .unwrap();
10143        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10144        let parsed = parse(
10145            &strict,
10146            &input(&["ex", "--color=false", "--no-color=false"]),
10147        )
10148        .unwrap();
10149        assert!(matches!(
10150            parsed.flags.get(&strict.cmd.flags[0]),
10151            Some(ParseValue::Bool(true))
10152        ));
10153    }
10154
10155    #[test]
10156    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10157        let spec = r#"
10158flag "-a --all"
10159flag "-i --inspect <PORT>" require_equals=#true
10160"#
10161        .parse::<Spec>()
10162        .unwrap();
10163
10164        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10165        let msg = format!("{err}");
10166        assert!(
10167            msg.contains("requires an argument") || msg.contains("inspect"),
10168            "bundled short must refuse the following word: {msg}"
10169        );
10170    }
10171
10172    #[test]
10173    fn test_default_missing_binds_when_the_value_is_left_off() {
10174        let spec = r#"
10175flag "-c --color <WHEN>" default_missing="always"
10176flag "-v --verbose"
10177"#
10178        .parse::<Spec>()
10179        .unwrap();
10180
10181        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10182        assert_eq!(flag_string_value(&parsed, "color"), "always");
10183
10184        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10185        assert_eq!(flag_string_value(&parsed, "color"), "never");
10186
10187        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10188        assert_eq!(flag_string_value(&parsed, "color"), "never");
10189
10190        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10191        assert_eq!(flag_string_value(&parsed, "color"), "always");
10192        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10193
10194        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10195        assert_eq!(flag_string_value(&parsed, "color"), "");
10196
10197        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10198        assert_eq!(flag_string_value(&parsed, "color"), "never");
10199
10200        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10201        assert_eq!(flag_string_value(&parsed, "color"), "always");
10202        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10203    }
10204
10205    #[test]
10206    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10207        let spec = r#"
10208flag "--apps <N>"
10209flag "--jobs <N>" default_missing="default missing"
10210flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10211"#
10212        .parse::<Spec>()
10213        .unwrap();
10214
10215        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10216        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10217
10218        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10219        assert!(
10220            err.to_string().contains("unexpected word: -1"),
10221            "default_missing must keep an unopted negative value separate: {err}"
10222        );
10223
10224        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10225        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10226
10227        let external_spec = r#"
10228external_subcommand #true
10229flag "--apps <N>"
10230"#
10231        .parse::<Spec>()
10232        .unwrap();
10233        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10234        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10235        assert!(parsed.external.is_none());
10236
10237        for words in [
10238            &["test", "--apps", "--jobs", "1"][..],
10239            &["test", "--apps", "--kids", "-1"][..],
10240        ] {
10241            let err = parse(&spec, &input(words)).unwrap_err();
10242            let message = err.to_string();
10243            assert!(
10244                message.contains("--apps") && message.contains("requires an argument"),
10245                "the earlier flag must report its missing value for {words:?}: {message}"
10246            );
10247        }
10248    }
10249
10250    #[test]
10251    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10252        let spec = r#"
10253flag "--bump [LEVEL]" value_optional=#true
10254flag "--verbose"
10255arg "[FILE]"
10256"#
10257        .parse::<Spec>()
10258        .unwrap();
10259
10260        let absent = parse(&spec, &input(&["test"])).unwrap();
10261        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10262
10263        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10264        let bump = bare
10265            .flags
10266            .iter()
10267            .find(|(flag, _)| flag.name == "bump")
10268            .map(|(_, value)| value)
10269            .unwrap();
10270        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10271        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10272        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10273
10274        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10275        assert_eq!(flag_string_value(&explicit, "bump"), "");
10276
10277        let corrected = parse(
10278            &spec,
10279            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10280        )
10281        .unwrap();
10282        let bump = corrected
10283            .flags
10284            .iter()
10285            .find(|(flag, _)| flag.name == "bump")
10286            .map(|(_, value)| value)
10287            .unwrap();
10288        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10289
10290        let collecting = r#"
10291flag "--tag [TAG]..." value_optional=#true
10292flag "--verbose"
10293"#
10294        .parse::<Spec>()
10295        .unwrap();
10296        let valued = parse(
10297            &collecting,
10298            &input(&["test", "--tag", "one", "two", "--verbose"]),
10299        )
10300        .unwrap();
10301        let tag = valued
10302            .flags
10303            .iter()
10304            .find(|(flag, _)| flag.name == "tag")
10305            .map(|(_, value)| value)
10306            .unwrap();
10307        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10308    }
10309
10310    #[test]
10311    fn test_repeatable_bare_optional_values_count_each_occurrence() {
10312        let spec = r#"
10313flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10314"#
10315        .parse::<Spec>()
10316        .unwrap();
10317
10318        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10319        let tag = parsed
10320            .flags
10321            .iter()
10322            .find(|(flag, _)| flag.name == "tag")
10323            .map(|(_, value)| value)
10324            .unwrap();
10325        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10326
10327        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10328        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10329    }
10330
10331    #[test]
10332    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10333        let spec = r#"
10334flag "--tag [TAG]..." var=#true value_optional=#true
10335flag "--verbose"
10336"#
10337        .parse::<Spec>()
10338        .unwrap();
10339
10340        for argv in [
10341            &["test", "--tag", "one", "two"][..],
10342            &["test", "--tag", "one", "two", "--verbose"][..],
10343            &["test", "--tag", "one", "--tag", "two"][..],
10344        ] {
10345            let parsed = parse(&spec, &input(argv)).unwrap();
10346            let tag = parsed
10347                .flags
10348                .iter()
10349                .find(|(flag, _)| flag.name == "tag")
10350                .map(|(_, value)| value)
10351                .unwrap();
10352            assert!(
10353                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10354                "argv={argv:?}: {tag:?}"
10355            );
10356        }
10357
10358        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10359        let tag = bare
10360            .flags
10361            .iter()
10362            .find(|(flag, _)| flag.name == "tag")
10363            .map(|(_, value)| value)
10364            .unwrap();
10365        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10366    }
10367
10368    #[test]
10369    fn test_default_missing_with_require_equals_refuses_the_following_word() {
10370        let spec = r#"
10371flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10372arg "[rest]"
10373"#
10374        .parse::<Spec>()
10375        .unwrap();
10376
10377        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10378        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10379
10380        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10381        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10382
10383        // The following word is not the value; the missing value is, and 80 is a positional.
10384        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10385        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10386        assert_eq!(
10387            parsed
10388                .args
10389                .values()
10390                .next()
10391                .map(|v| v.to_string())
10392                .as_deref(),
10393            Some("80")
10394        );
10395
10396        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10397        assert_eq!(flag_string_value(&parsed, "inspect"), "");
10398    }
10399
10400    #[test]
10401    fn test_default_missing_must_be_a_choice() {
10402        let spec = r#"
10403flag "--color <WHEN>" default_missing="always" {
10404    choices "auto" "always" "never"
10405}
10406"#
10407        .parse::<Spec>()
10408        .unwrap();
10409
10410        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10411        assert_eq!(flag_string_value(&parsed, "color"), "always");
10412
10413        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10414        assert_eq!(flag_string_value(&parsed, "color"), "never");
10415
10416        let spec = r#"
10417flag "--color <WHEN>" default_missing="wat" {
10418    choices "auto" "always" "never"
10419}
10420"#
10421        .parse::<Spec>()
10422        .unwrap();
10423
10424        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10425        let msg = format!("{err}");
10426        assert!(
10427            msg.contains("Invalid choice for option color: wat"),
10428            "missing default has to pass choices the same way a typed value does: {msg}"
10429        );
10430
10431        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
10432        let msg = format!("{err}");
10433        assert!(
10434            msg.contains("Invalid choice for option color: wat"),
10435            "an attached value that is not a choice is still refused: {msg}"
10436        );
10437
10438        let spec = r#"
10439flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
10440    choices "9229" "80"
10441}
10442arg "[rest]"
10443"#
10444        .parse::<Spec>()
10445        .unwrap();
10446
10447        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
10448        let msg = format!("{err}");
10449        assert!(
10450            msg.contains("Invalid choice for option inspect: wat"),
10451            "require_equals still binds the missing string, so the error is the choice: {msg}"
10452        );
10453    }
10454
10455    #[test]
10456    fn test_hyphen_values_still_start_short_flag_parsing() {
10457        let spec = r#"
10458flag "-d --working-dir <DIR>"
10459flag "-a --args <ARGS>"
10460"#
10461        .parse::<Spec>()
10462        .unwrap();
10463
10464        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
10465        let message = err.to_string();
10466        assert!(
10467            message.contains("--args") && message.contains("requires an argument"),
10468            "the recognized -d must leave the earlier -a missing: {message}"
10469        );
10470    }
10471
10472    /// `available_flags` has to agree with what an actual parse accepts, since
10473    /// its whole reason to exist is answering that question without one.
10474    mod available_flags {
10475        use super::*;
10476
10477        fn spec() -> Spec {
10478            r#"
10479bin "test"
10480flag "-v --verbose" global=#true
10481flag "--raw" global=#true effect="write"
10482flag "--local-only"
10483cmd "run" {
10484    flag "-r --raw"
10485    flag "-w --watch"
10486    cmd "once"
10487}
10488"#
10489            .parse::<Spec>()
10490            .unwrap()
10491        }
10492
10493        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
10494            let mut chain = vec![&spec.cmd];
10495            for segment in path {
10496                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
10497            }
10498            chain
10499        }
10500
10501        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
10502            let mut names: Vec<_> = available_flags(&chain(spec, path))
10503                .iter()
10504                .map(|f| f.name.clone())
10505                .collect();
10506            names.sort();
10507            names
10508        }
10509
10510        #[test]
10511        fn an_empty_chain_yields_nothing() {
10512            assert!(available_flags(&[]).is_empty());
10513        }
10514
10515        #[test]
10516        fn the_root_gets_its_own_flags() {
10517            let spec = spec();
10518            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
10519        }
10520
10521        #[test]
10522        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
10523            let spec = spec();
10524            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10525        }
10526
10527        #[test]
10528        fn a_re_declared_global_is_listed_once() {
10529            // The merge can leave the long key on the merged flag and the short
10530            // key on the pre-merge one. Same flag; it must not be listed twice.
10531            let spec = r#"
10532bin "test"
10533flag "-y --yes" global=#true effect="write"
10534cmd "rm" {
10535    flag "-y --yes"
10536}
10537"#
10538            .parse::<Spec>()
10539            .unwrap();
10540            let flags = available_flags(&chain(&spec, &["rm"]));
10541            assert_eq!(flags.len(), 1, "{flags:?}");
10542            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10543        }
10544
10545        #[test]
10546        fn a_re_declared_global_keeps_the_globals_declaration() {
10547            // `run` re-declares the long-only global `--raw` as `-r --raw`
10548            // without `global`. That is the same flag: the global's `effect`
10549            // survives, the orphan short is unioned in, and it stays global.
10550            let spec = spec();
10551            let flags = available_flags(&chain(&spec, &["run"]));
10552            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10553            assert!(raw.global);
10554            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10555            assert_eq!(raw.short, ['r']);
10556        }
10557
10558        #[test]
10559        fn it_matches_what_a_parse_accepts() {
10560            // The invariant. If these ever disagree, one of them is lying to a
10561            // caller about which flags a command takes.
10562            let spec = spec();
10563            for path in [vec![], vec!["run"], vec!["run", "once"]] {
10564                let argv = std::iter::once("test".to_string())
10565                    .chain(path.iter().map(|s| s.to_string()))
10566                    .collect::<Vec<_>>();
10567                let parsed = parse_partial(&spec, &argv).unwrap();
10568
10569                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10570                    .map(|f| f.name.clone())
10571                    .collect();
10572                from_parse.sort();
10573                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10574            }
10575        }
10576    }
10577
10578    // Provenance: which token bound what, and where a value came from when no token did.
10579
10580    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
10581    /// assert on the whole picture rather than on one field at a time.
10582    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10583        parsed
10584            .tokens
10585            .iter()
10586            .find(|token| token.index == index)
10587            .unwrap_or_else(|| panic!("no token at {index}"))
10588            .roles
10589            .iter()
10590            .map(render_role)
10591            .collect()
10592    }
10593
10594    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10595        parsed
10596            .flag_origins
10597            .iter()
10598            .find(|(f, _)| f.name == flag)
10599            .map(|(_, origins)| origins.clone())
10600            .unwrap_or_default()
10601    }
10602
10603    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10604        let env = env
10605            .iter()
10606            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10607            .collect();
10608        Parser::new(spec)
10609            .with_env(env)
10610            .explain(&input(words))
10611            .unwrap()
10612    }
10613
10614    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10615        explain_with_env(spec, words, &[])
10616    }
10617
10618    #[test]
10619    fn an_attached_long_value_is_recorded_on_the_flag_token() {
10620        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10621            .parse()
10622            .unwrap();
10623
10624        let parsed = explain(&spec, &["ex", "--env=prod"]);
10625
10626        assert_eq!(roles(&parsed, 0), ["program"]);
10627        assert_eq!(
10628            roles(&parsed, 1),
10629            ["flag env as --env", "value of env = [\"prod\"], attached"]
10630        );
10631        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
10632        // form while the detached one worked, and nothing could show the difference.
10633        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10634    }
10635
10636    #[test]
10637    fn a_detached_long_value_is_recorded_on_its_own_token() {
10638        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10639            .parse()
10640            .unwrap();
10641
10642        let parsed = explain(&spec, &["ex", "--env", "prod"]);
10643
10644        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10645        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10646    }
10647
10648    #[test]
10649    fn a_short_bundle_is_attributed_to_the_bundle_token() {
10650        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10651            .parse()
10652            .unwrap();
10653
10654        let parsed = explain(&spec, &["ex", "-abj8"]);
10655
10656        // One word the caller wrote, four things it did — and the re-queued tails are
10657        // folded back onto it rather than appearing as tokens nobody typed.
10658        assert_eq!(
10659            roles(&parsed, 1),
10660            [
10661                "flag a as -a",
10662                "flag b as -b",
10663                "flag j as -j",
10664                "value of j = [\"8\"], attached",
10665            ]
10666        );
10667        assert_eq!(parsed.tokens.len(), 2);
10668    }
10669
10670    #[test]
10671    fn a_delimiter_splits_one_token_into_several_values() {
10672        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
10673            .parse()
10674            .unwrap();
10675
10676        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
10677
10678        assert_eq!(
10679            roles(&parsed, 2),
10680            ["value of tags = [\"a\", \"b\", \"c\"]"],
10681            "the values meant, not the word typed"
10682        );
10683    }
10684
10685    #[test]
10686    fn a_separator_and_the_words_after_it_are_distinguished() {
10687        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
10688            .parse()
10689            .unwrap();
10690
10691        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
10692
10693        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
10694        assert_eq!(roles(&parsed, 2), ["separator"]);
10695        // Past the separator `-x` is data, not an unknown flag.
10696        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
10697    }
10698
10699    #[test]
10700    fn a_second_separator_is_data() {
10701        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
10702            .parse()
10703            .unwrap();
10704
10705        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
10706
10707        assert_eq!(roles(&parsed, 1), ["separator"]);
10708        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
10709    }
10710
10711    #[test]
10712    fn an_unknown_flag_says_what_took_it() {
10713        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10714            .parse()
10715            .unwrap();
10716
10717        let parsed = explain(&spec, &["ex", "--wat"]);
10718
10719        // The default is lax, so the word became data. Which is the useful thing to be
10720        // told: the alternative reading is "you have a typo".
10721        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
10722    }
10723
10724    #[test]
10725    fn a_subcommand_word_is_not_a_positional() {
10726        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
10727            .parse()
10728            .unwrap();
10729
10730        let parsed = explain(&spec, &["ex", "build", "a"]);
10731
10732        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
10733        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
10734    }
10735
10736    #[test]
10737    fn a_multicall_applet_is_read_at_argv0() {
10738        let spec: Spec =
10739            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
10740                .parse()
10741                .unwrap();
10742
10743        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
10744
10745        // argv[0] is both the program and the word that selected the applet, and the word
10746        // read there is not the word the caller wrote.
10747        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
10748        assert!(parsed.tokens[0].synthesized);
10749        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
10750    }
10751
10752    #[test]
10753    fn words_the_parse_never_reached_say_so() {
10754        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10755            .parse()
10756            .unwrap();
10757
10758        let parsed = Parser::new(&spec)
10759            .explain(&input(&["ex", "--help", "a"]))
10760            .unwrap();
10761
10762        assert_eq!(roles(&parsed, 2), ["unread"]);
10763    }
10764
10765    #[test]
10766    fn an_env_origin_names_the_variable_that_fired() {
10767        let spec: Spec =
10768            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
10769                .parse()
10770                .unwrap();
10771
10772        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
10773        assert_eq!(
10774            origins(&primary, "token"),
10775            [ValueOrigin::Env("EX_TOKEN".to_string())]
10776        );
10777
10778        // The fallback firing is a different fact from the primary firing, and which one it
10779        // was is what says which declaration to delete.
10780        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
10781        assert_eq!(
10782            origins(&fallback, "token"),
10783            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
10784        );
10785    }
10786
10787    #[test]
10788    fn a_default_origin_is_recorded_for_flags_and_args() {
10789        let spec: Spec =
10790            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
10791                .parse()
10792                .unwrap();
10793
10794        let parsed = explain(&spec, &["ex"]);
10795
10796        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
10797        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
10798        assert_eq!(arg.name, "src");
10799        assert_eq!(origins, &[ValueOrigin::Default]);
10800    }
10801
10802    #[test]
10803    fn a_default_if_origin_carries_the_condition_that_fired() {
10804        let spec: Spec = r#"
10805name "ex"
10806bin "ex"
10807flag "--profile <p>"
10808flag "--strict" {
10809    default_if "--profile" "prod" "true"
10810}
10811        "#
10812        .parse()
10813        .unwrap();
10814
10815        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
10816
10817        // The selector alone is ambiguous: several conditions may name it with different
10818        // `when` values, so the report has to say which one matched.
10819        assert_eq!(
10820            origins(&parsed, "strict"),
10821            [ValueOrigin::DefaultIf {
10822                selector: "--profile".to_string(),
10823                when: Some("prod".to_string()),
10824            }]
10825        );
10826    }
10827
10828    #[test]
10829    fn a_bare_optional_value_flag_records_default_missing() {
10830        let spec: Spec =
10831            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
10832                .parse()
10833                .unwrap();
10834
10835        let parsed = explain(&spec, &["ex", "--color", "-v"]);
10836
10837        // The flag was typed and the value was not, which is the distinction a spec author
10838        // is asking about when they ask why `--color` came out `always`.
10839        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
10840        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10841        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
10842    }
10843
10844    #[test]
10845    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
10846        let spec: Spec =
10847            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
10848                .parse()
10849                .unwrap();
10850
10851        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
10852
10853        // Why origins are a list: one declaration, two occurrences, two different answers.
10854        assert_eq!(
10855            roles(&parsed, 1),
10856            [
10857                "flag color as --color",
10858                "value of color = [\"red\"], attached"
10859            ]
10860        );
10861        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10862    }
10863
10864    #[test]
10865    fn an_override_names_the_flag_that_did_it() {
10866        let spec: Spec =
10867            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
10868                .parse()
10869                .unwrap();
10870
10871        let parsed = explain(&spec, &["ex", "--loud"]);
10872
10873        // Without the overriding name, "`--quiet` is unset despite its default" has no
10874        // answer: the fallback phase silently declines to fill an overridden flag.
10875        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
10876        assert!(origins(&parsed, "quiet").is_empty());
10877    }
10878
10879    #[test]
10880    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
10881        let spec: Spec = r#"
10882name "ex"
10883bin "ex"
10884cmd "run" restart_token=":::" {
10885    arg "<task>" default="build"
10886}
10887        "#
10888        .parse()
10889        .unwrap();
10890
10891        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
10892
10893        // The values belong to the last invocation, so provenance must too — but the words
10894        // of the first were still read, and a report that dropped them would show a command
10895        // line with a hole in it.
10896        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
10897        // And the token that did the resetting says so: without a role of its own it reads
10898        // as a word that did nothing, next to a `lint` that filled an arg now empty.
10899        assert_eq!(roles(&parsed, 3), ["restart"]);
10900        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
10901        assert!(parsed.arg_origins.is_empty());
10902    }
10903
10904    #[test]
10905    fn a_value_terminator_says_which_run_it_ended() {
10906        let spec: Spec = r#"
10907name "ex"
10908bin "ex"
10909flag "--exec <cmd>..." value_terminator=";"
10910arg "<src>"
10911        "#
10912        .parse()
10913        .unwrap();
10914
10915        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
10916
10917        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
10918        // The terminator is consumed and is not one of the values, which is the whole reason
10919        // it was declared — so it needs a row saying that rather than an empty one.
10920        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
10921        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
10922    }
10923
10924    #[test]
10925    fn an_args_value_terminator_says_which_run_it_ended() {
10926        let spec: Spec = r#"
10927name "ex"
10928bin "ex"
10929arg "<files>..." value_terminator=";"
10930arg "[dest]"
10931        "#
10932        .parse()
10933        .unwrap();
10934
10935        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
10936
10937        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
10938        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
10939        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
10940    }
10941
10942    #[test]
10943    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
10944        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
10945            .parse()
10946            .unwrap();
10947
10948        let parsed = Parser::new(&spec)
10949            .explain(&input(&["ex", "--env=prod"]))
10950            .unwrap();
10951
10952        // `parse` reports "missing required <src>" and nothing else, which is the report the
10953        // caller already had. This is the case the whole thing exists for.
10954        assert!(Parser::new(&spec)
10955            .parse(&input(&["ex", "--env=prod"]))
10956            .is_err());
10957        assert_eq!(
10958            roles(&parsed, 1),
10959            ["flag env as --env", "value of env = [\"prod\"], attached"]
10960        );
10961        assert!(
10962            parsed.errors.iter().any(|e| e.to_string().contains("src")),
10963            "{:?}",
10964            parsed.errors
10965        );
10966    }
10967
10968    #[test]
10969    fn an_external_subcommand_forwards_whole_tokens() {
10970        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
10971            .parse()
10972            .unwrap();
10973
10974        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
10975
10976        assert_eq!(roles(&parsed, 1), ["external"]);
10977        assert_eq!(roles(&parsed, 2), ["external"]);
10978    }
10979
10980    #[test]
10981    fn a_view_keeps_the_callers_argv_positions() {
10982        let spec: Spec = r#"
10983bin "ex"
10984view "runner" root="run"
10985cmd "run" {
10986    flag "--token <token>"
10987}
10988        "#
10989        .parse()
10990        .unwrap();
10991
10992        let parsed = explain(&spec, &["runner", "--token", "secret"]);
10993
10994        // A view re-enters the parse with the same argv, so the positions still mean what
10995        // the caller wrote.
10996        assert_eq!(roles(&parsed, 0), ["program"]);
10997        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10998    }
10999}