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 the active positional arguments, kept as an index rather than a reference
1655    // because an explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"`
1656    // arm).
1657    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1658    // argument filled?" has to consult `out.args` by key instead of counting.
1659    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1660    let mut enable_flags = true;
1661    let mut grouped_flag = false;
1662    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1663    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1664    // words that come after it — see `report_double_dash_violation`.
1665    let mut seen_double_dash = false;
1666    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1667    // reopen sigil classification for this invocation.
1668    let mut restart_seen = false;
1669    // Args already reported as having been offered a word before the `--` they require, so a
1670    // variadic one does not report the same violation for every word it is offered.
1671    let mut double_dash_violations: HashSet<String> = HashSet::new();
1672    // Scalar occurrences are scoped to the command level where they were written. Inherited
1673    // globals may therefore appear once before and once after a subcommand under clap's strict
1674    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1675    // opposite forms may override one another, while repeating either spelling is an error.
1676    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1677
1678    while !input.is_empty() {
1679        let token = input.pop_front().unwrap();
1680        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1681        let binding = token.binding;
1682        let argv = token.argv;
1683        let mut w = token.word;
1684        // A short's attached value is re-queued with `grouped_flag` set, and that
1685        // continuation is not a following word. `require_equals` refuses only the
1686        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1687        // only when the value is actually missing, so `-cnever` is still `never`.
1688        let attached_continuation = grouped_flag;
1689
1690        // A clause boundary is syntax even after an automatic trailing argument disabled
1691        // flags. Only an explicit `--` protects a literal separator.
1692        if !seen_double_dash {
1693            if let Some(clause) = out.cmd.clause.as_ref() {
1694                if clause.separator.as_deref() == Some(w.as_str()) {
1695                    while try_bind_default_missing(
1696                        &mut out.flags,
1697                        &mut out.flag_awaiting_value,
1698                        custom_env,
1699                        &mut out.flag_origins,
1700                    )? {}
1701                    if let Some(flag) = out.flag_awaiting_value.first() {
1702                        let spelling = flag
1703                            .long
1704                            .first()
1705                            .map(|long| format!("--{long}"))
1706                            .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1707                            .unwrap_or_else(|| flag.name.clone());
1708                        return Err(UsageErr::InvalidFlag {
1709                            token: spelling.clone(),
1710                            reason: "requires an argument".to_string(),
1711                            span: (0, spelling.len()).into(),
1712                            input: spelling,
1713                        }
1714                        .into());
1715                    }
1716                    let name = clause.name.clone();
1717                    finalize_current_clause(&mut out);
1718                    out.arg_origins.clear();
1719                    trace.record(argv, TokenRole::ClauseSeparator { name });
1720                    next_arg_idx = 0;
1721                    out.flag_awaiting_value.clear();
1722                    reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1723                    enable_flags = true;
1724                    seen_double_dash = false;
1725                    continue;
1726                }
1727            }
1728        }
1729
1730        // Check for restart_token - resets argument parsing for multiple command invocations
1731        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1732        if let Some(ref restart_token) = out.cmd.restart_token {
1733            if w == *restart_token {
1734                // Reset argument parsing state for a fresh command invocation, keeping the
1735                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1736                // is not cleared here either, so clearing it would let one arg report the same
1737                // violation once per invocation.
1738                out.args.clear();
1739                // With the values gone, so is where they came from — otherwise the second
1740                // invocation of `run lint ::: test` reports the first one's provenance. The
1741                // token trace is *not* cleared: those words were read, and a report that
1742                // dropped them would show a command line with a hole in it.
1743                out.arg_origins.clear();
1744                trace.record(argv, TokenRole::Restart);
1745                next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1746                restart_seen = true;
1747                out.flag_awaiting_value.clear(); // Clear any pending flag values
1748                enable_flags = true; // Reset -- separator effect
1749                seen_double_dash = false; // The next invocation needs its own `--`
1750                continue;
1751            }
1752        }
1753
1754        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1755        // like, and that has to be asked before the separator arm below rather than
1756        // after it. Asked after, a `--` was consumed as a separator while the flag
1757        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1758        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1759        // itself, which is what clap does with the same declaration — and no flag can
1760        // still be waiting once the separator has done its job, so the starvation rule
1761        // below has no path around it.
1762        if enable_flags
1763            && !attached_continuation
1764            && w.starts_with('-')
1765            && out
1766                .flag_awaiting_value
1767                .last()
1768                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1769        {
1770            // A variadic argument collects here too: which token supplied its first
1771            // value says nothing about how many it takes.
1772            let should_return = bind_pending_flag_value(
1773                spec,
1774                &out.cmd,
1775                &mut out.errors,
1776                &mut out.flags,
1777                &mut out.flag_awaiting_value,
1778                &mut w,
1779                &mut input,
1780                custom_env,
1781                trace,
1782                argv,
1783                // The token a hyphen-valued flag takes is the following word, never attached.
1784                false,
1785            )?;
1786            if should_return {
1787                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1788                return Ok((out, overridden_flags));
1789            }
1790            continue;
1791        }
1792
1793        // A flag whose value may be omitted that cannot take this token as a detached
1794        // value finishes bare and leaves the token for whatever comes next:
1795        // `--color --verbose` colours with the missing value and still sets verbose,
1796        // and `--inspect 9229` with `require_equals` binds the missing value rather
1797        // than treating 9229 as the port.
1798        if enable_flags
1799            && !attached_continuation
1800            && !out.flag_awaiting_value.is_empty()
1801            && out.flag_awaiting_value.last().is_some_and(|flag| {
1802                (flag.default_missing.is_some() || flag.value_optional)
1803                    && !accepts_detached_flag_value(flag, &w)
1804            })
1805        {
1806            try_bind_default_missing(
1807                &mut out.flags,
1808                &mut out.flag_awaiting_value,
1809                custom_env,
1810                &mut out.flag_origins,
1811            )?;
1812        }
1813
1814        // The first explicit `--` is still a separator after an `automatic` argument has
1815        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1816        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1817        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1818        // behavior as the bug it is.
1819        if w == "--" && !seen_double_dash {
1820            enable_flags = false;
1821
1822            // Only preserve the double dash token if we're collecting values for a variadic arg
1823            // in double_dash == `preserve` mode
1824            let should_preserve = active_args(&out.cmd)
1825                .get(next_arg_idx)
1826                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1827                .unwrap_or(false);
1828
1829            if should_preserve {
1830                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1831                // neither counts as one nor unlocks a `double_dash="required"` arg.
1832            } else {
1833                seen_double_dash = true;
1834                trace.record(argv, TokenRole::Separator);
1835
1836                // Everything after an explicit `--` belongs to the arg that requires one, so
1837                // jump the cursor there — past any earlier arg, including a greedy variadic
1838                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1839                // which is what `double_dash="required"` is generated from. Specs without such
1840                // an arg find nothing and keep the cursor where it was.
1841                let target = active_args(&out.cmd).iter().position(|arg| {
1842                    arg.double_dash == SpecDoubleDashChoices::Required
1843                        && !out.args.contains_key(arg)
1844                });
1845                if let Some(target) = target {
1846                    // Forward only. An unfilled required arg declared *before* the cursor is
1847                    // left where it is rather than rewound to — words already assigned to
1848                    // later args would have to be taken back for that to mean anything, and
1849                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1850                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1851                    // one ahead of others is already outside what this models.
1852                    if target > next_arg_idx {
1853                        next_arg_idx = target;
1854                    }
1855                }
1856                continue;
1857            }
1858        }
1859
1860        // long flags
1861        if enable_flags && w.starts_with("--") {
1862            grouped_flag = false;
1863            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1864            // an empty value while `--jobs` supplies none. Collapsing the two lost
1865            // the flag entirely.
1866            let split = w.split_once('=');
1867            let word = split.map(|(word, _)| word).unwrap_or(&w);
1868            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1869            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1870                let command_level = binding
1871                    .as_ref()
1872                    .map(|(_, level)| *level)
1873                    .unwrap_or(out.cmds.len() - 1);
1874                parsed_flag_spellings
1875                    .entry(Arc::as_ptr(f) as usize)
1876                    .or_default()
1877                    .insert(word.to_string());
1878                // Recorded before the action check below: a token that named a flag named it
1879                // whether or not the parse can carry on afterwards.
1880                trace.record(
1881                    argv,
1882                    TokenRole::Flag {
1883                        flag: Arc::clone(f),
1884                        spelling: word.to_string(),
1885                        negated: f.negate.as_deref() == Some(word),
1886                    },
1887                );
1888                if f.action != crate::SpecFlagAction::Set {
1889                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1890                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1891                    return Ok((out, overridden_flags));
1892                }
1893                apply_flag_overrides(
1894                    f,
1895                    &out.available_flags,
1896                    &mut out.flags,
1897                    &mut out.flag_awaiting_value,
1898                    &mut overridden_flags,
1899                    &mut out.overridden_flags,
1900                );
1901                if let Some(pending) = out.flag_awaiting_value.first() {
1902                    out.errors.push(render_missing_flag_value(pending, &w));
1903                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1904                    return Ok((out, overridden_flags));
1905                }
1906                // An attached value only means something to a flag that takes one:
1907                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1908                // give a flag that holds no value. Handing that leftover to the
1909                // positionals would re-split one token into two, so `ex --force=yes`
1910                // would fill an argument the caller never typed a word for.
1911                if f.arg.is_some() {
1912                    record_scalar_flag_occurrence(
1913                        &out.cmds,
1914                        f,
1915                        command_level,
1916                        None,
1917                        &mut scalar_occurrences,
1918                        &mut out.errors,
1919                    );
1920                    let f = Arc::clone(f);
1921                    out.flag_awaiting_value.push(Arc::clone(&f));
1922                    // The `=` has already settled that this text is the value, so it
1923                    // binds here rather than going back on the queue to be read as a
1924                    // token again — where `--jobs=--force` looked like a flag of its
1925                    // own and bound `force`, leaving `jobs` unset.
1926                    if let Some((_, val)) = split {
1927                        // The `=` settles where the *first* value came from and nothing
1928                        // more, so a variadic argument goes on collecting from the words
1929                        // after it exactly as the detached form does.
1930                        let mut val = val.to_string();
1931                        let should_return = bind_pending_flag_value(
1932                            spec,
1933                            &out.cmd,
1934                            &mut out.errors,
1935                            &mut out.flags,
1936                            &mut out.flag_awaiting_value,
1937                            &mut val,
1938                            &mut input,
1939                            custom_env,
1940                            trace,
1941                            argv,
1942                            // The `=` settled that this text is the value, so it rode in on
1943                            // the flag's own token.
1944                            true,
1945                        )?;
1946                        if should_return {
1947                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1948                            return Ok((out, overridden_flags));
1949                        }
1950                    }
1951                } else if f.count {
1952                    let arr = out
1953                        .flags
1954                        .entry(Arc::clone(f))
1955                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1956                        .try_as_multi_bool_mut()
1957                        .unwrap();
1958                    arr.push(true);
1959                } else {
1960                    let negate = f.negate.clone().unwrap_or_default();
1961                    let negated_form = word == negate;
1962                    let value = if f.bool_value {
1963                        match split.map(|(_, value)| value) {
1964                            Some("true") => !negated_form,
1965                            Some("false") => negated_form,
1966                            Some(value) => {
1967                                out.errors.push(UsageErr::InvalidValue {
1968                                    name: f.name.clone(),
1969                                    value: value.to_string(),
1970                                    reason: "expected `true` or `false`".to_string(),
1971                                });
1972                                continue;
1973                            }
1974                            None => !negated_form,
1975                        }
1976                    } else {
1977                        !negated_form
1978                    };
1979                    // Which form was typed is a question about the name, so it is
1980                    // asked of `word` rather than the whole token: the attached value
1981                    // is dropped just above, and comparing `--no-color=yes` against
1982                    // `--no-color` would take the negation down with it.
1983                    record_scalar_flag_occurrence(
1984                        &out.cmds,
1985                        f,
1986                        command_level,
1987                        Some(!negated_form),
1988                        &mut scalar_occurrences,
1989                        &mut out.errors,
1990                    );
1991                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1992                }
1993                continue;
1994            }
1995            if is_help_arg(spec, &out.cmd, &w) {
1996                out.errors
1997                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1998                trace.record(
1999                    argv,
2000                    TokenRole::Builtin {
2001                        spelling: w.clone(),
2002                    },
2003                );
2004                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2005                return Ok((out, overridden_flags));
2006            }
2007            if is_version_arg(spec, &out.cmds, &w) {
2008                out.errors.push(render_version_err(spec, w.len() > 2));
2009                trace.record(
2010                    argv,
2011                    TokenRole::Builtin {
2012                        spelling: w.clone(),
2013                    },
2014                );
2015                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2016                return Ok((out, overridden_flags));
2017            }
2018            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2019                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2020                trace.close(&input);
2021                return Err(refused.into());
2022            }
2023        }
2024
2025        // short flags
2026        //
2027        // A fresh token is checked whole before any of it is applied: `-az` with only
2028        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2029        // discovering that `z` names nothing. A grouped continuation is exempt — its
2030        // token was already checked when it arrived.
2031        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2032        let positional_negative_number = !declared_numeric_short
2033            && is_negative_number(&w)
2034            && active_args(&out.cmd)
2035                .get(next_arg_idx)
2036                .is_some_and(|arg| arg.allow_negative_numbers);
2037        if enable_flags
2038            && !grouped_flag
2039            // A word phase 1 already resolved to a flag needs no re-checking, and
2040            // the flags in scope have changed since, so re-checking would be wrong.
2041            && binding.is_none()
2042            && w.starts_with('-')
2043            && w.len() > 1
2044            && is_flag_like(&w)
2045            && !positional_negative_number
2046            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2047        {
2048            // Refused if this command asked for that; otherwise it carries on below
2049            // as one word, with none of its letters applied.
2050            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2051                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2052                trace.close(&input);
2053                return Err(refused.into());
2054            }
2055        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2056            let short = w.chars().nth(1).unwrap();
2057            if let Some(f) = binding
2058                .as_ref()
2059                .map(|(flag, _)| flag)
2060                .or_else(|| out.available_flags.get(&format!("-{short}")))
2061            {
2062                let command_level = binding
2063                    .as_ref()
2064                    .map(|(_, level)| *level)
2065                    .unwrap_or(out.cmds.len() - 1);
2066                if f.action != crate::SpecFlagAction::Set {
2067                    out.errors
2068                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2069                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2070                    return Ok((out, overridden_flags));
2071                }
2072                parsed_flag_spellings
2073                    .entry(Arc::as_ptr(f) as usize)
2074                    .or_default()
2075                    .insert(format!("-{short}"));
2076                trace.record(
2077                    argv,
2078                    TokenRole::Flag {
2079                        flag: Arc::clone(f),
2080                        spelling: format!("-{short}"),
2081                        // A short spelling is never the negated form: `negate` is a long.
2082                        negated: false,
2083                    },
2084                );
2085                apply_flag_overrides(
2086                    f,
2087                    &out.available_flags,
2088                    &mut out.flags,
2089                    &mut out.flag_awaiting_value,
2090                    &mut overridden_flags,
2091                    &mut out.overridden_flags,
2092                );
2093                if !attached_continuation {
2094                    if let Some(pending) = out.flag_awaiting_value.first() {
2095                        out.errors.push(render_missing_flag_value(pending, &w));
2096                        record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2097                        return Ok((out, overridden_flags));
2098                    }
2099                }
2100                let rest = &w[1 + short.len_utf8()..];
2101                if !rest.is_empty() {
2102                    // `-abc` is one token that names three flags, so the tail is read at the
2103                    // bundle's own position rather than at one of its own.
2104                    input.push_front(Token::new(format!("-{rest}"), argv));
2105                }
2106                // A fully consumed short is no longer a grouped continuation.
2107                // Leaving this set after `-ai` made `-i` skip `require_equals`
2108                // and bind the following word.
2109                grouped_flag = !rest.is_empty();
2110                if f.arg.is_some() {
2111                    record_scalar_flag_occurrence(
2112                        &out.cmds,
2113                        f,
2114                        command_level,
2115                        None,
2116                        &mut scalar_occurrences,
2117                        &mut out.errors,
2118                    );
2119                    out.flag_awaiting_value.push(Arc::clone(f));
2120                } else if f.count {
2121                    let arr = out
2122                        .flags
2123                        .entry(Arc::clone(f))
2124                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2125                        .try_as_multi_bool_mut()
2126                        .unwrap();
2127                    arr.push(true);
2128                } else {
2129                    let negate = f.negate.clone().unwrap_or_default();
2130                    let value = w != negate;
2131                    record_scalar_flag_occurrence(
2132                        &out.cmds,
2133                        f,
2134                        command_level,
2135                        Some(value),
2136                        &mut scalar_occurrences,
2137                        &mut out.errors,
2138                    );
2139                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2140                }
2141                continue;
2142            }
2143            // The letter nothing declared may still be one the parser supplies, and it may
2144            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2145            // neither reaches the whole-token spellings below.
2146            if let Some(err) = supplied_short(spec, &out.cmds, short) {
2147                out.errors.push(err);
2148                trace.record(
2149                    argv,
2150                    TokenRole::Builtin {
2151                        spelling: format!("-{short}"),
2152                    },
2153                );
2154                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2155                return Ok((out, overridden_flags));
2156            }
2157            if is_help_arg(spec, &out.cmd, &w) {
2158                out.errors
2159                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2160                trace.record(
2161                    argv,
2162                    TokenRole::Builtin {
2163                        spelling: w.clone(),
2164                    },
2165                );
2166                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2167                return Ok((out, overridden_flags));
2168            }
2169            if is_version_arg(spec, &out.cmds, &w) {
2170                out.errors.push(render_version_err(spec, w.len() > 2));
2171                trace.record(
2172                    argv,
2173                    TokenRole::Builtin {
2174                        spelling: w.clone(),
2175                    },
2176                );
2177                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2178                return Ok((out, overridden_flags));
2179            }
2180            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2181                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2182                trace.close(&input);
2183                return Err(refused.into());
2184            }
2185            if grouped_flag {
2186                grouped_flag = false;
2187                w.remove(0);
2188                // What is left is a short flag's attached value, and one `=` between
2189                // the letter and the value is a separator: `-j=8` means 8. Only one,
2190                // so `-j==8` still means `=8`.
2191                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2192                    w.remove(0);
2193                }
2194            }
2195        }
2196
2197        // Only while flags are still being read. A flag still waiting when the separator
2198        // was consumed is starved: its value would have to come from after the `--`,
2199        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2200        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2201        // was gone. Left waiting, it is reported as the missing value it is.
2202        // `require_equals` refuses a detached value: `--flag value` is a missing
2203        // value, not a flag of `"value"`. The attached form is still bound above.
2204        // Reported here rather than left waiting until the end of the line: falling
2205        // through would offer `value` to the positionals and call it an unexpected
2206        // word, which is the wrong error and a different one from usage-argv.
2207        if enable_flags
2208            && !attached_continuation
2209            && !out.flag_awaiting_value.is_empty()
2210            && out
2211                .flag_awaiting_value
2212                .last()
2213                .is_some_and(|flag| flag.require_equals)
2214        {
2215            let flag = out.flag_awaiting_value.last().unwrap();
2216            let token = flag
2217                .long
2218                .first()
2219                .map(|l| format!("--{l}"))
2220                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2221                .unwrap_or_else(|| flag.name.clone());
2222            out.errors.push(UsageErr::InvalidFlag {
2223                token: token.clone(),
2224                reason: "requires an argument".to_string(),
2225                span: (0, 0).into(),
2226                input: format!("{token} {w}"),
2227            });
2228            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2229            return Ok((out, overridden_flags));
2230        }
2231        if enable_flags
2232            && !out.flag_awaiting_value.is_empty()
2233            && (attached_continuation
2234                || out
2235                    .flag_awaiting_value
2236                    .last()
2237                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2238        {
2239            // Held before the drain pops it: a flag whose argument is variadic keeps
2240            // taking values after this first one.
2241            let should_return = bind_pending_flag_value(
2242                spec,
2243                &out.cmd,
2244                &mut out.errors,
2245                &mut out.flags,
2246                &mut out.flag_awaiting_value,
2247                &mut w,
2248                &mut input,
2249                custom_env,
2250                trace,
2251                argv,
2252                attached_continuation,
2253            )?;
2254            if should_return {
2255                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2256                return Ok((out, overridden_flags));
2257            }
2258            continue;
2259        }
2260
2261        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2262            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2263            .flatten()
2264        {
2265            if value.is_empty() {
2266                out.errors.push(UsageErr::InvalidValue {
2267                    name: arg.name.clone(),
2268                    value: w.clone(),
2269                    reason: format!("expected a value after sigil {sigil:?}"),
2270                });
2271                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2272                return Ok((out, overridden_flags));
2273            }
2274            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2275            let suppress_trailing_delimiter =
2276                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2277            let delimiter = if suppress_trailing_delimiter && trailing_value {
2278                None
2279            } else {
2280                arg.delimiter
2281            };
2282            let parts = match delimiter {
2283                Some(delimiter) => value
2284                    .split(delimiter)
2285                    .map(str::to_string)
2286                    .collect::<Vec<_>>(),
2287                None => vec![value.to_string()],
2288            };
2289            let mut refused = false;
2290            for part in &parts {
2291                if validate_choices(
2292                    spec,
2293                    &out.cmd,
2294                    &mut out.errors,
2295                    ChoiceTarget::arg(arg),
2296                    part,
2297                    arg.choices.as_ref(),
2298                    custom_env,
2299                )? {
2300                    refused = true;
2301                    break;
2302                }
2303            }
2304            if refused {
2305                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2306                return Ok((out, overridden_flags));
2307            }
2308            trace.record(
2309                argv,
2310                TokenRole::Sigil {
2311                    arg: Arc::new(arg.clone()),
2312                    sigil: sigil.to_string(),
2313                    values: parts.clone(),
2314                },
2315            );
2316            let key = Arc::new(arg.clone());
2317            if arg.var {
2318                let arr = out
2319                    .args
2320                    .entry(key)
2321                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2322                    .try_as_multi_string_mut()
2323                    .unwrap();
2324                arr.extend(parts);
2325            } else {
2326                out.args.insert(key, ParseValue::String(value.to_string()));
2327            }
2328            continue;
2329        }
2330
2331        if out.cmd.allow_missing_positional {
2332            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2333            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2334                if current.required || out.args.contains_key(current) {
2335                    break;
2336                }
2337                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2338                    .iter()
2339                    .filter(|arg| arg.required && arg.sigil.is_none())
2340                    .count();
2341                if required_after == 0 {
2342                    break;
2343                }
2344                let remaining_values = 1 + input
2345                    .iter()
2346                    .filter(|token| {
2347                        (!enable_flags || !is_flag_like(&token.word))
2348                            && (!enable_flags
2349                                || restart_seen
2350                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2351                    })
2352                    .count();
2353                if remaining_values > required_after {
2354                    break;
2355                }
2356                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2357            }
2358        }
2359
2360        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2361            if arg.var
2362                && out.args.contains_key(arg)
2363                && arg.value_terminator.as_deref() == Some(w.as_str())
2364            {
2365                trace.record(
2366                    argv,
2367                    TokenRole::ValueTerminator {
2368                        ends: arg.name.clone(),
2369                    },
2370                );
2371                next_arg_idx += 1;
2372                continue;
2373            }
2374            // Before anything else: an arg that requires `--` accepts nothing until one has been
2375            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2376            // reported as an invalid choice, and from reaching that function's help escape.
2377            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2378                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2379                trace.record(
2380                    argv,
2381                    TokenRole::Refused {
2382                        reason: format!("{} only accepts words after `--`", arg.name),
2383                    },
2384                );
2385                // Drop the word without filling the arg or advancing the cursor: every later
2386                // word hits the same arg and is rejected the same way, so the parse still ends
2387                // in an error rather than in `unexpected word`.
2388                continue;
2389            }
2390            // Split before judging, as the flag path does: after the split the word is
2391            // no longer one value, and `choices` has to be asked about each. Judging
2392            // first rejects `src:docs` against a list that both halves are on, and
2393            // names the whole word rather than the half that was wrong.
2394            let trailing_value =
2395                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2396            let suppress_trailing_delimiter =
2397                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2398            let delimiter = if suppress_trailing_delimiter && trailing_value {
2399                None
2400            } else {
2401                arg.delimiter
2402            };
2403            let parts: Vec<String> = match delimiter {
2404                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2405                None => vec![w.clone()],
2406            };
2407            let mut refused = false;
2408            for part in &parts {
2409                if validate_choices(
2410                    spec,
2411                    &out.cmd,
2412                    &mut out.errors,
2413                    ChoiceTarget::arg(arg),
2414                    part,
2415                    arg.choices.as_ref(),
2416                    custom_env,
2417                )? {
2418                    refused = true;
2419                    break;
2420                }
2421            }
2422            if refused {
2423                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2424                return Ok((out, overridden_flags));
2425            }
2426            // `double_dash="automatic"` means the first value this arg takes is the last
2427            // token read as anything but data: a wrapper declaring it can forward flags
2428            // without its caller typing a `--`. Set before the value is stored, so the
2429            // rest of the command line is already past flag parsing.
2430            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2431                enable_flags = false;
2432            }
2433            // A flag-like word reaching a positional while flags are still being read was
2434            // offered to every declaration and matched none: under the default
2435            // `unknown_flags="value"` it becomes data, and saying so is the difference
2436            // between "you have a typo" and "this argument took your typo".
2437            let unknown_flag = enable_flags
2438                && !positional_negative_number
2439                && is_flag_like(&w)
2440                && binding.is_none();
2441            trace.record(
2442                argv,
2443                if unknown_flag {
2444                    TokenRole::UnknownFlag {
2445                        bound_as: Some(Arc::new(arg.clone())),
2446                    }
2447                } else {
2448                    TokenRole::Arg {
2449                        arg: Arc::new(arg.clone()),
2450                        values: parts.clone(),
2451                    }
2452                },
2453            );
2454            if arg.var {
2455                let arr = out
2456                    .args
2457                    .entry(Arc::new(arg.clone()))
2458                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2459                    .try_as_multi_string_mut()
2460                    .unwrap();
2461                // The values this word carried, split above so that everything
2462                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2463                // counts the values the user meant rather than the words they typed.
2464                arr.extend(parts.iter().cloned());
2465                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2466                    next_arg_idx += 1;
2467                }
2468            } else {
2469                out.args
2470                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2471                next_arg_idx += 1;
2472            }
2473            if out
2474                .cmd
2475                .clause
2476                .as_ref()
2477                .is_some_and(|clause| clause.separator.is_none())
2478                && next_arg_idx >= active_args(&out.cmd).len()
2479            {
2480                while try_bind_default_missing(
2481                    &mut out.flags,
2482                    &mut out.flag_awaiting_value,
2483                    custom_env,
2484                    &mut out.flag_origins,
2485                )? {}
2486                if let Some(flag) = out.flag_awaiting_value.first() {
2487                    let spelling = flag
2488                        .long
2489                        .first()
2490                        .map(|long| format!("--{long}"))
2491                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2492                        .unwrap_or_else(|| flag.name.clone());
2493                    return Err(UsageErr::InvalidFlag {
2494                        token: spelling.clone(),
2495                        reason: "requires an argument".to_string(),
2496                        span: (0, spelling.len()).into(),
2497                        input: spelling,
2498                    }
2499                    .into());
2500                }
2501                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2502                finalize_current_clause(&mut out);
2503                out.arg_origins.clear();
2504                trace.record(argv, TokenRole::ClauseSeparator { name });
2505                next_arg_idx = 0;
2506                out.flag_awaiting_value.clear();
2507                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2508                enable_flags = true;
2509                seen_double_dash = false;
2510            }
2511            continue;
2512        }
2513        if is_help_arg(spec, &out.cmd, &w) {
2514            out.errors
2515                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2516            trace.record(
2517                argv,
2518                TokenRole::Builtin {
2519                    spelling: w.clone(),
2520                },
2521            );
2522            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2523            return Ok((out, overridden_flags));
2524        }
2525        if is_version_arg(spec, &out.cmds, &w) {
2526            out.errors.push(render_version_err(spec, w.len() > 2));
2527            trace.record(
2528                argv,
2529                TokenRole::Builtin {
2530                    spelling: w.clone(),
2531                },
2532            );
2533            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2534            return Ok((out, overridden_flags));
2535        }
2536        trace.record(
2537            argv,
2538            TokenRole::Refused {
2539                reason: "no declaration takes this word".to_string(),
2540            },
2541        );
2542        trace.close(&input);
2543        bail!("unexpected word: {w}");
2544    }
2545
2546    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2547    if validate_clauses {
2548        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2549    }
2550    let clause_flag_names = out
2551        .cmd
2552        .clause
2553        .iter()
2554        .flat_map(|clause| &clause.flags)
2555        .map(|flag| flag.name.clone())
2556        .collect::<HashSet<_>>();
2557
2558    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2559    // declarations with the same canonical name therefore share one public value entry even
2560    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2561    // both, which is what exclusivity needs.
2562    let flag_was_parsed =
2563        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2564
2565    // The spellings the selected command's own declaration speaks for, on this object.
2566    //
2567    // Empty unless that declaration really is this object's: a parent and child may each
2568    // declare `--clean` without merging, leaving two flags that share a name, and the
2569    // ancestor's must not be read as the child's. The test is whether every spelling the child
2570    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2571    // an ancestor whose long form the child took over.
2572    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2573        let declared: HashSet<String> = out
2574            .cmd
2575            .flags
2576            .iter()
2577            .filter(|declared| declared.name == flag.name)
2578            .flat_map(flag_keys)
2579            .collect();
2580        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2581        // some other inherited global already owns — `-c --clean` beside an inherited
2582        // `-c --config` — and that collision is resolved in the other global's favor, so the
2583        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2584        // unrelated collision disown the child from the `--clean` it plainly does own.
2585        //
2586        // Still enough to tell the two-object case apart, which is what this guards: when a
2587        // child re-declares a global as global, the child's own spellings resolve to the
2588        // child's separate flag, so none of them lands on the ancestor's.
2589        let speaks_for_this_flag = declared.iter().any(|spelling| {
2590            out.available_flags
2591                .get(spelling)
2592                .is_some_and(|available| Arc::ptr_eq(available, flag))
2593        });
2594        if speaks_for_this_flag {
2595            declared
2596        } else {
2597            HashSet::new()
2598        }
2599    };
2600
2601    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2602    //
2603    // A child that re-declares an inherited global merges into one object answering to two
2604    // alias sets whose declarations may disagree, so there is no single owner to name: the
2605    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2606    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2607    // child's in one invocation — and each carries its own declaration's answer.
2608    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2609        let child = child_spellings(flag);
2610        let child_exclusive = !child.is_empty()
2611            && out
2612                .cmd
2613                .flags
2614                .iter()
2615                .any(|declared| declared.name == flag.name && declared.exclusive);
2616        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2617            Some(spellings) => (
2618                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2619                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2620            ),
2621            // An environment value has no spelling to attribute it by. The declaration the
2622            // selected command has in scope is the one that answers — which is the child's
2623            // when it re-declared the flag, and the ancestor's when it did not.
2624            None => (child_exclusive, flag.exclusive && child.is_empty()),
2625        }
2626    };
2627
2628    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2629        let (child, ancestor) = exclusivity_in_play(flag);
2630        child || ancestor
2631    };
2632
2633    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2634    // command that otherwise needs an input. Companions are still diagnosed below, but an
2635    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2636    // whether it was alone or not.
2637    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2638        .filter(|flag| !clause_flag_names.contains(&flag.name))
2639        .any(|flag| {
2640            exclusive_occurrence(flag)
2641                && !overridden_flags.contains(&flag.name)
2642                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2643        });
2644    let requirements_apply = |command_index: usize| {
2645        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2646    };
2647
2648    if out.cmd.arg_required_else_help && !command_has_argv {
2649        out.errors.push(render_help_err(spec, &out.cmd, false));
2650    }
2651
2652    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2653    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2654    // the child would be here instead. The spec has carried `subcommand_required` since it was
2655    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2656    // complete invocation while usage-argv and clap both refused it.
2657    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2658        let mut names: Vec<&str> = out
2659            .cmd
2660            .subcommands
2661            .iter()
2662            // Aliases share a map entry with the name they point at; listing both would offer
2663            // the same command twice under two spellings.
2664            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2665            .map(|(name, _)| name.as_str())
2666            .collect();
2667        names.sort_unstable();
2668        out.errors.push(UsageErr::MissingSubcommand(
2669            out.cmd.name.clone(),
2670            names.join(", "),
2671        ));
2672    }
2673
2674    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2675    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2676    if !exclusive_present {
2677        for arg in out
2678            .cmds
2679            .iter()
2680            .enumerate()
2681            .filter(|(index, _)| requirements_apply(*index))
2682            .flat_map(|(_, cmd)| &cmd.args)
2683        {
2684            if out.args.contains_key(arg) {
2685                continue;
2686            }
2687            // Already reported as needing a `--`; one mistake should not yield two messages.
2688            if double_dash_violations.contains(&arg.name) {
2689                continue;
2690            }
2691            let required_if = arg.required_if.iter().any(|selector| {
2692                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2693            });
2694            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2695                selector_explicit_has_value(
2696                    &condition.selector,
2697                    &condition.value,
2698                    &out,
2699                    &overridden_flags,
2700                    custom_env,
2701                )
2702            });
2703            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2704                && arg.required_if_eq_all.iter().all(|condition| {
2705                    selector_explicit_has_value(
2706                        &condition.selector,
2707                        &condition.value,
2708                        &out,
2709                        &overridden_flags,
2710                        custom_env,
2711                    )
2712                });
2713            let unless_any = arg.required_unless.iter().any(|selector| {
2714                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2715            });
2716            let unless_all = !arg.required_unless_all.is_empty()
2717                && arg.required_unless_all.iter().all(|selector| {
2718                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2719                });
2720            let required_unless = !(unless_any
2721                || unless_all
2722                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2723            if (arg.required
2724                || required_if
2725                || required_if_eq
2726                || required_if_eq_all
2727                || required_unless)
2728                && arg.default.is_empty()
2729            {
2730                // Check if there's an env var available (custom env map takes precedence)
2731                let has_env = arg
2732                    .env
2733                    .as_ref()
2734                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2735                if !has_env {
2736                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2737                }
2738            }
2739        }
2740    }
2741
2742    // Conflicts are a question about the invocation as a whole rather than about any one
2743    // token, so they are checked here beside the requirement checks rather than at the
2744    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2745    // Its own loop: the requirement loop below skips the flags that *were* given, which
2746    // is exactly the set this needs.
2747    //
2748    // A value from the environment counts on both sides, matching what
2749    // `selector_is_explicit` says about the other flag: the question is whether a flag
2750    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2751    // would make the same pair of flags a conflict or not depending on which one
2752    // happened to be typed.
2753    for flag in unique_flags(out.available_flags.values())
2754        .filter(|flag| !clause_flag_names.contains(&flag.name))
2755    {
2756        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2757        if !given || overridden_flags.contains(&flag.name) {
2758            continue;
2759        }
2760        for other in &flag.conflicts {
2761            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2762                out.errors.push(UsageErr::InvalidFlag {
2763                    token: format!("--{}", flag.name),
2764                    reason: format!("conflicts with {other}"),
2765                    span: (0, 0).into(),
2766                    input: format!("--{} {other}", flag.name),
2767                });
2768            }
2769        }
2770        // The positive form, checked in the same pass and under the same rule: a value
2771        // from the environment satisfies a requirement, because the question is whether
2772        // the other flag has a value rather than how it got one. A flag that was
2773        // overridden away has not been given, so it cannot satisfy anything either —
2774        // which is what `selector_is_explicit` already accounts for.
2775        //
2776        // Reported as the missing flag rather than as something wrong with the flag that
2777        // named it, which is what clap says too: an unmet `requires` is a required
2778        // argument that was not provided. Named by its own name, resolved through the
2779        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2780        let owner = out
2781            .cmds
2782            .iter()
2783            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2784            .unwrap_or(out.cmds.len() - 1);
2785        if !exclusive_present && requirements_apply(owner) {
2786            for other in &flag.requires {
2787                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2788                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2789                    if other.starts_with('-') {
2790                        out.errors.push(UsageErr::MissingFlag(name));
2791                    } else {
2792                        out.errors.push(UsageErr::MissingArg(name));
2793                    }
2794                }
2795            }
2796            for condition in &flag.requires_if {
2797                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2798                    && !selector_is_satisfied(
2799                        &condition.requires,
2800                        &out,
2801                        &overridden_flags,
2802                        custom_env,
2803                    )
2804                {
2805                    let name = selector_flag_name(&condition.requires, &out)
2806                        .unwrap_or_else(|| condition.requires.clone());
2807                    out.errors.push(UsageErr::MissingFlag(name));
2808                }
2809            }
2810        }
2811    }
2812
2813    // Positionals can declare the same pairwise conflict as flags. Their selector is
2814    // the bare argument name, while a flag keeps its dashed spelling.
2815    for (command_index, arg) in out
2816        .cmds
2817        .iter()
2818        .enumerate()
2819        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2820    {
2821        let given = arg_is_explicit(arg, &out, custom_env);
2822        if !given {
2823            continue;
2824        }
2825        for other in &arg.conflicts {
2826            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2827                out.errors.push(UsageErr::InvalidFlag {
2828                    token: arg.name.clone(),
2829                    reason: format!("conflicts with {other}"),
2830                    span: (0, 0).into(),
2831                    input: format!("{} {other}", arg.name),
2832                });
2833            }
2834        }
2835        if !exclusive_present && requirements_apply(command_index) {
2836            for other in &arg.requires {
2837                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2838                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2839                    if other.starts_with('-') {
2840                        out.errors.push(UsageErr::MissingFlag(name));
2841                    } else {
2842                        out.errors.push(UsageErr::MissingArg(name));
2843                    }
2844                }
2845            }
2846        }
2847    }
2848
2849    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2850    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2851    // positionals included, which is what distinguishes it from being in a group with
2852    // every other flag.
2853    //
2854    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2855    // exclusive one is nobody saying anything, and counting it would make the exclusive
2856    // flag unusable on any command that has a default. Environment values do count, also as
2857    // `conflicts` reads them, so the spec parser and the derive agree.
2858    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
2859        .filter(|flag| !clause_flag_names.contains(&flag.name))
2860    {
2861        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2862        // but re-declared aliases can leave distinct declarations with that same name in
2863        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2864        // compare the parser's `Arc`s by identity here.
2865        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2866        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2867            continue;
2868        }
2869        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2870            .filter(|other| !clause_flag_names.contains(&other.name))
2871            .find(|other| {
2872                !Arc::ptr_eq(other, flag)
2873                    && !overridden_flags.contains(&other.name)
2874                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2875            })
2876            .map(|other| format!("--{}", other.name));
2877        let other_arg = active_args(&out.cmd).iter().find(|arg| {
2878            out.args.keys().any(|given| given.name == arg.name)
2879                || out
2880                    .clauses
2881                    .values()
2882                    .flatten()
2883                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
2884                || arg
2885                    .env
2886                    .as_ref()
2887                    .is_some_and(|env| env_contains(custom_env, env))
2888        });
2889        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2890        // exclusive flag belonging to the child itself does not conflict with the command word
2891        // needed to reach that child — so the question is not who owns the flag but whose
2892        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2893        // separated.
2894        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2895        let selected_subcommand =
2896            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2897        let other = other_flag
2898            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2899            .or(selected_subcommand);
2900        if let Some(other) = other {
2901            out.errors.push(UsageErr::InvalidFlag {
2902                token: format!("--{}", flag.name),
2903                reason: format!("must be given on its own, and {other} was given too"),
2904                span: (0, 0).into(),
2905                input: format!("--{} {other}", flag.name),
2906            });
2907        }
2908    }
2909
2910    // Groups, checked once per group rather than per flag: both questions a group asks —
2911    // how many members were given, and whether that is enough — are about the set, which
2912    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2913    //
2914    // The same "given" rule as everything else here, so a member filled from the
2915    // environment or a default counts.
2916    // Every command in the chain, not only the selected one: a group may name global
2917    // flags, which belong to an ancestor and are declared there.
2918    let mut group_errors: Vec<UsageErr> = Vec::new();
2919    for (command_index, group) in out
2920        .cmds
2921        .iter()
2922        .enumerate()
2923        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2924    {
2925        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2926        // `--file` are two spellings of one flag, and a group naming both — or naming one
2927        // flag twice — would otherwise report that flag as conflicting with itself the
2928        // moment it was given. Deduplicated rather than refused where the group is
2929        // written, because listing both spellings is redundant, not wrong.
2930        let mut given: Vec<&str> = Vec::new();
2931        let mut seen: Vec<String> = Vec::new();
2932        for selector in &group.members {
2933            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2934                continue;
2935            }
2936            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2937            if seen.contains(&name) {
2938                continue;
2939            }
2940            seen.push(name);
2941            given.push(selector.as_str());
2942        }
2943        if !group.multiple && given.len() > 1 {
2944            group_errors.push(UsageErr::InvalidFlag {
2945                token: given[1].to_string(),
2946                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2947                span: (0, 0).into(),
2948                input: format!("{} {}", given[0], given[1]),
2949            });
2950        }
2951        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2952        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2953        // exclusivity must count only what was supplied, or a defaulted member would
2954        // collide with the sibling the user actually typed.
2955        let satisfied = group
2956            .members
2957            .iter()
2958            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2959        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2960            // The members are what a user has to type, so they are in the message; the
2961            // group's name is there too, since a command with several groups would
2962            // otherwise report the same sentence twice with nothing to tell them apart.
2963            group_errors.push(UsageErr::MissingGroup {
2964                group: group.name.clone(),
2965                members: group.members.join(", "),
2966            });
2967        }
2968    }
2969    out.errors.extend(group_errors);
2970
2971    if !exclusive_present {
2972        for flag in unique_flags(out.available_flags.values())
2973            .filter(|flag| !clause_flag_names.contains(&flag.name))
2974        {
2975            let owner = out
2976                .cmds
2977                .iter()
2978                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2979                .unwrap_or(out.cmds.len() - 1);
2980            if !requirements_apply(owner) {
2981                continue;
2982            }
2983            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2984                continue;
2985            }
2986            let has_default =
2987                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2988            let has_env = flag_has_env(flag, custom_env);
2989            let required_if = flag.required_if.iter().any(|selector| {
2990                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2991            });
2992            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2993                selector_explicit_has_value(
2994                    &condition.selector,
2995                    &condition.value,
2996                    &out,
2997                    &overridden_flags,
2998                    custom_env,
2999                )
3000            });
3001            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3002                && flag.required_if_eq_all.iter().all(|condition| {
3003                    selector_explicit_has_value(
3004                        &condition.selector,
3005                        &condition.value,
3006                        &out,
3007                        &overridden_flags,
3008                        custom_env,
3009                    )
3010                });
3011            let unless_any = flag.required_unless.iter().any(|selector| {
3012                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3013            });
3014            let unless_all = !flag.required_unless_all.is_empty()
3015                && flag.required_unless_all.iter().all(|selector| {
3016                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3017                });
3018            let required_unless = !(unless_any
3019                || unless_all
3020                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3021            if (flag.required
3022                || required_if
3023                || required_if_eq
3024                || required_if_eq_all
3025                || required_unless)
3026                && !has_default
3027                && !has_env
3028            {
3029                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3030            }
3031        }
3032    }
3033
3034    // Validate var_min/var_max constraints for variadic args
3035    for (arg, value) in &out.args {
3036        if arg.var {
3037            if let ParseValue::MultiString(values) = value {
3038                if let Some(min) = arg.var_min {
3039                    if values.len() < min {
3040                        out.errors.push(UsageErr::VarArgTooFew {
3041                            name: arg.name.clone(),
3042                            min,
3043                            got: values.len(),
3044                        });
3045                    }
3046                }
3047                if let Some(max) = arg.var_max {
3048                    if values.len() > max {
3049                        out.errors.push(UsageErr::VarArgTooMany {
3050                            name: arg.name.clone(),
3051                            max,
3052                            got: values.len(),
3053                        });
3054                    }
3055                }
3056            }
3057        }
3058    }
3059
3060    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3061    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3062    // by binding once per occurrence, where the per-occurrence count is still available.
3063    for flag in unique_flags(out.available_flags.values())
3064        .filter(|flag| !clause_flag_names.contains(&flag.name))
3065    {
3066        if flag.var {
3067            let bound = match out.flags.get(flag) {
3068                Some(ParseValue::MultiString(values)) => values.len(),
3069                Some(ParseValue::MultiBool(values)) => values.len(),
3070                Some(_) => 1,
3071                None => 0,
3072            };
3073            // A partial parse deliberately leaves the final value-optional flag pending so
3074            // completion can still answer for it. It is nevertheless a real occurrence for
3075            // the repeated flag's bounds; the full parser closes it just after this phase.
3076            let pending = out
3077                .flag_awaiting_value
3078                .iter()
3079                .filter(|pending| {
3080                    Arc::ptr_eq(pending, flag)
3081                        && (pending.value_optional || pending.default_missing.is_some())
3082                })
3083                .count();
3084            let count = bound + pending;
3085            if count == 0 {
3086                continue;
3087            }
3088            if let Some(min) = flag.var_min {
3089                if count < min {
3090                    out.errors.push(UsageErr::VarFlagTooFew {
3091                        name: flag.name.clone(),
3092                        min,
3093                        got: count,
3094                    });
3095                }
3096            }
3097            if let Some(max) = flag.var_max {
3098                if count > max {
3099                    out.errors.push(UsageErr::VarFlagTooMany {
3100                        name: flag.name.clone(),
3101                        max,
3102                        got: count,
3103                    });
3104                }
3105            }
3106        }
3107    }
3108
3109    Ok((out, overridden_flags))
3110}
3111
3112fn validate_expression(
3113    name: &str,
3114    expression: Option<&str>,
3115    message: Option<&str>,
3116    parsed: &ParseValue,
3117    errors: &mut Vec<UsageErr>,
3118) {
3119    let Some(expression) = expression else {
3120        return;
3121    };
3122    #[cfg(not(feature = "validation"))]
3123    let _ = expression;
3124    let values: &[String] = match parsed {
3125        ParseValue::String(value) => std::slice::from_ref(value),
3126        ParseValue::MultiString(values) => values,
3127        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3128    };
3129    #[cfg(feature = "validation")]
3130    for value in values {
3131        let reason = match usage_validation::validate(expression, value) {
3132            Ok(true) => continue,
3133            Ok(false) => message
3134                .unwrap_or("does not satisfy the validation expression")
3135                .to_string(),
3136            Err(error) => format!("validation expression failed: {error}"),
3137        };
3138        errors.push(UsageErr::InvalidValue {
3139            name: name.to_string(),
3140            value: value.clone(),
3141            reason,
3142        });
3143        break;
3144    }
3145    #[cfg(not(feature = "validation"))]
3146    if let Some(value) = values.first() {
3147        let _ = message;
3148        errors.push(UsageErr::InvalidValue {
3149            name: name.to_string(),
3150            value: value.clone(),
3151            reason: "expression validation requires the `validation` feature".to_string(),
3152        });
3153    }
3154}
3155
3156#[cfg(all(test, not(feature = "validation")))]
3157mod optional_validation_tests {
3158    use crate::{parse, Spec};
3159
3160    #[test]
3161    fn validation_declarations_require_the_opt_in_runtime_feature() {
3162        let spec: Spec = r#"
3163name "ex"
3164bin "ex"
3165arg "<port>" validate="int(value) > 0"
3166        "#
3167        .parse()
3168        .unwrap();
3169        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3170        assert!(
3171            error
3172                .to_string()
3173                .contains("requires the `validation` feature"),
3174            "{error:?}"
3175        );
3176    }
3177}
3178
3179fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3180    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3181}
3182
3183fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3184    overrider
3185        .overrides
3186        .iter()
3187        .any(|selector| flag_matches_selector(overridden, selector))
3188}
3189
3190fn apply_prefix_flag_overrides(
3191    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3192    flag: Arc<SpecFlag>,
3193) {
3194    prefix_flags
3195        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3196}
3197
3198fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3199    prefix_flags
3200        .iter()
3201        .flat_map(|(_, words)| words.iter().cloned())
3202        .collect()
3203}
3204
3205fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3206    match custom_env {
3207        Some(env) => env.contains_key(env_var),
3208        None => std::env::var(env_var).is_ok(),
3209    }
3210}
3211
3212fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3213    flag.env_names()
3214        .any(|env_var| env_contains(custom_env, env_var))
3215}
3216
3217fn fallback_is_true(value: &str) -> bool {
3218    matches!(value, "1" | "true" | "True" | "TRUE")
3219}
3220
3221fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3222    match delimiter {
3223        Some(delimiter) => values
3224            .iter()
3225            .flat_map(|value| value.split(delimiter).map(str::to_string))
3226            .collect(),
3227        None => values.to_vec(),
3228    }
3229}
3230
3231fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3232    if let Some(min) = arg.var_min {
3233        if count < min {
3234            errors.push(UsageErr::VarArgTooFew {
3235                name: arg.name.clone(),
3236                min,
3237                got: count,
3238            });
3239        }
3240    }
3241    if let Some(max) = arg.var_max {
3242        if count > max {
3243            errors.push(UsageErr::VarArgTooMany {
3244                name: arg.name.clone(),
3245                max,
3246                got: count,
3247            });
3248        }
3249    }
3250}
3251
3252fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3253    if let Some(min) = flag.var_min {
3254        if count < min {
3255            errors.push(UsageErr::VarFlagTooFew {
3256                name: flag.name.clone(),
3257                min,
3258                got: count,
3259            });
3260        }
3261    }
3262    if let Some(max) = flag.var_max {
3263        if count > max {
3264            errors.push(UsageErr::VarFlagTooMany {
3265                name: flag.name.clone(),
3266                max,
3267                got: count,
3268            });
3269        }
3270    }
3271}
3272
3273fn validate_flag_arg_fallback_count(
3274    flag: &SpecFlag,
3275    arg: &SpecArg,
3276    count: usize,
3277    errors: &mut Vec<UsageErr>,
3278) {
3279    if let Some(min) = arg.var_min {
3280        if count < min {
3281            errors.push(UsageErr::VarFlagTooFew {
3282                name: flag.name.clone(),
3283                min,
3284                got: count,
3285            });
3286        }
3287    }
3288    if let Some(max) = arg.var_max {
3289        if count > max {
3290            errors.push(UsageErr::VarFlagTooMany {
3291                name: flag.name.clone(),
3292                max,
3293                got: count,
3294            });
3295        }
3296    }
3297}
3298
3299/// Bind a fallback the way an unconditional `default` does: one value, or several
3300/// for `var`, and choices checked the same way.
3301fn bind_flag_fallback(
3302    flag: &Arc<SpecFlag>,
3303    values: &[String],
3304    out: &mut ParseOutput,
3305    custom_env: Option<&HashMap<String, String>>,
3306    origin: ValueOrigin,
3307) -> Result<(), miette::Error> {
3308    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3309        return Ok(());
3310    };
3311    out.flags.insert(Arc::clone(flag), value);
3312    out.flag_origins
3313        .entry(Arc::clone(flag))
3314        .or_default()
3315        .push(origin);
3316    Ok(())
3317}
3318
3319fn flag_fallback_value(
3320    flag: &SpecFlag,
3321    values: &[String],
3322    errors: &mut Vec<UsageErr>,
3323    custom_env: Option<&HashMap<String, String>>,
3324) -> Result<Option<ParseValue>, miette::Error> {
3325    if values.is_empty() {
3326        return Ok(None);
3327    }
3328    if let Some(arg) = flag.arg.as_ref() {
3329        let values = split_fallback_values(values, arg.delimiter);
3330        if flag.var || arg.var {
3331            if flag.var {
3332                validate_flag_fallback_count(flag, values.len(), errors);
3333            }
3334            if arg.var {
3335                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3336            }
3337            validate_choice_values(
3338                ChoiceTarget::option(flag),
3339                &values,
3340                arg.choices.as_ref(),
3341                custom_env,
3342            )?;
3343            Ok(Some(ParseValue::MultiString(values)))
3344        } else {
3345            let value = values.into_iter().next().unwrap_or_default();
3346            validate_choice_value(
3347                ChoiceTarget::option(flag),
3348                &value,
3349                arg.choices.as_ref(),
3350                custom_env,
3351            )?;
3352            Ok(Some(ParseValue::String(value)))
3353        }
3354    } else if flag.var {
3355        validate_flag_fallback_count(flag, values.len(), errors);
3356        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3357        Ok(Some(ParseValue::MultiBool(bools)))
3358    } else {
3359        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3360    }
3361}
3362
3363/// Fill scoped flags inside the clause instances argv created.
3364///
3365/// The returned sets contain argv and environment sources only. Relationship
3366/// validation uses them to keep defaults from becoming explicit conflicts while
3367/// still allowing a fallback value to satisfy a required scoped flag.
3368fn apply_clause_flag_fallbacks(
3369    out: &mut ParseOutput,
3370    overridden_flags: &HashSet<String>,
3371    custom_env: Option<&HashMap<String, String>>,
3372) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3373    let Some(clause) = out.cmd.clause.clone() else {
3374        return Ok(None);
3375    };
3376    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3377        return Ok(None);
3378    };
3379    let positional_instances = positional_instances.clone();
3380    let mut flag_instances = out
3381        .clause_flags
3382        .shift_remove(&clause.name)
3383        .unwrap_or_default();
3384    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3385    let get_env = |key: &str| -> Option<String> {
3386        custom_env
3387            .and_then(|values| values.get(key).cloned())
3388            .or_else(|| {
3389                custom_env
3390                    .is_none()
3391                    .then(|| std::env::var(key).ok())
3392                    .flatten()
3393            })
3394    };
3395    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3396
3397    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3398        let positional = &positional_instances[instance_index];
3399        let mut explicit = scoped
3400            .keys()
3401            .map(|flag| flag.name.clone())
3402            .collect::<HashSet<_>>();
3403
3404        // Environment first so every default_if sees all explicit sources,
3405        // independent of declaration order.
3406        for declared in &clause.flags {
3407            let flag = Arc::new(declared.clone());
3408            if scoped.contains_key(&flag) {
3409                continue;
3410            }
3411            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3412                continue;
3413            };
3414            if let Some(warning) = flag_deprecation(declared) {
3415                out.warnings.push(warning);
3416            }
3417            if flag_env_is_deprecated(declared, env_name) {
3418                out.warnings
3419                    .push(Warning::env(env_name, flag_current_env(declared)));
3420            }
3421            if let Some(value) = flag_fallback_value(
3422                declared,
3423                std::slice::from_ref(&env_value),
3424                &mut out.errors,
3425                custom_env,
3426            )? {
3427                scoped.insert(flag, value);
3428                explicit.insert(declared.name.clone());
3429            }
3430        }
3431
3432        let condition_matches =
3433            |condition: &crate::SpecDefaultIf| {
3434                if let Some(flag) = clause
3435                    .flags
3436                    .iter()
3437                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3438                {
3439                    if !explicit.contains(&flag.name) {
3440                        return false;
3441                    }
3442                    return condition.when.as_ref().is_none_or(|expected| {
3443                        scoped
3444                            .iter()
3445                            .find(|(present, _)| present.name == flag.name)
3446                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3447                    });
3448                }
3449                if let Some(arg) = clause.args.iter().find(|arg| {
3450                    !condition.selector.starts_with('-') && arg.name == condition.selector
3451                }) {
3452                    return positional.get(arg).is_some_and(|value| {
3453                        condition
3454                            .when
3455                            .as_ref()
3456                            .is_none_or(|expected| parse_value_has(value, expected))
3457                    });
3458                }
3459                let command_flag = out
3460                    .available_flags
3461                    .values()
3462                    .chain(out.flags.keys())
3463                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3464                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3465                command_flag.is_some_and(|flag| {
3466                    !overridden_flags.contains(&flag.name)
3467                        && command_flag_has_explicit_source(flag, out)
3468                        && condition.when.as_ref().is_none_or(|expected| {
3469                            out.flags
3470                                .get(flag)
3471                                .is_some_and(|value| parse_value_has(value, expected))
3472                        })
3473                })
3474            };
3475
3476        let conditional = clause
3477            .flags
3478            .iter()
3479            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3480            .filter_map(|flag| {
3481                flag.default_if
3482                    .iter()
3483                    .find(|condition| condition_matches(condition))
3484                    .map(|condition| (flag.clone(), condition.value.clone()))
3485            })
3486            .collect::<Vec<_>>();
3487        for (declared, value) in conditional {
3488            if let Some(value) = flag_fallback_value(
3489                &declared,
3490                std::slice::from_ref(&value),
3491                &mut out.errors,
3492                custom_env,
3493            )? {
3494                scoped.insert(Arc::new(declared), value);
3495            }
3496        }
3497
3498        for declared in &clause.flags {
3499            let flag = Arc::new(declared.clone());
3500            if scoped.contains_key(&flag) {
3501                continue;
3502            }
3503            let values = if !declared.default.is_empty() {
3504                &declared.default
3505            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3506                &arg.default
3507            } else {
3508                continue;
3509            };
3510            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3511            {
3512                scoped.insert(flag, value);
3513            }
3514        }
3515        explicit_instances.push(explicit);
3516    }
3517
3518    out.clause_flags.insert(clause.name, flag_instances);
3519    Ok(Some(explicit_instances))
3520}
3521
3522fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3523    out.flags.contains_key(flag)
3524        && out.flag_origins.get(flag).is_none_or(|origins| {
3525            origins
3526                .iter()
3527                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3528        })
3529}
3530
3531fn default_if_condition_matches(
3532    condition: &crate::SpecDefaultIf,
3533    out: &ParseOutput,
3534    overridden_flags: &HashSet<String>,
3535    custom_env: Option<&HashMap<String, String>>,
3536) -> bool {
3537    match &condition.when {
3538        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3539        Some(when) => {
3540            let Some(flag) = out
3541                .available_flags
3542                .values()
3543                .chain(out.flags.keys())
3544                .find(|flag| flag_matches_selector(flag, &condition.selector))
3545            else {
3546                return false;
3547            };
3548            if overridden_flags.contains(&flag.name) {
3549                return false;
3550            }
3551            explicit_flag_has_value(flag, when, out, custom_env)
3552        }
3553    }
3554}
3555
3556/// Whether an explicitly supplied value of `flag` equals `expected`.
3557///
3558/// clap treats command-line and environment values as explicit for `requires_if`, but
3559/// not defaults. Keep that source distinction here instead of consulting the flag's
3560/// defaults through `selector_is_satisfied`.
3561fn explicit_flag_has_value(
3562    flag: &SpecFlag,
3563    expected: &str,
3564    out: &ParseOutput,
3565    custom_env: Option<&HashMap<String, String>>,
3566) -> bool {
3567    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3568        ParseValue::Bool(value) => value.to_string() == expected,
3569        ParseValue::String(value) => value == expected,
3570        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3571        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3572    });
3573    if out.flags.contains_key(flag) {
3574        return parsed_matches;
3575    }
3576
3577    let value = flag.env_names().find_map(|env| match custom_env {
3578        Some(values) => values.get(env).cloned(),
3579        None => std::env::var(env).ok(),
3580    });
3581    value.is_some_and(
3582        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3583            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3584            None if flag.arg.is_none() => {
3585                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3586            }
3587            None => value == expected,
3588        },
3589    )
3590}
3591
3592fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3593    match value {
3594        ParseValue::Bool(value) => value.to_string() == expected,
3595        ParseValue::String(value) => value == expected,
3596        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3597        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3598    }
3599}
3600
3601/// Clause flags share the command's spelling table so they can be recognized while parsing,
3602/// but their values and validation belong to one clause instance rather than the command.
3603fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3604    out.cmd
3605        .clause
3606        .as_ref()
3607        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3608}
3609
3610fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3611    out.cmd
3612        .clause
3613        .as_ref()?
3614        .flags
3615        .iter()
3616        .find(|flag| flag_matches_selector(flag, selector))
3617}
3618
3619fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3620    out.clause_flags
3621        .values()
3622        .flatten()
3623        .any(|instance| instance.keys().any(|present| present.name == flag.name))
3624        || out.flags.keys().any(|present| present.name == flag.name)
3625}
3626
3627fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3628    out.clause_flags.values().flatten().any(|instance| {
3629        instance
3630            .iter()
3631            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3632    }) || out
3633        .flags
3634        .iter()
3635        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3636}
3637
3638fn validate_clause_relationships(
3639    out: &mut ParseOutput,
3640    overridden_flags: &HashSet<String>,
3641    custom_env: Option<&HashMap<String, String>>,
3642    explicit_instances: Option<&[HashSet<String>]>,
3643) {
3644    let Some(clause) = out.cmd.clause.as_ref() else {
3645        return;
3646    };
3647    let Some(instances) = out.clauses.get(&clause.name) else {
3648        return;
3649    };
3650    let command_flag_is_explicit = |selector: &str| {
3651        out.available_flags
3652            .values()
3653            .chain(out.flags.keys())
3654            .filter(|flag| !is_clause_scoped_flag(out, flag))
3655            .any(|flag| {
3656                flag_matches_selector(flag, selector)
3657                    && !overridden_flags.contains(&flag.name)
3658                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3659            })
3660    };
3661    let command_flag_matches_value = |selector: &str, expected: &str| {
3662        out.available_flags
3663            .values()
3664            .chain(out.flags.keys())
3665            .filter(|flag| !is_clause_scoped_flag(out, flag))
3666            .find(|flag| flag_matches_selector(flag, selector))
3667            .is_some_and(|flag| {
3668                !overridden_flags.contains(&flag.name)
3669                    && explicit_flag_has_value(flag, expected, out, custom_env)
3670            })
3671    };
3672    let command_flag_is_satisfied = |selector: &str| {
3673        out.available_flags
3674            .values()
3675            .chain(out.flags.keys())
3676            .filter(|flag| !is_clause_scoped_flag(out, flag))
3677            .any(|flag| flag_matches_selector(flag, selector))
3678            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3679    };
3680    let mut errors = Vec::new();
3681    for (instance_index, instance) in instances.iter().enumerate() {
3682        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
3683        let scoped = out
3684            .clause_flags
3685            .get(&clause.name)
3686            .and_then(|instances| instances.get(instance_index));
3687        let scoped_flag = |selector: &str| {
3688            clause
3689                .flags
3690                .iter()
3691                .find(|flag| flag_matches_selector(flag, selector))
3692        };
3693        let scoped_value = |flag: &SpecFlag| {
3694            scoped.and_then(|values| {
3695                values
3696                    .iter()
3697                    .find(|(present, _)| present.name == flag.name)
3698                    .map(|(_, value)| value)
3699            })
3700        };
3701        let arg_is_explicit = |selector: &str| {
3702            instance
3703                .keys()
3704                .any(|arg| !selector.starts_with('-') && arg.name == selector)
3705        };
3706        let selector_is_explicit = |selector: &str| {
3707            scoped_flag(selector).is_some_and(|flag| {
3708                explicit
3709                    .map(|given| given.contains(&flag.name))
3710                    .unwrap_or_else(|| scoped_value(flag).is_some())
3711            }) || command_flag_is_explicit(selector)
3712                || arg_is_explicit(selector)
3713        };
3714        let selector_has_value = |selector: &str, expected: &str| {
3715            scoped_flag(selector)
3716                .filter(|flag| {
3717                    explicit
3718                        .map(|given| given.contains(&flag.name))
3719                        .unwrap_or_else(|| scoped_value(flag).is_some())
3720                })
3721                .and_then(scoped_value)
3722                .is_some_and(|value| parse_value_has(value, expected))
3723                || command_flag_matches_value(selector, expected)
3724                || instance.iter().any(|(arg, value)| {
3725                    !selector.starts_with('-')
3726                        && arg.name == selector
3727                        && parse_value_has(value, expected)
3728                })
3729        };
3730        let selector_is_satisfied = |selector: &str| {
3731            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
3732                || command_flag_is_satisfied(selector)
3733                || arg_is_explicit(selector)
3734        };
3735        let selector_name = |selector: &str| {
3736            scoped_flag(selector)
3737                .map(|flag| flag.name.clone())
3738                .or_else(|| selector_flag_name(selector, out))
3739                .unwrap_or_else(|| selector.to_string())
3740        };
3741
3742        for flag in &clause.flags {
3743            let value = scoped_value(flag);
3744            if value.is_none() {
3745                let required_if = flag
3746                    .required_if
3747                    .iter()
3748                    .any(|selector| selector_is_explicit(selector));
3749                let required_if_eq = flag
3750                    .required_if_eq
3751                    .iter()
3752                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
3753                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3754                    && flag
3755                        .required_if_eq_all
3756                        .iter()
3757                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
3758                let unless_any = flag
3759                    .required_unless
3760                    .iter()
3761                    .any(|selector| selector_is_explicit(selector));
3762                let unless_all = !flag.required_unless_all.is_empty()
3763                    && flag
3764                        .required_unless_all
3765                        .iter()
3766                        .all(|selector| selector_is_explicit(selector));
3767                let required_unless = !(unless_any
3768                    || unless_all
3769                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3770                if flag.required
3771                    || required_if
3772                    || required_if_eq
3773                    || required_if_eq_all
3774                    || required_unless
3775                {
3776                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
3777                }
3778                continue;
3779            }
3780
3781            let explicitly_provided = explicit
3782                .map(|given| given.contains(&flag.name))
3783                .unwrap_or(true);
3784
3785            if explicitly_provided {
3786                for other in &flag.conflicts {
3787                    if selector_is_explicit(other) {
3788                        errors.push(UsageErr::InvalidFlag {
3789                            token: format!("--{}", flag.name),
3790                            reason: format!("conflicts with {other}"),
3791                            span: (0, 0).into(),
3792                            input: format!("--{} {other}", flag.name),
3793                        });
3794                    }
3795                }
3796                for other in &flag.requires {
3797                    if !selector_is_satisfied(other) {
3798                        errors.push(UsageErr::MissingFlag(selector_name(other)));
3799                    }
3800                }
3801                for condition in &flag.requires_if {
3802                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
3803                        && !selector_is_satisfied(&condition.requires)
3804                    {
3805                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
3806                    }
3807                }
3808            }
3809            if let (true, Some(value)) = (flag.var, value) {
3810                let count = match value {
3811                    ParseValue::MultiString(values) => values.len(),
3812                    ParseValue::MultiBool(values) => values.len(),
3813                    _ => 1,
3814                };
3815                if let Some(min) = flag.var_min.filter(|min| count < *min) {
3816                    errors.push(UsageErr::VarFlagTooFew {
3817                        name: flag.name.clone(),
3818                        min,
3819                        got: count,
3820                    });
3821                }
3822                if let Some(max) = flag.var_max.filter(|max| count > *max) {
3823                    errors.push(UsageErr::VarFlagTooMany {
3824                        name: flag.name.clone(),
3825                        max,
3826                        got: count,
3827                    });
3828                }
3829            }
3830            if let (Some(arg), Some(value)) = (&flag.arg, value) {
3831                validate_expression(
3832                    &flag.name,
3833                    arg.validate.as_deref(),
3834                    arg.validate_error.as_deref(),
3835                    value,
3836                    &mut errors,
3837                );
3838            }
3839        }
3840        for arg in &clause.args {
3841            let given = instance.keys().any(|present| present.name == arg.name);
3842            if !given {
3843                let required_if = arg
3844                    .required_if
3845                    .iter()
3846                    .any(|selector| selector_is_explicit(selector));
3847                let required_if_eq = arg
3848                    .required_if_eq
3849                    .iter()
3850                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
3851                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3852                    && arg
3853                        .required_if_eq_all
3854                        .iter()
3855                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
3856                let unless_any = arg
3857                    .required_unless
3858                    .iter()
3859                    .any(|selector| selector_is_explicit(selector));
3860                let unless_all = !arg.required_unless_all.is_empty()
3861                    && arg
3862                        .required_unless_all
3863                        .iter()
3864                        .all(|selector| selector_is_explicit(selector));
3865                let required_unless = !(unless_any
3866                    || unless_all
3867                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3868                if required_if || required_if_eq || required_if_eq_all || required_unless {
3869                    errors.push(UsageErr::MissingClauseArg {
3870                        clause: clause.name.clone(),
3871                        instance: instance_index + 1,
3872                        arg: arg.name.clone(),
3873                    });
3874                }
3875                continue;
3876            }
3877            for other in &arg.conflicts {
3878                if selector_is_explicit(other) {
3879                    errors.push(UsageErr::InvalidFlag {
3880                        token: arg.name.clone(),
3881                        reason: format!("conflicts with {other}"),
3882                        span: (0, 0).into(),
3883                        input: format!("{} {other}", arg.name),
3884                    });
3885                }
3886            }
3887            for other in &arg.requires {
3888                if selector_is_satisfied(other) {
3889                    continue;
3890                }
3891                if other.starts_with('-') {
3892                    errors.push(UsageErr::MissingFlag(selector_name(other)));
3893                } else {
3894                    errors.push(UsageErr::MissingClauseArg {
3895                        clause: clause.name.clone(),
3896                        instance: instance_index + 1,
3897                        arg: other.clone(),
3898                    });
3899                }
3900            }
3901        }
3902    }
3903    out.errors.extend(errors);
3904}
3905
3906fn selector_explicit_has_value(
3907    selector: &str,
3908    expected: &str,
3909    out: &ParseOutput,
3910    overridden_flags: &HashSet<String>,
3911    custom_env: Option<&HashMap<String, String>>,
3912) -> bool {
3913    if let Some(flag) = selected_clause_flag(out, selector) {
3914        return clause_flag_has_value(out, flag, expected);
3915    }
3916    if let Some(flag) = out
3917        .available_flags
3918        .values()
3919        .chain(out.flags.keys())
3920        .filter(|flag| !is_clause_scoped_flag(out, flag))
3921        .find(|flag| flag_matches_selector(flag, selector))
3922    {
3923        return !overridden_flags.contains(&flag.name)
3924            && explicit_flag_has_value(flag, expected, out, custom_env);
3925    }
3926    let Some(arg) = selector_arg(selector, out) else {
3927        return false;
3928    };
3929    let parsed = out
3930        .args
3931        .iter()
3932        .find(|(given, _)| given.name == arg.name)
3933        .map(|(_, value)| value)
3934        .or_else(|| {
3935            out.clauses.values().flatten().find_map(|instance| {
3936                instance
3937                    .iter()
3938                    .find(|(given, _)| given.name == arg.name)
3939                    .map(|(_, value)| value)
3940            })
3941        });
3942    if let Some(value) = parsed {
3943        return match value {
3944            ParseValue::String(value) => value == expected,
3945            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3946            ParseValue::Bool(value) => value.to_string() == expected,
3947            ParseValue::MultiBool(values) => {
3948                values.iter().any(|value| value.to_string() == expected)
3949            }
3950        };
3951    }
3952    let value = arg.env_names().find_map(|env| match custom_env {
3953        Some(values) => values.get(env).cloned(),
3954        None => std::env::var(env).ok(),
3955    });
3956    value.is_some_and(|value| match arg.delimiter {
3957        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3958        None => value == expected,
3959    })
3960}
3961
3962fn selector_is_explicit(
3963    selector: &str,
3964    out: &ParseOutput,
3965    overridden_flags: &HashSet<String>,
3966    custom_env: Option<&HashMap<String, String>>,
3967) -> bool {
3968    let scoped_flag_is_explicit =
3969        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
3970    let flag_is_explicit = out
3971        .available_flags
3972        .values()
3973        .chain(out.flags.keys())
3974        .filter(|flag| !is_clause_scoped_flag(out, flag))
3975        .any(|flag| {
3976            flag_matches_selector(flag, selector)
3977                && !overridden_flags.contains(&flag.name)
3978                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3979        });
3980    scoped_flag_is_explicit
3981        || flag_is_explicit
3982        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3983}
3984
3985/// The name of the flag a selector points at, for an error that has to name it.
3986///
3987/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3988/// about a flag that is *missing* has to say which one, and the selector may be a short
3989/// form or an alias rather than the name.
3990fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3991    if let Some(flag) = selected_clause_flag(out, selector) {
3992        return Some(flag.name.clone());
3993    }
3994    out.available_flags
3995        .values()
3996        .chain(out.flags.keys())
3997        .filter(|flag| !is_clause_scoped_flag(out, flag))
3998        .find(|flag| flag_matches_selector(flag, selector))
3999        .map(|flag| flag.name.clone())
4000        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4001}
4002
4003/// Whether a selector's flag ended up with a value, however it got one.
4004///
4005/// The rule for a *positive* relationship, and the difference from
4006/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4007/// exclusivity — has to count only what was given, or a flag with a default would
4008/// conflict with everything and no command line would parse. A positive one asks whether
4009/// the flag it names has a value, and a default is a value: that is already how plain
4010/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4011/// otherwise would have made the same flag missing here and present ten lines below.
4012fn selector_is_satisfied(
4013    selector: &str,
4014    out: &ParseOutput,
4015    overridden_flags: &HashSet<String>,
4016    custom_env: Option<&HashMap<String, String>>,
4017) -> bool {
4018    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4019        return true;
4020    }
4021    let flag_is_satisfied = out
4022        .available_flags
4023        .values()
4024        .chain(out.flags.keys())
4025        .filter(|flag| !is_clause_scoped_flag(out, flag))
4026        .filter(|flag| flag_matches_selector(flag, selector))
4027        .any(|flag| {
4028            !overridden_flags.contains(&flag.name)
4029                && (!flag.default.is_empty()
4030                    || flag.arg.iter().any(|a| !a.default.is_empty())
4031                    || flag.default_if.iter().any(|condition| {
4032                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4033                    }))
4034        });
4035    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4036}
4037
4038fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4039    // Bare words are positional selectors. Keep accepting a flag's internal name above
4040    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4041    if selector.starts_with('-') {
4042        return None;
4043    }
4044    out.cmds
4045        .iter()
4046        .flat_map(active_args)
4047        .find(|arg| arg.name == selector)
4048}
4049
4050fn arg_is_explicit(
4051    arg: &SpecArg,
4052    out: &ParseOutput,
4053    custom_env: Option<&HashMap<String, String>>,
4054) -> bool {
4055    out.args.keys().any(|given| given.name == arg.name)
4056        || out
4057            .clauses
4058            .values()
4059            .flatten()
4060            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4061        || arg
4062            .env
4063            .as_ref()
4064            .is_some_and(|env| env_contains(custom_env, env))
4065}
4066
4067fn apply_flag_overrides(
4068    flag: &Arc<SpecFlag>,
4069    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4070    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4071    pending_flags: &mut Vec<Arc<SpecFlag>>,
4072    overridden_flags: &mut HashSet<String>,
4073    // The reportable half of the same fact: which flag did the overriding. The set above
4074    // only stops a default or an environment value restoring what was overridden, and
4075    // "`--quiet` is unset despite its default" has no answer without the name.
4076    attributed: &mut BTreeMap<String, String>,
4077) {
4078    let overridden_names: HashSet<String> = available_flags
4079        .values()
4080        .chain(parsed_flags.keys())
4081        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4082        .map(|other| other.name.clone())
4083        .collect();
4084
4085    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4086    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4087    for name in &overridden_names {
4088        attributed.insert(name.clone(), flag.name.clone());
4089    }
4090    overridden_flags.extend(overridden_names);
4091    // An explicit occurrence always restores this flag, including self-overrides.
4092    overridden_flags.remove(&flag.name);
4093    attributed.remove(&flag.name);
4094}
4095
4096#[cfg(feature = "cli-help")]
4097fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
4098    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
4099}
4100
4101#[cfg(feature = "cli-help")]
4102fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
4103    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
4104        if !out.is_empty() {
4105            out.push('\n');
4106        }
4107        out.push_str(&docs::cli::render_help(spec, cmd, true));
4108        let mut children: Vec<_> = cmd
4109            .subcommands
4110            .values()
4111            .filter(|child| !child.hide)
4112            .collect();
4113        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4114        for child in children {
4115            append(out, spec, child);
4116        }
4117    }
4118
4119    let mut out = String::new();
4120    append(&mut out, spec, cmd);
4121    UsageErr::Help(out)
4122}
4123
4124#[cfg(not(feature = "cli-help"))]
4125fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
4126    UsageErr::Help("help".to_string())
4127}
4128
4129#[cfg(not(feature = "cli-help"))]
4130fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
4131    UsageErr::Help("help".to_string())
4132}
4133
4134/// The version to answer with. `--version` prefers the long text and `-V` the concise
4135/// one, each falling back to the other when only one is declared.
4136fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4137    let value = if long {
4138        spec.long_version.as_ref().or(spec.version.as_ref())
4139    } else {
4140        spec.version.as_ref().or(spec.long_version.as_ref())
4141    };
4142    UsageErr::Version(value.cloned().unwrap_or_default())
4143}
4144
4145fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
4146    use crate::SpecFlagAction;
4147    match flag.action {
4148        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
4149        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
4150        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
4151        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
4152        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4153        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4154    }
4155}
4156
4157/// Report a required flag value that was displaced by a later option.
4158fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4159    let token = flag
4160        .long
4161        .first()
4162        .map(|long| format!("--{long}"))
4163        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4164        .unwrap_or_else(|| flag.name.clone());
4165    UsageErr::InvalidFlag {
4166        token: token.clone(),
4167        reason: "requires an argument".to_string(),
4168        span: (0, 0).into(),
4169        input: format!("{token} {following}"),
4170    }
4171}
4172
4173#[derive(Copy, Clone)]
4174struct ChoiceTarget<'a> {
4175    kind: &'a str,
4176    name: &'a str,
4177}
4178
4179impl<'a> ChoiceTarget<'a> {
4180    fn arg(arg: &'a SpecArg) -> Self {
4181        Self {
4182            kind: "arg",
4183            name: &arg.name,
4184        }
4185    }
4186
4187    fn option(flag: &'a SpecFlag) -> Self {
4188        Self {
4189            kind: "option",
4190            name: &flag.name,
4191        }
4192    }
4193}
4194
4195/// Whether every letter of a short token names a flag in scope.
4196///
4197/// Scanning stops at the first letter whose flag takes a value, because everything
4198/// after it is that value rather than more letters.
4199fn short_bundle_is_known(
4200    spec: &Spec,
4201    cmds: &[SpecCommand],
4202    available: &BTreeMap<String, Arc<SpecFlag>>,
4203    token: &str,
4204) -> bool {
4205    for c in token.chars().skip(1) {
4206        match available.get(&format!("-{c}")) {
4207            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4208            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4209            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4210            // clap all answer it with help.
4211            None if supplied_short(spec, cmds, c).is_some() => {}
4212            None => return false,
4213            Some(f) if f.arg.is_some() => return true,
4214            Some(_) => {}
4215        }
4216    }
4217    true
4218}
4219
4220/// The response `-h` or `-V` produces where nothing declares that letter.
4221///
4222/// The letter form of the flags the parser supplies rather than a spec declaring them,
4223/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4224/// about here one letter at a time, because a bundle is read one letter at a time.
4225///
4226/// Always the short response: `-h` is short help however many letters share its token,
4227/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4228/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4229fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
4230    let cmd = cmds.last()?;
4231    match letter {
4232        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
4233        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4234        _ => None,
4235    }
4236}
4237
4238/// Refuse a flag-like token that named nothing, if this command asked for that.
4239///
4240/// Called from the flag branches, where the lookup has just failed and nothing from
4241/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4242/// than after setting `-a`.
4243fn reject_unknown_flag_if_asked(
4244    spec: &Spec,
4245    path: &[SpecCommand],
4246    token: &str,
4247) -> Result<(), UsageErr> {
4248    // A lone `-` is a value by convention. A negative number reaches this only
4249    // when no pending value opted into the narrower exception.
4250    if !is_flag_like(token) {
4251        return Ok(());
4252    }
4253    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4254        return Ok(());
4255    }
4256    Err(UsageErr::InvalidFlag {
4257        token: token.to_string(),
4258        reason: "no such flag".to_string(),
4259        span: (0, 0).into(),
4260        input: token.to_string(),
4261    })
4262}
4263
4264/// Whether a flag-like token that matches nothing is a value or an error, here.
4265///
4266/// The nearest enclosing command that stated a preference wins, then the spec,
4267/// then the default. Inherited, unlike `effect`: it describes how a command line
4268/// is read, and a CLI that forwards options tends to forward them at every level.
4269fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4270    path.iter()
4271        .rev()
4272        .find_map(|cmd| cmd.unknown_flags)
4273        .or(spec.unknown_flags)
4274        .unwrap_or_default()
4275}
4276
4277/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4278/// ones.
4279///
4280/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4281/// a field may make the narrower negative-number exception.
4282fn is_flag_like(token: &str) -> bool {
4283    match token.strip_prefix('-') {
4284        None | Some("") => false,
4285        Some(_) => true,
4286    }
4287}
4288
4289fn is_negative_number(token: &str) -> bool {
4290    token.strip_prefix('-').is_some_and(is_number)
4291}
4292
4293/// Whether a flag may claim its following token as a detached value.
4294///
4295/// A required value keeps the historical negative-number exception. A value
4296/// that may be omitted needs an explicit opt-in to distinguish a negative value
4297/// from the flag's bare form.
4298fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4299    !flag.require_equals
4300        && (!is_flag_like(token)
4301            || flag.allow_hyphen_values()
4302            || (is_negative_number(token)
4303                && (flag
4304                    .arg
4305                    .as_ref()
4306                    .is_some_and(|arg| arg.allow_negative_numbers)
4307                    || (flag.default_missing.is_none() && !flag.value_optional))))
4308}
4309
4310fn record_scalar_flag_occurrence(
4311    cmds: &[SpecCommand],
4312    flag: &Arc<SpecFlag>,
4313    command_level: usize,
4314    bool_value: Option<bool>,
4315    occurrences: &mut HashMap<(usize, usize), u8>,
4316    errors: &mut Vec<UsageErr>,
4317) {
4318    let strict = cmds.get(command_level).is_some_and(|cmd| {
4319        !cmd.args_override_self
4320            || cmd
4321                .clause
4322                .as_ref()
4323                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4324    });
4325    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4326    if !strict || flag.count || collects_values {
4327        return;
4328    }
4329
4330    let bit = match bool_value {
4331        Some(false) if flag.negate.is_some() => 0b10,
4332        _ => 0b01,
4333    };
4334    let key = (Arc::as_ptr(flag) as usize, command_level);
4335    let seen = occurrences.entry(key).or_default();
4336    if *seen & bit != 0 {
4337        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4338    }
4339    *seen |= bit;
4340}
4341
4342/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4343/// external command.
4344///
4345/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4346/// so it was never a candidate to *select* anything either. usage-argv uses the
4347/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4348/// treated every token that `starts_with('-')` as a flag.
4349fn is_command_word(token: &str) -> bool {
4350    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4351}
4352
4353/// Whether an otherwise numeric-looking token is an exact declared short flag.
4354///
4355/// This check belongs in both parse phases: phase 1 must skip the flag while it
4356/// searches for a later subcommand, and phase 2 must bind it instead of offering
4357/// it to an `allow_negative_numbers` positional.
4358fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4359    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4360}
4361
4362/// Whether an unmatched word belongs to the root's default command.
4363///
4364/// Ordinary words always do. A negative number only does when the default command's
4365/// first positional explicitly accepts one; otherwise it stays at the root, matching
4366/// usage-argv and generated Go.
4367fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4368    !is_negative_number(token)
4369        || cmd
4370            .find_subcommand(default_name)
4371            .and_then(|default| default.args.first())
4372            .is_some_and(|arg| arg.allow_negative_numbers)
4373}
4374
4375/// Digits, at most one `.`, and an optional exponent.
4376///
4377/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4378/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4379/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4380/// two disagreed about `-1e5` when one used a float parse and the other did not.
4381fn is_number(rest: &str) -> bool {
4382    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4383        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4384        None => (rest, None),
4385    };
4386
4387    let mut seen_digit = false;
4388    let mut seen_dot = false;
4389    for c in mantissa.chars() {
4390        match c {
4391            '0'..='9' => seen_digit = true,
4392            '.' if !seen_dot => seen_dot = true,
4393            _ => return false,
4394        }
4395    }
4396    if !seen_digit {
4397        return false;
4398    }
4399
4400    match exponent {
4401        None => true,
4402        Some(exp) => {
4403            let digits = exp
4404                .strip_prefix('+')
4405                .or_else(|| exp.strip_prefix('-'))
4406                .unwrap_or(exp);
4407            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4408        }
4409    }
4410}
4411
4412/// Bind one value to the flag waiting for it, and let a variadic argument go on
4413/// collecting from the words that follow.
4414///
4415/// Every route to a flag's value comes through here — the following word, the text
4416/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4417/// like — so that all three agree on how many values the flag ends up with.
4418#[allow(clippy::too_many_arguments)]
4419fn bind_pending_flag_value(
4420    spec: &Spec,
4421    cmd: &SpecCommand,
4422    errors: &mut Vec<UsageErr>,
4423    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4424    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4425    word: &mut String,
4426    input: &mut VecDeque<Token>,
4427    custom_env: Option<&HashMap<String, String>>,
4428    trace: &mut Trace,
4429    // Which token supplied `word`, and whether it rode along on the flag's own token
4430    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4431    // their own positions and are recorded where they are read.
4432    argv: usize,
4433    attached: bool,
4434) -> miette::Result<bool> {
4435    // Held before the drain pops it, along with what the flag is already carrying: a
4436    // `var_max` bounds the values this occurrence takes, not the list they are appended
4437    // to, so a second `--include` starts counting again.
4438    let collecting = flag_awaiting_value
4439        .last()
4440        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4441        .cloned()
4442        .map(|flag| {
4443            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4444            (flag, carried)
4445        });
4446    let mut bound = vec![];
4447    let refused = drain_pending_flag_values(
4448        spec,
4449        cmd,
4450        errors,
4451        flags,
4452        flag_awaiting_value,
4453        word,
4454        custom_env,
4455        &mut bound,
4456    )?;
4457    for (flag, values) in bound {
4458        trace.record(
4459            argv,
4460            TokenRole::Value {
4461                flag,
4462                values,
4463                attached,
4464            },
4465        );
4466    }
4467    if refused {
4468        return Ok(true);
4469    }
4470    let Some((flag, carried)) = collecting else {
4471        return Ok(false);
4472    };
4473    collect_variadic_flag_values(
4474        spec,
4475        cmd,
4476        errors,
4477        flags,
4478        flag_awaiting_value,
4479        &flag,
4480        carried,
4481        input,
4482        custom_env,
4483        trace,
4484    )
4485}
4486
4487/// Keep feeding a flag whose argument is variadic from the words that follow it.
4488///
4489/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4490/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4491/// This is greedy by design — a command declaring both such a flag and positionals will
4492/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4493///
4494/// `carried` is what the flag already held when this occurrence began, so the bound
4495/// counts this run rather than everything the flag has collected across the command
4496/// line. Each value goes through the same drain as the first, so choices are checked
4497/// and the value lands in the same list rather than by a second route that could
4498/// disagree.
4499#[allow(clippy::too_many_arguments)]
4500fn collect_variadic_flag_values(
4501    spec: &Spec,
4502    cmd: &SpecCommand,
4503    errors: &mut Vec<UsageErr>,
4504    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4505    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4506    flag: &Arc<SpecFlag>,
4507    carried: usize,
4508    input: &mut VecDeque<Token>,
4509    custom_env: Option<&HashMap<String, String>>,
4510    trace: &mut Trace,
4511) -> miette::Result<bool> {
4512    let max = flag
4513        .arg
4514        .as_ref()
4515        .and_then(|arg| arg.var_max)
4516        .unwrap_or(usize::MAX);
4517    while flags
4518        .get(flag)
4519        .map(value_count)
4520        .unwrap_or(0)
4521        .saturating_sub(carried)
4522        < max
4523    {
4524        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4525            break;
4526        };
4527        if flag
4528            .arg
4529            .as_ref()
4530            .and_then(|arg| arg.value_terminator.as_deref())
4531            == Some(next)
4532        {
4533            let terminator = input.pop_front().unwrap();
4534            trace.record(
4535                terminator.argv,
4536                TokenRole::ValueTerminator {
4537                    ends: flag.name.clone(),
4538                },
4539            );
4540            break;
4541        }
4542        // The separator is left where it is: stopping here hands it to the arm that
4543        // knows what it means, rather than reading it as one more value.
4544        if next == "--"
4545            || (is_flag_like(next)
4546                && !(flag
4547                    .arg
4548                    .as_ref()
4549                    .is_some_and(|arg| arg.allow_negative_numbers)
4550                    && is_negative_number(next)))
4551        {
4552            break;
4553        }
4554        let taken = input.pop_front().unwrap();
4555        let argv = taken.argv;
4556        let mut word = taken.word;
4557        flag_awaiting_value.push(Arc::clone(flag));
4558        let mut bound = vec![];
4559        let refused = drain_pending_flag_values(
4560            spec,
4561            cmd,
4562            errors,
4563            flags,
4564            flag_awaiting_value,
4565            &mut word,
4566            custom_env,
4567            &mut bound,
4568        )?;
4569        for (flag, values) in bound {
4570            // A later word of the same occurrence is its own token, and never attached:
4571            // only the first value can ride along on the flag.
4572            trace.record(
4573                argv,
4574                TokenRole::Value {
4575                    flag,
4576                    values,
4577                    attached: false,
4578                },
4579            );
4580        }
4581        if refused {
4582            return Ok(true);
4583        }
4584    }
4585    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4586    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4587    // several values, so the run can end up *past* the bound rather than on it, and stopping
4588    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4589    // case — three values out of the one word the loop was entitled to take.
4590    //
4591    // Counted against `carried` like the loop itself, so this stays a statement about the
4592    // occurrence rather than about the list the occurrences build up.
4593    let taken = flags
4594        .get(flag)
4595        .map(value_count)
4596        .unwrap_or(0)
4597        .saturating_sub(carried);
4598    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4599        if taken < min {
4600            errors.push(UsageErr::VarFlagTooFew {
4601                name: flag.name.clone(),
4602                min,
4603                got: taken,
4604            });
4605        }
4606    }
4607    if taken > max {
4608        errors.push(UsageErr::VarFlagTooMany {
4609            name: flag.name.clone(),
4610            max,
4611            got: taken,
4612        });
4613    }
4614    Ok(false)
4615}
4616
4617/// How many values a flag is holding, for a bound that counts them.
4618fn value_count(value: &ParseValue) -> usize {
4619    match value {
4620        ParseValue::MultiString(values) => values.len(),
4621        ParseValue::MultiBool(values) => values.len(),
4622        _ => 1,
4623    }
4624}
4625
4626/// Finish a value-optional flag that was given with no value.
4627///
4628/// Returns whether anything was bound. Completions keep the flag waiting — a
4629/// half-typed `--color ` is a question about the value — so this is asked only
4630/// once a full parse has decided the value is not coming, or once the next token
4631/// has made that decision.
4632///
4633/// The missing string is a real value: if the flag names `choices`, it has to
4634/// be one of them, the same way an env var or a `default` is checked. Binding
4635/// first and failing later would leave the flag set to a value the spec forbids.
4636fn try_bind_default_missing(
4637    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4638    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4639    custom_env: Option<&HashMap<String, String>>,
4640    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4641) -> miette::Result<bool> {
4642    let Some(flag) = flag_awaiting_value.last() else {
4643        return Ok(false);
4644    };
4645    let value = match flag.default_missing.clone() {
4646        Some(value) => value,
4647        None if flag.value_optional => {
4648            let flag = flag_awaiting_value.pop().unwrap();
4649            // Presence in the map distinguishes this from an absent flag; an
4650            // empty collection distinguishes it from an explicitly empty
4651            // `--flag=` string without inventing a sentinel value.
4652            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4653            origins
4654                .entry(Arc::clone(&flag))
4655                .or_default()
4656                .push(ValueOrigin::DefaultMissing);
4657            if flag.var {
4658                // A repeated bare occurrence is still an occurrence. The string collection
4659                // uses an empty value for it, just as the concrete `default_missing` path
4660                // pushes one value per occurrence; otherwise bounds and consumers silently
4661                // lose every bare repeat after the first.
4662                flags
4663                    .entry(flag)
4664                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4665                    .try_as_multi_string_mut()
4666                    .unwrap()
4667                    .push(String::new());
4668            } else if variadic_value {
4669                // A variadic occurrence stays pending after each value. Reaching the next
4670                // flag (or EOF) closes that same occurrence; it must not erase what it took.
4671                flags
4672                    .entry(flag)
4673                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4674            } else {
4675                // A scalar pending here is a new bare occurrence. The normal permissive
4676                // repeat policy makes the later occurrence a correction, including a
4677                // correction from an explicit value back to the bare tri-state.
4678                flags.insert(flag, ParseValue::MultiString(Vec::new()));
4679            }
4680            return Ok(true);
4681        }
4682        None => return Ok(false),
4683    };
4684    if let Some(arg) = flag.arg.as_ref() {
4685        validate_choice_value(
4686            ChoiceTarget::option(flag),
4687            &value,
4688            arg.choices.as_ref(),
4689            custom_env,
4690        )?;
4691    }
4692    let flag = flag_awaiting_value.pop().unwrap();
4693    origins
4694        .entry(Arc::clone(&flag))
4695        .or_default()
4696        .push(ValueOrigin::DefaultMissing);
4697    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4698    if collecting {
4699        let arr = flags
4700            .entry(flag)
4701            .or_insert_with(|| ParseValue::MultiString(vec![]))
4702            .try_as_multi_string_mut()
4703            .unwrap();
4704        arr.push(value);
4705    } else {
4706        flags.insert(flag, ParseValue::String(value));
4707    }
4708    Ok(true)
4709}
4710
4711/// `bound` collects what each drained flag took, in the order it took it. The values are
4712/// the word after any `delimiter` split, which is the only place that split is known: by the
4713/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
4714/// second occurrence has appended to the same list.
4715#[allow(clippy::too_many_arguments)]
4716fn drain_pending_flag_values(
4717    spec: &Spec,
4718    cmd: &SpecCommand,
4719    errors: &mut Vec<UsageErr>,
4720    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4721    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4722    word: &mut String,
4723    custom_env: Option<&HashMap<String, String>>,
4724    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
4725) -> miette::Result<bool> {
4726    while let Some(flag) = flag_awaiting_value.pop() {
4727        let arg = flag.arg.as_ref().unwrap();
4728        // Split before anything judges the word, because after the split it is no longer
4729        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
4730        // Judging first would reject the whole word against a list neither half is on.
4731        let parts: Vec<String> = match arg.delimiter {
4732            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
4733            None => vec![std::mem::take(word)],
4734        };
4735        for part in &parts {
4736            if validate_choices(
4737                spec,
4738                cmd,
4739                errors,
4740                ChoiceTarget::option(&flag),
4741                part,
4742                arg.choices.as_ref(),
4743                custom_env,
4744            )? {
4745                return Ok(true);
4746            }
4747        }
4748        word.clear();
4749        bound.push((Arc::clone(&flag), parts.clone()));
4750        // Two ways to hold several values, and both record a list: a `var` flag
4751        // collects one per occurrence, a variadic argument collects several from one.
4752        if flag.var || arg.var {
4753            let arr = flags
4754                .entry(flag)
4755                .or_insert_with(|| ParseValue::MultiString(vec![]))
4756                .try_as_multi_string_mut()
4757                .unwrap();
4758            arr.extend(parts);
4759        } else {
4760            // Nowhere for a second value to go, so the word stands as it was typed. A
4761            // delimiter on a flag that takes one value is refused where it is written.
4762            flags.insert(
4763                flag,
4764                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
4765            );
4766        }
4767    }
4768    Ok(false)
4769}
4770
4771fn choice_error(
4772    target: ChoiceTarget<'_>,
4773    value: &str,
4774    choices: Option<&SpecChoices>,
4775    custom_env: Option<&HashMap<String, String>>,
4776) -> Option<String> {
4777    let choices = choices?;
4778    if !choices.strict {
4779        return None;
4780    }
4781    let values = choices.values_with_env(custom_env);
4782    if choices.matches_with_env(value, custom_env) {
4783        return None;
4784    }
4785    if let Some(env) = choices.env() {
4786        if values.is_empty() {
4787            return Some(format!(
4788                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
4789                target.kind, target.name,
4790            ));
4791        }
4792    }
4793    Some(format!(
4794        "Invalid choice for {} {}: {value}, expected one of {}",
4795        target.kind,
4796        target.name,
4797        values.join(", ")
4798    ))
4799}
4800
4801fn validate_choices(
4802    spec: &Spec,
4803    cmd: &SpecCommand,
4804    errors: &mut Vec<UsageErr>,
4805    target: ChoiceTarget<'_>,
4806    value: &str,
4807    choices: Option<&SpecChoices>,
4808    custom_env: Option<&HashMap<String, String>>,
4809) -> miette::Result<bool> {
4810    if is_help_arg(spec, cmd, value)
4811        && choices
4812            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
4813    {
4814        errors.push(render_help_err(spec, cmd, value.len() > 2));
4815        return Ok(true);
4816    }
4817
4818    if let Some(err) = choice_error(target, value, choices, custom_env) {
4819        bail!("{err}");
4820    }
4821    Ok(false)
4822}
4823
4824fn validate_choice_value(
4825    target: ChoiceTarget<'_>,
4826    value: &str,
4827    choices: Option<&SpecChoices>,
4828    custom_env: Option<&HashMap<String, String>>,
4829) -> miette::Result<()> {
4830    if let Some(err) = choice_error(target, value, choices, custom_env) {
4831        bail!("{err}");
4832    }
4833    Ok(())
4834}
4835
4836fn validate_choice_values(
4837    target: ChoiceTarget<'_>,
4838    values: &[String],
4839    choices: Option<&SpecChoices>,
4840    custom_env: Option<&HashMap<String, String>>,
4841) -> miette::Result<()> {
4842    for value in values {
4843        validate_choice_value(target, value, choices, custom_env)?;
4844    }
4845    Ok(())
4846}
4847
4848/// Everything a parse records about where it stopped: the positional cursor, so callers that
4849/// do not re-run the parse — completions, above all — agree with it, and the token trace.
4850///
4851/// Every exit from the binding phase comes through here, which is what makes it the right
4852/// place to close the trace: whatever is still queued was never read, and saying so is more
4853/// useful than leaving those words out of the report entirely.
4854fn record_stop(
4855    out: &mut ParseOutput,
4856    next_arg_idx: usize,
4857    seen_double_dash: bool,
4858    trace: &mut Trace,
4859    unread: &VecDeque<Token>,
4860) {
4861    out.next_arg = active_args(&out.cmd)
4862        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
4863        .cloned()
4864        .map(Arc::new);
4865    out.double_dash_seen = seen_double_dash;
4866    finalize_current_clause(out);
4867    trace.close(unread);
4868    out.tokens = std::mem::take(&mut trace.tokens);
4869}
4870
4871fn finalize_current_clause(out: &mut ParseOutput) {
4872    let Some(clause) = out.cmd.clause.as_ref() else {
4873        return;
4874    };
4875    let has_scoped_flags = clause
4876        .flags
4877        .iter()
4878        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
4879    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
4880        return;
4881    }
4882    out.clauses
4883        .entry(clause.name.clone())
4884        .or_default()
4885        .push(std::mem::take(&mut out.args));
4886    let mut flags = IndexMap::new();
4887    for clause_flag in &clause.flags {
4888        let key = Arc::new(clause_flag.clone());
4889        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
4890            flags.insert(flag, value);
4891        }
4892    }
4893    out.clause_flags
4894        .entry(clause.name.clone())
4895        .or_default()
4896        .push(flags);
4897}
4898
4899fn restore_current_clause(out: &mut ParseOutput) {
4900    let Some(clause) = out.cmd.clause.as_ref() else {
4901        return;
4902    };
4903    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
4904        out.args = current;
4905    }
4906    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
4907        out.flags.extend(current);
4908    }
4909}
4910
4911fn reset_clause_scalar_occurrences(
4912    out: &ParseOutput,
4913    occurrences: &mut HashMap<(usize, usize), u8>,
4914) {
4915    let Some(clause) = out.cmd.clause.as_ref() else {
4916        return;
4917    };
4918    let names = clause
4919        .flags
4920        .iter()
4921        .map(|flag| flag.name.as_str())
4922        .collect::<HashSet<_>>();
4923    let pointers = unique_flags(out.available_flags.values())
4924        .filter(|flag| names.contains(flag.name.as_str()))
4925        .map(|flag| Arc::as_ptr(flag) as usize)
4926        .collect::<HashSet<_>>();
4927    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
4928}
4929
4930fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
4931    while active_args(cmd)
4932        .get(idx)
4933        .is_some_and(|arg| arg.sigil.is_some())
4934    {
4935        idx += 1;
4936    }
4937    idx
4938}
4939
4940fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
4941    cmd.clause
4942        .as_ref()
4943        .map(|clause| clause.args.as_slice())
4944        .unwrap_or(cmd.args.as_slice())
4945}
4946
4947fn match_sigil_arg<'a>(
4948    cmd: &'a SpecCommand,
4949    word: &'a str,
4950) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4951    cmd.args
4952        .iter()
4953        .filter_map(|arg| {
4954            let sigil = arg.sigil.as_deref()?;
4955            let value = word.strip_prefix(sigil)?;
4956            Some((arg, sigil, value))
4957        })
4958        .max_by_key(|(_, sigil, _)| sigil.len())
4959}
4960
4961fn match_sigil_arg_chain<'a>(
4962    cmds: &'a [SpecCommand],
4963    word: &'a str,
4964) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4965    cmds.iter()
4966        .filter_map(|cmd| match_sigil_arg(cmd, word))
4967        .max_by_key(|(_, sigil, _)| sigil.len())
4968}
4969
4970/// Record that `arg` was handed a word before the `--` it requires.
4971///
4972/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
4973/// message is emitted only the first time each arg is seen. The set is also what suppresses the
4974/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
4975/// end of the parse.
4976fn report_double_dash_violation(
4977    arg: &SpecArg,
4978    errors: &mut Vec<UsageErr>,
4979    violations: &mut HashSet<String>,
4980) {
4981    if violations.insert(arg.name.clone()) {
4982        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4983    }
4984}
4985
4986/// `--version` and `-V`, which the parser supplies where the spec declares a version.
4987///
4988/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
4989/// command tables — both of which accepted these spellings while this parser called them
4990/// unknown words. The help page has always listed `-V, --version` under the same
4991/// condition, and said in as many words that it did so "only where a version is
4992/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
4993/// page advertising a flag the parse refused.
4994///
4995/// The root only, because that is where the page lists it: `version` is a property of
4996/// the program, and a subcommand answering with the program's version is a claim no spec
4997/// made. A declared flag wins by arriving first — every scan consults this only after
4998/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
4999/// keeps `--version` supplied beside it.
5000fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5001    (spec.version.is_some() || spec.long_version.is_some())
5002        && cmds.len() == 1
5003        && !spec.cmd.disable_version_flag
5004        && (w == "--version" || w == "-V")
5005}
5006
5007fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5008    spec.disable_help != Some(true)
5009        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5010            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5011}
5012
5013impl ParseOutput {
5014    pub fn as_env(&self) -> BTreeMap<String, String> {
5015        let mut env = BTreeMap::new();
5016        for (flag, val) in &self.flags {
5017            let key = format!("usage_{}", crate::case::snake(&flag.name));
5018            let val = match val {
5019                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5020                ParseValue::String(s) => s.clone(),
5021                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5022                ParseValue::MultiString(s) => crate::shell_words::join(s),
5023            };
5024            env.insert(key, val);
5025        }
5026        for (arg, val) in &self.args {
5027            let key = format!("usage_{}", crate::case::snake(&arg.name));
5028            env.insert(key, val.to_string());
5029        }
5030        env
5031    }
5032}
5033
5034impl Display for ParseValue {
5035    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5036        match self {
5037            ParseValue::Bool(b) => write!(f, "{b}"),
5038            ParseValue::String(s) => write!(f, "{s}"),
5039            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5040            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5041        }
5042    }
5043}
5044
5045/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5046fn render_token(token: &TokenBinding) -> String {
5047    let roles = token.roles.iter().map(render_role).join(", ");
5048    let synthesized = if token.synthesized { " (read as)" } else { "" };
5049    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5050}
5051
5052fn render_role(role: &TokenRole) -> String {
5053    match role {
5054        TokenRole::Program => "program".to_string(),
5055        TokenRole::Command { name } => format!("subcommand {name}"),
5056        TokenRole::Flag {
5057            flag,
5058            spelling,
5059            negated,
5060        } => {
5061            let negated = if *negated { ", negated" } else { "" };
5062            format!("flag {} as {spelling}{negated}", flag.name)
5063        }
5064        TokenRole::Value {
5065            flag,
5066            values,
5067            attached,
5068        } => {
5069            let attached = if *attached { ", attached" } else { "" };
5070            format!("value of {} = {values:?}{attached}", flag.name)
5071        }
5072        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5073        TokenRole::Sigil { arg, sigil, values } => {
5074            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5075        }
5076        TokenRole::Separator => "separator".to_string(),
5077        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5078        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5079        TokenRole::Restart => "restart".to_string(),
5080        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5081        TokenRole::UnknownFlag { bound_as } => match bound_as {
5082            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5083            None => "unknown flag".to_string(),
5084        },
5085        TokenRole::Refused { reason } => format!("refused: {reason}"),
5086        TokenRole::External => "external".to_string(),
5087        TokenRole::Unread => "unread".to_string(),
5088    }
5089}
5090
5091impl Debug for ParseOutput {
5092    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5093        f.debug_struct("ParseOutput")
5094            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5095            .field(
5096                "args",
5097                &self
5098                    .args
5099                    .iter()
5100                    .map(|(a, w)| format!("{}: {w}", a.name))
5101                    .collect_vec(),
5102            )
5103            .field("clauses", &self.clauses)
5104            .field(
5105                "available_flags",
5106                &self
5107                    .available_flags
5108                    .iter()
5109                    .map(|(f, w)| format!("{f}: {w}"))
5110                    .collect_vec(),
5111            )
5112            .field(
5113                "flags",
5114                &self
5115                    .flags
5116                    .iter()
5117                    .map(|(f, w)| format!("{}: {w}", f.name))
5118                    .collect_vec(),
5119            )
5120            .field("flag_awaiting_value", &self.flag_awaiting_value)
5121            .field("errors", &self.errors)
5122            .field("external", &self.external)
5123            // Provenance, one line per token and one per fallback. This is the parser's
5124            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5125            // first — `usage explain` renders the same facts for a reader.
5126            .field(
5127                "tokens",
5128                &self.tokens.iter().map(render_token).collect_vec(),
5129            )
5130            .field(
5131                "origins",
5132                &self
5133                    .flag_origins
5134                    .iter()
5135                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5136                    .chain(
5137                        self.arg_origins
5138                            .iter()
5139                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5140                    )
5141                    .collect_vec(),
5142            )
5143            .field("overridden_flags", &self.overridden_flags)
5144            .finish()
5145    }
5146}
5147
5148#[cfg(test)]
5149mod tests {
5150    use super::*;
5151    use crate::SpecFlagAction;
5152
5153    fn input(words: &[&str]) -> Vec<String> {
5154        words.iter().map(|word| (*word).to_string()).collect()
5155    }
5156
5157    #[test]
5158    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5159        // The page has always listed `-V, --version` wherever a `version` is declared,
5160        // and usage-argv and usage-go have always accepted both. This parser called them
5161        // unknown words, so the one implementation the corpus measures the others against
5162        // was the one that disagreed.
5163        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5164            .parse()
5165            .unwrap();
5166
5167        for spelling in ["--version", "-V"] {
5168            let err = parse(&spec, &input(&["ex", spelling]))
5169                .expect_err("answering with a version ends the parse");
5170            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5171        }
5172    }
5173
5174    #[test]
5175    fn the_supplied_version_flag_is_the_roots_alone() {
5176        // `version` describes the program, and the page lists the entry on the program's
5177        // own page only. A subcommand answering with it would be a claim no spec made.
5178        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5179            .parse()
5180            .unwrap();
5181
5182        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5183        assert_eq!(err.to_string(), "unexpected word: --version");
5184    }
5185
5186    #[test]
5187    fn no_declared_version_supplies_nothing() {
5188        // A `--version` answering with nothing is worse than one that is not there, which
5189        // is why the entry is conditional on the page and the spelling on the parse.
5190        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5191
5192        for spelling in ["--version", "-V"] {
5193            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5194            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5195        }
5196    }
5197
5198    #[test]
5199    fn disable_version_flag_removes_the_supplied_spellings() {
5200        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5201            .parse()
5202            .unwrap();
5203
5204        for spelling in ["--version", "-V"] {
5205            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5206            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5207        }
5208    }
5209
5210    #[test]
5211    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5212        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5213        // parse agrees without being told, because a declared flag is matched first.
5214        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5215            .parse()
5216            .unwrap();
5217
5218        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5219        assert_eq!(out.flags.len(), 1);
5220
5221        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5222        assert_eq!(err.to_string(), "1.2.3");
5223    }
5224
5225    #[test]
5226    fn the_supplied_spellings_split_the_two_version_texts() {
5227        // The same split `render_action_err` gives a declared version flag: the long
5228        // spelling prefers `long_version`, the short prefers the concise one.
5229        let spec: Spec =
5230            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5231                .parse()
5232                .unwrap();
5233
5234        assert_eq!(
5235            parse(&spec, &input(&["ex", "--version"]))
5236                .unwrap_err()
5237                .to_string(),
5238            "1.2.3 (abcdef)"
5239        );
5240        assert_eq!(
5241            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5242            "1.2.3"
5243        );
5244    }
5245
5246    #[test]
5247    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5248        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5249        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5250        // naming nothing: usage-argv and usage-go both resolve the letter through the
5251        // same lookup that finds a declared short, and clap prints help for it too.
5252        let spec: Spec =
5253            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5254                .parse()
5255                .unwrap();
5256
5257        for token in ["-vh", "-hv"] {
5258            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5259            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5260        }
5261        for token in ["-vV", "-Vv"] {
5262            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5263            assert_eq!(err.to_string(), "1.2.3", "{token}");
5264        }
5265    }
5266
5267    #[test]
5268    fn a_bundled_help_letter_asks_for_the_short_page() {
5269        // Whatever else shares the token: `-h` is the short spelling, and the letters
5270        // beside it say nothing about which page was asked for.
5271        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"
5272            .parse()
5273            .unwrap();
5274
5275        let short = parse(&spec, &input(&["ex", "-vh"]))
5276            .unwrap_err()
5277            .to_string();
5278        let long = parse(&spec, &input(&["ex", "--help"]))
5279            .unwrap_err()
5280            .to_string();
5281        assert!(short.contains("Be loud"), "{short}");
5282        assert!(!short.contains("at length"), "{short}");
5283        assert!(long.contains("at length"), "{long}");
5284    }
5285
5286    #[test]
5287    fn the_bundled_version_letter_is_the_roots_alone() {
5288        // The same rule the whole-token spelling follows, asked one letter at a time.
5289        let spec: Spec =
5290            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5291                .parse()
5292                .unwrap();
5293
5294        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5295        assert_eq!(err.to_string(), "unexpected word: -vV");
5296    }
5297
5298    #[test]
5299    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5300        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5301        // this spec's own `-h`, taking its value from the rest of the token.
5302        let spec: Spec =
5303            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5304                .parse()
5305                .unwrap();
5306
5307        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5308        assert_eq!(out.flags.len(), 2);
5309        assert!(out
5310            .flags
5311            .iter()
5312            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
5313    }
5314
5315    #[test]
5316    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5317        let spec: Spec =
5318            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5319                .parse()
5320                .unwrap();
5321
5322        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5323        assert_eq!(err.to_string(), "unexpected word: -vh");
5324    }
5325
5326    #[test]
5327    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5328        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5329        // on the way to discovering that `z` names nothing.
5330        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5331            .parse()
5332            .unwrap();
5333
5334        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5335        assert!(out.flags.is_empty(), "{:?}", out.flags);
5336        assert_eq!(out.args.len(), 1);
5337    }
5338
5339    fn spec_with_arg(arg: SpecArg) -> Spec {
5340        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5341        Spec {
5342            name: "test".to_string(),
5343            bin: "test".to_string(),
5344            cmd,
5345            ..Default::default()
5346        }
5347    }
5348
5349    fn spec_with_flag(flag: SpecFlag) -> Spec {
5350        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5351        Spec {
5352            name: "test".to_string(),
5353            bin: "test".to_string(),
5354            cmd,
5355            ..Default::default()
5356        }
5357    }
5358
5359    fn parse_with_env(
5360        spec: &Spec,
5361        words: &[&str],
5362        env: &[(&str, &str)],
5363    ) -> Result<ParseOutput, miette::Error> {
5364        let env = env
5365            .iter()
5366            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5367            .collect();
5368        Parser::new(spec).with_env(env).parse(&input(words))
5369    }
5370
5371    fn first_string_value(parsed: &ParseOutput) -> &str {
5372        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5373            return value;
5374        }
5375        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5376            return value;
5377        }
5378        panic!("expected first parsed value to be ParseValue::String");
5379    }
5380
5381    #[test]
5382    fn custom_environment_parser_dispatches_executable_views() {
5383        let spec: Spec = r#"
5384bin "ex"
5385view "runner" root="run"
5386cmd "run" {
5387    flag "--token <token>" env="TOKEN"
5388}
5389        "#
5390        .parse()
5391        .unwrap();
5392        let parsed = Parser::new(&spec)
5393            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5394            .parse(&input(&["runner"]))
5395            .unwrap();
5396
5397        assert_eq!(parsed.cmd.name, "runner");
5398        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5399            && matches!(value, ParseValue::String(value) if value == "secret")));
5400    }
5401
5402    #[test]
5403    fn an_executable_view_keeps_the_hosts_version_action() {
5404        let spec: Spec = r#"
5405bin "ex"
5406version "1.2.3"
5407flag "-V --version" action="version"
5408flag "--verbose" global=#true
5409view "runner" root="run" globals=#true
5410cmd "run"
5411        "#
5412        .parse()
5413        .unwrap();
5414
5415        let error = Parser::new(&spec)
5416            .parse(&input(&["runner", "--version"]))
5417            .expect_err("the host version action should answer before view projection");
5418        assert_eq!(error.to_string(), "1.2.3");
5419
5420        let error = Parser::new(&spec)
5421            .parse(&input(&["runner", "--verbose", "--version"]))
5422            .expect_err("the host version action should remain after a carried global");
5423        assert_eq!(error.to_string(), "1.2.3");
5424    }
5425
5426    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5427        let flag = parsed
5428            .flags
5429            .keys()
5430            .find(|flag| flag.name == name)
5431            .unwrap_or_else(|| panic!("expected flag {name}"));
5432        let value = parsed
5433            .flags
5434            .get(flag)
5435            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5436        match value {
5437            ParseValue::String(value) => value,
5438            _ => panic!("expected flag {name} to be ParseValue::String"),
5439        }
5440    }
5441
5442    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5443        let err = result.expect_err("expected parser error");
5444        assert_eq!(format!("{err}"), expected);
5445    }
5446
5447    #[test]
5448    fn a_short_version_action_falls_back_to_the_long_version() {
5449        let flag = SpecFlag::builder()
5450            .short('R')
5451            .action(SpecFlagAction::Version)
5452            .build();
5453        let spec = Spec {
5454            name: "test".to_string(),
5455            bin: "test".to_string(),
5456            long_version: Some("1.2.3\ncommit abc123".to_string()),
5457            ..Default::default()
5458        };
5459        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
5460            panic!("expected version action")
5461        };
5462        assert_eq!(version, "1.2.3\ncommit abc123");
5463    }
5464
5465    #[cfg(feature = "unstable_choices_env")]
5466    fn spec_arg_choices_env(key: &str) -> Spec {
5467        spec_with_arg(
5468            SpecArg::builder()
5469                .name("env")
5470                .choices_env(key)
5471                .required(false)
5472                .build(),
5473        )
5474    }
5475
5476    #[cfg(feature = "unstable_choices_env")]
5477    fn spec_flag_choices_env(key: &str) -> Spec {
5478        spec_with_flag(
5479            SpecFlag::builder()
5480                .long("env")
5481                .arg(SpecArg::builder().name("env").choices_env(key).build())
5482                .build(),
5483        )
5484    }
5485
5486    #[test]
5487    fn test_parse() {
5488        let cmd = SpecCommand::builder()
5489            .name("test")
5490            .arg(SpecArg::builder().name("arg").build())
5491            .flag(SpecFlag::builder().long("flag").build())
5492            .build();
5493        let spec = Spec {
5494            name: "test".to_string(),
5495            bin: "test".to_string(),
5496            cmd,
5497            ..Default::default()
5498        };
5499        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5500        let parsed = parse(&spec, &input).unwrap();
5501        assert_eq!(parsed.cmds.len(), 1);
5502        assert_eq!(parsed.cmds[0].name, "test");
5503        assert_eq!(parsed.args.len(), 1);
5504        assert_eq!(parsed.flags.len(), 1);
5505        assert_eq!(parsed.available_flags.len(), 1);
5506    }
5507
5508    #[test]
5509    fn test_flag_overrides_last_occurrence_wins() {
5510        let spec: Spec = r#"
5511flag "--stdin" default=#true
5512flag "--file <file>" overrides="--stdin"
5513        "#
5514        .parse()
5515        .unwrap();
5516
5517        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5518        assert_eq!(file_wins.flags.len(), 1);
5519        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5520        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5521
5522        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5523        assert_eq!(stdin_wins.flags.len(), 1);
5524        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5525        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5526    }
5527
5528    #[test]
5529    fn test_flag_override_clears_pending_value() {
5530        let spec: Spec = r#"
5531flag "--file <file>" overrides="--stdin"
5532flag "--stdin"
5533arg "[input]"
5534        "#
5535        .parse()
5536        .unwrap();
5537
5538        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5539        assert_eq!(parsed.flags.len(), 1);
5540        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5541        assert_eq!(first_string_value(&parsed), "input.txt");
5542    }
5543
5544    #[cfg(unix)]
5545    #[test]
5546    fn a_mount_on_the_root_discovers_subcommands() {
5547        // The root is a command like any other, so it can find its own subcommands
5548        // by running something. Uses `echo` rather than a fixture because resolving
5549        // a mount is what is being tested.
5550        let spec: Spec = r#"
5551name "ex"
5552bin "ex"
5553cmd "declared"
5554mount run="echo 'cmd \"discovered\"'"
5555"#
5556        .parse()
5557        .unwrap();
5558
5559        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5560        assert_eq!(out.cmd.name, "discovered");
5561    }
5562
5563    #[test]
5564    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5565        let spec: Spec = r#"
5566name "ex"
5567bin "ex"
5568mount run="this command must never run"
5569cmd "declared"
5570"#
5571        .parse()
5572        .unwrap();
5573
5574        Parser::new(&spec)
5575            .with_mount_outputs(HashMap::new())
5576            .parse(&input(&["ex", "declared"]))
5577            .expect("a declared command does not resolve the mount");
5578
5579        let error = Parser::new(&spec)
5580            .with_mount_outputs(HashMap::new())
5581            .parse(&input(&["ex", "discovered"]))
5582            .unwrap_err();
5583        assert!(
5584            error
5585                .to_string()
5586                .contains("No injected output was provided for mount command"),
5587            "{error}"
5588        );
5589    }
5590
5591    #[cfg(unix)]
5592    #[test]
5593    fn completion_sees_root_mounted_commands_with_nothing_typed() {
5594        // The case a root mount exists for. `mycli <tab>` has no word to trigger
5595        // discovery with, so a completion has to resolve up front or the mounted
5596        // commands are never offered.
5597        let spec: Spec = r#"
5598name "ex"
5599bin "ex"
5600cmd "declared"
5601mount run="echo 'cmd \"discovered\"'"
5602"#
5603        .parse()
5604        .unwrap();
5605
5606        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5607        assert!(
5608            out.cmd.subcommands.contains_key("discovered"),
5609            "a completion should see mounted commands; got {:?}",
5610            out.cmd.subcommands.keys().collect::<Vec<_>>()
5611        );
5612    }
5613
5614    #[cfg(unix)]
5615    #[test]
5616    fn completion_and_execution_agree_about_discovery() {
5617        // Offering a command that a real parse would hand to the default instead is
5618        // worse than not offering it, so the gate applies to both paths. The mount
5619        // fails if it runs, which is how both halves are checked at once.
5620        let spec: Spec = r#"
5621name "ex"
5622bin "ex"
5623default_subcommand "run"
5624cmd "run" {
5625  arg "<task>"
5626}
5627mount run="exit 1"
5628"#
5629        .parse()
5630        .unwrap();
5631
5632        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5633        assert!(
5634            !out.cmd.subcommands.contains_key("discovered"),
5635            "a completion must not offer what execution will not route"
5636        );
5637
5638        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5639        assert_eq!(out.cmd.name, "run");
5640    }
5641
5642    #[cfg(unix)]
5643    #[test]
5644    fn a_default_subcommand_outranks_discovery() {
5645        // The default already says what an unmatched word means, and says it for
5646        // free. The mount fails if it runs, so parsing proves discovery was skipped.
5647        let spec: Spec = r#"
5648name "ex"
5649bin "ex"
5650default_subcommand "run"
5651cmd "run" {
5652  arg "<task>"
5653}
5654mount run="exit 1"
5655"#
5656        .parse()
5657        .unwrap();
5658
5659        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5660        assert_eq!(out.cmd.name, "run");
5661    }
5662
5663    #[cfg(unix)]
5664    #[test]
5665    fn a_mount_may_ask_to_outrank_the_default() {
5666        // Opting in, and paying for it: discovery runs first, so a discovered
5667        // command wins over the fallback.
5668        let spec: Spec = r#"
5669name "ex"
5670bin "ex"
5671default_subcommand "run"
5672cmd "run" {
5673  arg "<task>"
5674}
5675mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5676"#
5677        .parse()
5678        .unwrap();
5679
5680        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5681        assert_eq!(out.cmd.name, "discovered");
5682
5683        // A word it does not know still reaches the default.
5684        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5685        assert_eq!(out.cmd.name, "run");
5686    }
5687
5688    #[cfg(unix)]
5689    #[test]
5690    fn a_flag_does_not_run_the_mount() {
5691        // A flag matches no subcommand, which would have been enough to trigger
5692        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
5693        // so parsing at all is the proof that it did not.
5694        let spec: Spec = r#"
5695name "ex"
5696bin "ex"
5697flag "--verbose"
5698cmd "declared"
5699mount run="exit 1"
5700"#
5701        .parse()
5702        .unwrap();
5703
5704        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
5705        assert_eq!(out.cmd.name, "ex");
5706    }
5707
5708    #[cfg(unix)]
5709    #[test]
5710    fn a_declared_subcommand_does_not_run_the_mount() {
5711        // The mount would fail if it ran, so this parsing at all is the proof that
5712        // discovery is skipped when the word is already known. Worth pinning: a root
5713        // mount that resolved eagerly would spawn a process on every invocation.
5714        let spec: Spec = r#"
5715name "ex"
5716bin "ex"
5717cmd "declared"
5718mount run="exit 1"
5719"#
5720        .parse()
5721        .unwrap();
5722
5723        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
5724        assert_eq!(out.cmd.name, "declared");
5725    }
5726
5727    #[test]
5728    fn a_root_mount_survives_being_written_out() {
5729        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
5730            .parse()
5731            .unwrap();
5732        assert_eq!(spec.cmd.mounts.len(), 1);
5733
5734        let reparsed: Spec = spec.to_string().parse().unwrap();
5735        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
5736        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
5737    }
5738
5739    #[test]
5740    fn test_mount_prefix_applies_flag_overrides() {
5741        let stdin = Arc::new(
5742            SpecFlag::builder()
5743                .name("stdin")
5744                .long("stdin")
5745                .global(true)
5746                .build(),
5747        );
5748        let file = Arc::new(
5749            SpecFlag::builder()
5750                .name("file")
5751                .long("file")
5752                .arg(SpecArg::builder().name("file").build())
5753                .global(true)
5754                .overrides_with(vec!["--stdin".to_string()])
5755                .build(),
5756        );
5757        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
5758
5759        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
5760        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
5761
5762        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
5763    }
5764
5765    #[test]
5766    fn test_flag_override_suppresses_env_value() {
5767        let spec: Spec = r#"
5768flag "--stdin" env="USE_STDIN"
5769flag "--file <file>" overrides="--stdin"
5770        "#
5771        .parse()
5772        .unwrap();
5773
5774        let parsed = parse_with_env(
5775            &spec,
5776            &["test", "--file", "input.txt"],
5777            &[("USE_STDIN", "true")],
5778        )
5779        .unwrap();
5780        assert_eq!(parsed.flags.len(), 1);
5781        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5782    }
5783
5784    #[test]
5785    fn test_flag_override_suppresses_required_check() {
5786        let spec: Spec = r#"
5787flag "--stdin" required=#true
5788flag "--file <file>" overrides="--stdin"
5789        "#
5790        .parse()
5791        .unwrap();
5792
5793        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5794        assert_eq!(parsed.flags.len(), 1);
5795        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5796    }
5797
5798    #[test]
5799    fn test_flag_required_if() {
5800        let spec: Spec = r#"
5801flag "--dir <dir>"
5802flag "--file <file>" required_if="--dir"
5803        "#
5804        .parse()
5805        .unwrap();
5806
5807        parse(&spec, &input(&["test"])).unwrap();
5808        assert_parse_err(
5809            parse(&spec, &input(&["test", "--dir", "src"])),
5810            "Missing required flag: --file <file>",
5811        );
5812        parse(
5813            &spec,
5814            &input(&["test", "--dir", "src", "--file", "input.txt"]),
5815        )
5816        .unwrap();
5817    }
5818
5819    #[test]
5820    fn test_flag_required_unless() {
5821        let spec: Spec = r#"
5822flag "--stdin"
5823flag "--file <file>" required_unless="--stdin"
5824        "#
5825        .parse()
5826        .unwrap();
5827
5828        assert_parse_err(
5829            parse(&spec, &input(&["test"])),
5830            "Missing required flag: --file <file>",
5831        );
5832        parse(&spec, &input(&["test", "--stdin"])).unwrap();
5833        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5834    }
5835
5836    #[test]
5837    fn complete_required_relationship_truth_tables() {
5838        let spec: Spec = r#"
5839name "ex"
5840bin "ex"
5841flag "--mode <mode>"
5842flag "--scope <scope>"
5843flag "--token <token>" {
5844    required_if_eq "--mode" "remote"
5845}
5846flag "--approval <approval>" {
5847    required_if_eq_all "--mode" "remote" "--scope" "global"
5848}
5849flag "--input <input>" {
5850    required_unless "--stdin" "--file"
5851}
5852flag "--checksum <checksum>" {
5853    required_unless_all "--stdin" "--file"
5854}
5855flag "--stdin"
5856flag "--file <file>"
5857arg "[request]" {
5858    requires "--mode" "--scope"
5859}
5860"#
5861        .parse()
5862        .unwrap();
5863        let parse_args = |args: &[&str]| {
5864            parse(
5865                &spec,
5866                &args
5867                    .iter()
5868                    .map(|arg| (*arg).to_string())
5869                    .collect::<Vec<_>>(),
5870            )
5871        };
5872
5873        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
5874        assert!(parse_args(&[
5875            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
5876        ])
5877        .is_err());
5878        parse_args(&[
5879            "ex",
5880            "--mode",
5881            "remote",
5882            "--token",
5883            "secret",
5884            "--scope",
5885            "global",
5886            "--approval",
5887            "yes",
5888            "--stdin",
5889            "--file",
5890            "in",
5891        ])
5892        .unwrap();
5893        parse_args(&[
5894            "ex",
5895            "--mode",
5896            "local",
5897            "--scope",
5898            "project",
5899            "--stdin",
5900            "--checksum",
5901            "sum",
5902            "request.json",
5903        ])
5904        .unwrap();
5905
5906        let reparsed: Spec = spec.to_string().parse().unwrap();
5907        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
5908        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
5909        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
5910        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
5911    }
5912
5913    #[test]
5914    fn test_conditional_requirements_treat_env_as_explicit() {
5915        let spec: Spec = r#"
5916flag "--dir <dir>" env="INPUT_DIR"
5917flag "--stdin" env="USE_STDIN"
5918flag "--file <file>" required_if="--dir" required_unless="--stdin"
5919        "#
5920        .parse()
5921        .unwrap();
5922
5923        assert_parse_err(
5924            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5925            "Missing required flag: --file <file>",
5926        );
5927        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5928    }
5929
5930    #[test]
5931    fn test_custom_env_does_not_fall_back_to_process_env() {
5932        assert!(std::env::var("PATH").is_ok());
5933        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5934
5935        assert_parse_err(
5936            parse_with_env(&spec, &["test"], &[]),
5937            "Missing required flag: --file <file>",
5938        );
5939    }
5940
5941    #[test]
5942    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5943        let spec: Spec = r#"
5944flag "--dir <dir>" default="src"
5945flag "--file <file>" required_if="--dir"
5946        "#
5947        .parse()
5948        .unwrap();
5949
5950        parse(&spec, &input(&["test"])).unwrap();
5951    }
5952
5953    #[test]
5954    fn test_conditional_requirements_see_overridden_flags_as_absent() {
5955        let spec: Spec = r#"
5956flag "--stdin"
5957flag "--dir <dir>" overrides="--stdin"
5958flag "--file <file>" required_unless="--stdin"
5959        "#
5960        .parse()
5961        .unwrap();
5962
5963        assert_parse_err(
5964            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5965            "Missing required flag: --file <file>",
5966        );
5967    }
5968
5969    #[test]
5970    fn short_flag_is_one_character_not_one_byte() {
5971        // A short is declared and read by character. Counting bytes instead either
5972        // refuses the declaration or slices the token inside the character, and clap
5973        // — which many specs are generated from — accepts shorts like this one.
5974        let spec = spec_with_flag(
5975            SpecFlag::builder()
5976                .short('磨')
5977                .long("polish")
5978                .arg(SpecArg::builder().name("opt").build())
5979                .build(),
5980        );
5981        let attached = Parser::new(&spec)
5982            .parse(&input(&["test", "-磨VALUE"]))
5983            .unwrap();
5984        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5985        let detached = Parser::new(&spec)
5986            .parse(&input(&["test", "-磨", "V"]))
5987            .unwrap();
5988        assert_eq!(flag_string_value(&detached, "polish"), "V");
5989    }
5990
5991    #[test]
5992    fn test_as_env() {
5993        let cmd = SpecCommand::builder()
5994            .name("test")
5995            .arg(SpecArg::builder().name("arg").build())
5996            .flag(SpecFlag::builder().long("flag").build())
5997            .flag(
5998                SpecFlag::builder()
5999                    .long("force")
6000                    .negate("--no-force")
6001                    .build(),
6002            )
6003            .build();
6004        let spec = Spec {
6005            name: "test".to_string(),
6006            bin: "test".to_string(),
6007            cmd,
6008            ..Default::default()
6009        };
6010        let input = vec![
6011            "test".to_string(),
6012            "--flag".to_string(),
6013            "--no-force".to_string(),
6014        ];
6015        let parsed = parse(&spec, &input).unwrap();
6016        let env = parsed.as_env();
6017        assert_eq!(env.len(), 2);
6018        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6019        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6020    }
6021
6022    #[test]
6023    fn test_arg_env_var() {
6024        let cmd = SpecCommand::builder()
6025            .name("test")
6026            .arg(
6027                SpecArg::builder()
6028                    .name("input")
6029                    .env("TEST_ARG_INPUT")
6030                    .required(true)
6031                    .build(),
6032            )
6033            .build();
6034        let spec = Spec {
6035            name: "test".to_string(),
6036            bin: "test".to_string(),
6037            cmd,
6038            ..Default::default()
6039        };
6040
6041        // Set env var
6042        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
6043
6044        let input = vec!["test".to_string()];
6045        let parsed = parse(&spec, &input).unwrap();
6046
6047        assert_eq!(parsed.args.len(), 1);
6048        let arg = parsed.args.keys().next().unwrap();
6049        assert_eq!(arg.name, "input");
6050        let value = parsed.args.values().next().unwrap();
6051        assert_eq!(value.to_string(), "test_file.txt");
6052
6053        // Clean up
6054        std::env::remove_var("TEST_ARG_INPUT");
6055    }
6056
6057    #[test]
6058    fn test_flag_env_var_with_arg() {
6059        let cmd = SpecCommand::builder()
6060            .name("test")
6061            .flag(
6062                SpecFlag::builder()
6063                    .long("output")
6064                    .env("TEST_FLAG_OUTPUT")
6065                    .arg(SpecArg::builder().name("file").build())
6066                    .build(),
6067            )
6068            .build();
6069        let spec = Spec {
6070            name: "test".to_string(),
6071            bin: "test".to_string(),
6072            cmd,
6073            ..Default::default()
6074        };
6075
6076        // Set env var
6077        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
6078
6079        let input = vec!["test".to_string()];
6080        let parsed = parse(&spec, &input).unwrap();
6081
6082        assert_eq!(parsed.flags.len(), 1);
6083        let flag = parsed.flags.keys().next().unwrap();
6084        assert_eq!(flag.name, "output");
6085        let value = parsed.flags.values().next().unwrap();
6086        assert_eq!(value.to_string(), "output.txt");
6087
6088        // Clean up
6089        std::env::remove_var("TEST_FLAG_OUTPUT");
6090    }
6091
6092    #[test]
6093    fn test_flag_env_var_boolean() {
6094        let cmd = SpecCommand::builder()
6095            .name("test")
6096            .flag(
6097                SpecFlag::builder()
6098                    .long("verbose")
6099                    .env("TEST_FLAG_VERBOSE")
6100                    .build(),
6101            )
6102            .build();
6103        let spec = Spec {
6104            name: "test".to_string(),
6105            bin: "test".to_string(),
6106            cmd,
6107            ..Default::default()
6108        };
6109
6110        // Set env var to true
6111        std::env::set_var("TEST_FLAG_VERBOSE", "true");
6112
6113        let input = vec!["test".to_string()];
6114        let parsed = parse(&spec, &input).unwrap();
6115
6116        assert_eq!(parsed.flags.len(), 1);
6117        let flag = parsed.flags.keys().next().unwrap();
6118        assert_eq!(flag.name, "verbose");
6119        let value = parsed.flags.values().next().unwrap();
6120        assert_eq!(value.to_string(), "true");
6121
6122        // Clean up
6123        std::env::remove_var("TEST_FLAG_VERBOSE");
6124    }
6125
6126    #[test]
6127    fn test_env_var_precedence() {
6128        // CLI args should take precedence over env vars
6129        let cmd = SpecCommand::builder()
6130            .name("test")
6131            .arg(
6132                SpecArg::builder()
6133                    .name("input")
6134                    .env("TEST_PRECEDENCE_INPUT")
6135                    .required(true)
6136                    .build(),
6137            )
6138            .build();
6139        let spec = Spec {
6140            name: "test".to_string(),
6141            bin: "test".to_string(),
6142            cmd,
6143            ..Default::default()
6144        };
6145
6146        // Set env var
6147        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
6148
6149        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6150        let parsed = parse(&spec, &input).unwrap();
6151
6152        assert_eq!(parsed.args.len(), 1);
6153        let value = parsed.args.values().next().unwrap();
6154        // CLI arg should take precedence
6155        assert_eq!(value.to_string(), "cli_file.txt");
6156
6157        // Clean up
6158        std::env::remove_var("TEST_PRECEDENCE_INPUT");
6159    }
6160
6161    #[test]
6162    fn test_flag_var_true_with_single_default() {
6163        // When var=true and default="bar", the default should be MultiString(["bar"])
6164        let cmd = SpecCommand::builder()
6165            .name("test")
6166            .flag(
6167                SpecFlag::builder()
6168                    .long("foo")
6169                    .var(true)
6170                    .arg(SpecArg::builder().name("foo").build())
6171                    .default_value("bar")
6172                    .build(),
6173            )
6174            .build();
6175        let spec = Spec {
6176            name: "test".to_string(),
6177            bin: "test".to_string(),
6178            cmd,
6179            ..Default::default()
6180        };
6181
6182        // User doesn't provide the flag
6183        let input = vec!["test".to_string()];
6184        let parsed = parse(&spec, &input).unwrap();
6185
6186        assert_eq!(parsed.flags.len(), 1);
6187        let flag = parsed.flags.keys().next().unwrap();
6188        assert_eq!(flag.name, "foo");
6189        let value = parsed.flags.values().next().unwrap();
6190        // Should be MultiString, not String
6191        match value {
6192            ParseValue::MultiString(v) => {
6193                assert_eq!(v.len(), 1);
6194                assert_eq!(v[0], "bar");
6195            }
6196            _ => panic!("Expected MultiString, got {:?}", value),
6197        }
6198    }
6199
6200    #[test]
6201    fn test_flag_var_true_with_multiple_defaults() {
6202        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6203        let cmd = SpecCommand::builder()
6204            .name("test")
6205            .flag(
6206                SpecFlag::builder()
6207                    .long("foo")
6208                    .var(true)
6209                    .arg(SpecArg::builder().name("foo").build())
6210                    .default_values(["xyz", "bar"])
6211                    .build(),
6212            )
6213            .build();
6214        let spec = Spec {
6215            name: "test".to_string(),
6216            bin: "test".to_string(),
6217            cmd,
6218            ..Default::default()
6219        };
6220
6221        // User doesn't provide the flag
6222        let input = vec!["test".to_string()];
6223        let parsed = parse(&spec, &input).unwrap();
6224
6225        assert_eq!(parsed.flags.len(), 1);
6226        let value = parsed.flags.values().next().unwrap();
6227        // Should be MultiString with both values
6228        match value {
6229            ParseValue::MultiString(v) => {
6230                assert_eq!(v.len(), 2);
6231                assert_eq!(v[0], "xyz");
6232                assert_eq!(v[1], "bar");
6233            }
6234            _ => panic!("Expected MultiString, got {:?}", value),
6235        }
6236    }
6237
6238    #[test]
6239    fn test_flag_var_false_with_default_remains_string() {
6240        // When var=false (default), the default should still be String("bar")
6241        let cmd = SpecCommand::builder()
6242            .name("test")
6243            .flag(
6244                SpecFlag::builder()
6245                    .long("foo")
6246                    .var(false) // Default behavior
6247                    .arg(SpecArg::builder().name("foo").build())
6248                    .default_value("bar")
6249                    .build(),
6250            )
6251            .build();
6252        let spec = Spec {
6253            name: "test".to_string(),
6254            bin: "test".to_string(),
6255            cmd,
6256            ..Default::default()
6257        };
6258
6259        // User doesn't provide the flag
6260        let input = vec!["test".to_string()];
6261        let parsed = parse(&spec, &input).unwrap();
6262
6263        assert_eq!(parsed.flags.len(), 1);
6264        let value = parsed.flags.values().next().unwrap();
6265        // Should be String, not MultiString
6266        match value {
6267            ParseValue::String(s) => {
6268                assert_eq!(s, "bar");
6269            }
6270            _ => panic!("Expected String, got {:?}", value),
6271        }
6272    }
6273
6274    #[test]
6275    fn test_arg_var_true_with_single_default() {
6276        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6277        let cmd = SpecCommand::builder()
6278            .name("test")
6279            .arg(
6280                SpecArg::builder()
6281                    .name("files")
6282                    .var(true)
6283                    .default_value("default.txt")
6284                    .required(false)
6285                    .build(),
6286            )
6287            .build();
6288        let spec = Spec {
6289            name: "test".to_string(),
6290            bin: "test".to_string(),
6291            cmd,
6292            ..Default::default()
6293        };
6294
6295        // User doesn't provide the arg
6296        let input = vec!["test".to_string()];
6297        let parsed = parse(&spec, &input).unwrap();
6298
6299        assert_eq!(parsed.args.len(), 1);
6300        let value = parsed.args.values().next().unwrap();
6301        // Should be MultiString, not String
6302        match value {
6303            ParseValue::MultiString(v) => {
6304                assert_eq!(v.len(), 1);
6305                assert_eq!(v[0], "default.txt");
6306            }
6307            _ => panic!("Expected MultiString, got {:?}", value),
6308        }
6309    }
6310
6311    #[test]
6312    fn test_arg_var_true_with_multiple_defaults() {
6313        // When arg has var=true and multiple defaults
6314        let cmd = SpecCommand::builder()
6315            .name("test")
6316            .arg(
6317                SpecArg::builder()
6318                    .name("files")
6319                    .var(true)
6320                    .default_values(["file1.txt", "file2.txt"])
6321                    .required(false)
6322                    .build(),
6323            )
6324            .build();
6325        let spec = Spec {
6326            name: "test".to_string(),
6327            bin: "test".to_string(),
6328            cmd,
6329            ..Default::default()
6330        };
6331
6332        // User doesn't provide the arg
6333        let input = vec!["test".to_string()];
6334        let parsed = parse(&spec, &input).unwrap();
6335
6336        assert_eq!(parsed.args.len(), 1);
6337        let value = parsed.args.values().next().unwrap();
6338        // Should be MultiString with both values
6339        match value {
6340            ParseValue::MultiString(v) => {
6341                assert_eq!(v.len(), 2);
6342                assert_eq!(v[0], "file1.txt");
6343                assert_eq!(v[1], "file2.txt");
6344            }
6345            _ => panic!("Expected MultiString, got {:?}", value),
6346        }
6347    }
6348
6349    #[test]
6350    fn test_arg_var_false_with_default_remains_string() {
6351        // When arg has var=false (default), the default should still be String
6352        let cmd = SpecCommand::builder()
6353            .name("test")
6354            .arg(
6355                SpecArg::builder()
6356                    .name("file")
6357                    .var(false)
6358                    .default_value("default.txt")
6359                    .required(false)
6360                    .build(),
6361            )
6362            .build();
6363        let spec = Spec {
6364            name: "test".to_string(),
6365            bin: "test".to_string(),
6366            cmd,
6367            ..Default::default()
6368        };
6369
6370        // User doesn't provide the arg
6371        let input = vec!["test".to_string()];
6372        let parsed = parse(&spec, &input).unwrap();
6373
6374        assert_eq!(parsed.args.len(), 1);
6375        let value = parsed.args.values().next().unwrap();
6376        // Should be String, not MultiString
6377        match value {
6378            ParseValue::String(s) => {
6379                assert_eq!(s, "default.txt");
6380            }
6381            _ => panic!("Expected String, got {:?}", value),
6382        }
6383    }
6384
6385    #[test]
6386    fn test_scalar_defaults_validate_only_first_default_choice() {
6387        let specs = [
6388            spec_with_arg(
6389                SpecArg::builder()
6390                    .name("env")
6391                    .var(false)
6392                    .default_values(["dev", "prod"])
6393                    .choices(["dev"])
6394                    .required(false)
6395                    .build(),
6396            ),
6397            spec_with_flag(
6398                SpecFlag::builder()
6399                    .long("env")
6400                    .arg(
6401                        SpecArg::builder()
6402                            .name("env")
6403                            .default_values(["dev", "prod"])
6404                            .choices(["dev"])
6405                            .build(),
6406                    )
6407                    .build(),
6408            ),
6409        ];
6410
6411        for spec in specs {
6412            let parsed = parse(&spec, &input(&["test"])).unwrap();
6413            assert_eq!(first_string_value(&parsed), "dev");
6414        }
6415    }
6416
6417    #[test]
6418    fn a_delimiter_turns_one_word_into_several_values() {
6419        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6420            .parse()
6421            .unwrap();
6422
6423        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6424        let multi = |value: &ParseValue| match value {
6425            ParseValue::MultiString(values) => values.clone(),
6426            other => panic!("expected several values, got {other:?}"),
6427        };
6428        let tags = parsed
6429            .flags
6430            .iter()
6431            .find(|(f, _)| f.name == "tags")
6432            .map(|(_, v)| v)
6433            .unwrap();
6434        assert_eq!(multi(tags), vec!["a", "b", "c"]);
6435        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6436    }
6437
6438    #[test]
6439    fn a_positional_splits_before_its_choices_are_asked() {
6440        // The flag path did this and the positional path did not, so a word whose parts
6441        // were all choices was rejected as one value, and a bad half was reported as the
6442        // whole word.
6443        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
6444            .parse()
6445            .unwrap();
6446
6447        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6448
6449        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6450        let message = err.to_string();
6451        assert!(message.contains("nowhere"), "{message}");
6452        assert!(
6453            !message.contains("src:nowhere"),
6454            "the bad half should be named, not the whole word: {message}"
6455        );
6456    }
6457
6458    #[test]
6459    fn a_split_value_is_counted_and_judged_as_values() {
6460        // Split during the parse rather than after it, so everything downstream sees the
6461        // values the user meant rather than the words they typed: `choices` judges each
6462        // one, and the bounds count them.
6463        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
6464            .parse()
6465            .unwrap();
6466
6467        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6468        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6469        assert!(err.to_string().contains("staging"), "{err}");
6470        assert!(
6471            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6472            "three values should breach var_max=2"
6473        );
6474    }
6475
6476    #[test]
6477    fn a_split_bound_counts_one_occurrence_at_a_time() {
6478        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
6479        // delimiter the collection simply stops at it, so it could never be exceeded; a word
6480        // carrying several values can carry an occurrence past it in one step, and that is
6481        // the only way this bound is ever breached.
6482        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
6483            .parse()
6484            .unwrap();
6485
6486        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
6487        assert!(
6488            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
6489            "three values out of one word is still three values"
6490        );
6491        // The rule the corpus documents for plain words, on split ones: a second occurrence
6492        // starts counting again rather than adding to the first.
6493        parse(
6494            &spec,
6495            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
6496        )
6497        .expect("two per occurrence, twice, is within the bound");
6498    }
6499
6500    #[test]
6501    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
6502        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
6503            .parse()
6504            .unwrap();
6505
6506        parse(
6507            &spec,
6508            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
6509        )
6510        .expect("each occurrence satisfies the bound independently");
6511
6512        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
6513        assert!(
6514            error
6515                .to_string()
6516                .contains("requires at least 2 value(s), got 1"),
6517            "{error:?}"
6518        );
6519    }
6520
6521    #[test]
6522    fn an_exclusive_flag_has_to_be_alone() {
6523        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6524            .parse()
6525            .unwrap();
6526
6527        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6528
6529        // Any other flag.
6530        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6531        assert!(err.to_string().contains("on its own"), "{err}");
6532
6533        // And a positional, which is what makes this more than a conflict with every
6534        // other flag.
6535        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6536        assert!(err.to_string().contains("on its own"), "{err}");
6537
6538        // Not given, so it imposes nothing.
6539        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6540    }
6541
6542    #[test]
6543    fn an_exclusive_flag_conflicts_with_clause_arguments() {
6544        let spec: Spec = r#"name "ex"
6545bin "ex"
6546flag "--dump" exclusive=#true
6547clause "tasks" separator=":::" {
6548  arg "<task>"
6549}
6550"#
6551        .parse()
6552        .unwrap();
6553
6554        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6555        assert!(err.to_string().contains("on its own"), "{err}");
6556    }
6557
6558    #[test]
6559    fn a_partial_parse_reports_the_next_implicit_clause_argument() {
6560        let spec: Spec = r#"name "ex"
6561bin "ex"
6562clause "tools" {
6563  arg "<tool@version>"
6564}
6565"#
6566        .parse()
6567        .unwrap();
6568
6569        let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
6570
6571        assert_eq!(
6572            parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
6573            Some("tool@version")
6574        );
6575    }
6576
6577    #[test]
6578    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6579        // Only what was supplied counts, as `conflicts` reads it. A default counting as
6580        // company would make an exclusive flag unusable on any command that has one.
6581        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6582            .parse()
6583            .unwrap();
6584
6585        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6586        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6587    }
6588
6589    #[test]
6590    fn an_exclusive_flag_bypasses_required_siblings() {
6591        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6592            .parse()
6593            .unwrap();
6594
6595        parse(&spec, &input(&["ex", "--dump"]))
6596            .expect("exclusive is the command's requiredness escape");
6597        assert!(parse(
6598            &spec,
6599            &input(&["ex", "--dump", "--out", "somewhere", "target"])
6600        )
6601        .is_err());
6602    }
6603
6604    #[test]
6605    fn an_environment_value_counts_for_an_exclusive_flag() {
6606        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6607            .parse()
6608            .unwrap();
6609
6610        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6611        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6612    }
6613
6614    #[test]
6615    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6616        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6617            .parse()
6618            .unwrap();
6619
6620        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6621        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6622    }
6623
6624    #[test]
6625    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6626        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6627            .parse()
6628            .unwrap();
6629
6630        parse(&spec, &input(&["ex", "run", "--clean"]))
6631            .expect("the child flag is alone within the child command");
6632        assert!(
6633            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6634            "the parent flag still conflicts with selecting the child"
6635        );
6636    }
6637
6638    #[test]
6639    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6640        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6641            .parse()
6642            .unwrap();
6643
6644        parse(&spec, &input(&["ex", "run", "--clean"]))
6645            .expect("the child-local exclusive flag is alone inside the child command");
6646        assert!(
6647            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6648            "the ancestor spelling still conflicts with selecting the child"
6649        );
6650    }
6651
6652    #[test]
6653    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6654        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
6655            .parse()
6656            .unwrap();
6657
6658        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6659        assert!(
6660            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6661            "the distinct parent declaration is still company despite sharing a name"
6662        );
6663    }
6664
6665    #[test]
6666    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6667        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6668            .parse()
6669            .unwrap();
6670
6671        parse(&spec, &input(&["ex", "run", "--clean"]))
6672            .expect("the child exclusive flag is valid alone");
6673        assert!(
6674            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6675            "merging with the inherited global must not discard child exclusivity"
6676        );
6677    }
6678
6679    #[test]
6680    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6681        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6682            .parse()
6683            .unwrap();
6684
6685        parse(&spec, &input(&["ex", "run", "--clean"]))
6686            .expect("the typed long form belongs to the child declaration");
6687        assert!(
6688            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6689            "the inherited short form still belongs to the ancestor"
6690        );
6691        assert!(
6692            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6693            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6694        );
6695    }
6696
6697    #[test]
6698    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
6699        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
6700            .parse()
6701            .unwrap();
6702
6703        parse(&spec, &input(&["ex", "run", "--clean"]))
6704            .expect("the child's spelling does not activate the orphan ancestor alias");
6705        assert!(
6706            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6707            "the inherited short alias still belongs to the ancestor exclusive flag"
6708        );
6709    }
6710
6711    #[test]
6712    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
6713        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"
6714            .parse()
6715            .unwrap();
6716
6717        assert!(
6718            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
6719            "the inherited negated alias still belongs to the ancestor exclusive flag"
6720        );
6721    }
6722
6723    #[test]
6724    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
6725        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
6726        // an unrelated inherited global already owns. That collision is resolved in the other
6727        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
6728        // the `--clean` it declared, and its exclusivity holds.
6729        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"
6730            .parse()
6731            .unwrap();
6732
6733        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
6734        assert!(
6735            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6736            "one unrelated alias collision cannot disown the child from its own flag"
6737        );
6738    }
6739
6740    #[test]
6741    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
6742        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
6743        // subcommand word the flag can only be the ancestor's, because that is the only one in
6744        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
6745        // is set. The pair below differ in nothing else, which is what makes this one rule
6746        // rather than two behaviors.
6747        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6748            .parse()
6749            .unwrap();
6750        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
6751            .expect("the ancestor owns this occurrence, and it is not exclusive");
6752        assert!(
6753            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6754            "after the subcommand word the child's declaration is in scope, and it is exclusive"
6755        );
6756
6757        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6758            .parse()
6759            .unwrap();
6760        assert!(
6761            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
6762            "the same rule, with an exclusive ancestor: selecting the child is company for it"
6763        );
6764    }
6765
6766    #[test]
6767    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
6768        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
6769        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
6770        // the ancestor ever declared, so the ancestor's answer still governs it.
6771        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
6772            .parse()
6773            .unwrap();
6774
6775        assert!(
6776            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6777            "the orphan ancestor alias is still the ancestor's exclusive flag"
6778        );
6779        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
6780            .expect("the child's own spelling drops the exclusivity the child did not restate");
6781    }
6782
6783    #[test]
6784    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
6785        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
6786        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
6787        // occurrence to the ancestor because `-c` appeared in it lost that.
6788        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6789            .parse()
6790            .unwrap();
6791
6792        assert!(
6793            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
6794            "the child spelling is exclusive whatever it was typed beside"
6795        );
6796        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
6797            .expect("the ancestor's own spelling was never exclusive");
6798    }
6799
6800    #[test]
6801    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
6802        // An environment value has no spelling to attribute, so the declaration the selected
6803        // command has in scope answers — in both directions. Comparing whole alias sets asked
6804        // the ancestor instead, because the merged flag also carries its orphan `-c`.
6805        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"
6806            .parse()
6807            .unwrap();
6808
6809        assert!(
6810            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
6811            "the child added exclusivity the environment value has to honor"
6812        );
6813
6814        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"
6815            .parse()
6816            .unwrap();
6817
6818        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
6819            .expect("the child dropped the exclusivity, and the environment value follows it");
6820    }
6821
6822    #[test]
6823    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
6824        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
6825        // the same reason the companion check does.
6826        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"
6827            .parse()
6828            .unwrap();
6829
6830        parse(&spec, &input(&["ex", "run", "--clean"]))
6831            .expect("a merged child exclusive flag is still the command's requiredness escape");
6832    }
6833
6834    #[test]
6835    fn a_group_allows_one_member_and_refuses_two() {
6836        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
6837            .parse()
6838            .unwrap();
6839
6840        // One is fine, and so is none: a plain group says "at most one".
6841        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
6842        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
6843
6844        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
6845        assert!(err.to_string().contains("group input"), "{err}");
6846    }
6847
6848    #[test]
6849    fn positional_selectors_work_in_conflicts_and_groups() {
6850        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
6851            .parse()
6852            .unwrap();
6853        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
6854            .expect("the flag alone is valid");
6855        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
6856        assert!(parse(
6857            &conflicts,
6858            &input(&["ex", "--from-file", "vars.env", "literal"])
6859        )
6860        .is_err());
6861
6862        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
6863            .parse()
6864            .unwrap();
6865        assert!(parse(
6866            &positional_source,
6867            &input(&["ex", "--from-file", "vars.env", "literal"])
6868        )
6869        .is_err());
6870
6871        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
6872            .parse()
6873            .unwrap();
6874        assert!(parse(&group, &input(&["ex"])).is_err());
6875        parse(&group, &input(&["ex", "target-name"]))
6876            .expect("a positional satisfies a required group");
6877        assert!(parse(
6878            &group,
6879            &input(&["ex", "--file", "input.txt", "target-name"])
6880        )
6881        .is_err());
6882    }
6883
6884    #[test]
6885    fn a_required_group_needs_one_of_its_members() {
6886        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6887            .parse()
6888            .unwrap();
6889
6890        let err = parse(&spec, &input(&["ex"])).unwrap_err();
6891        // The members, because that is what a user has to type; the name, because a
6892        // command with several groups would otherwise report the same sentence twice.
6893        assert!(err.to_string().contains("--file, --url"), "{err}");
6894        assert!(err.to_string().contains("input"), "{err}");
6895
6896        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
6897    }
6898
6899    #[test]
6900    fn a_multiple_group_only_polices_requiredness() {
6901        // `multiple` with `required` is "at least one of these", so two is fine and
6902        // none is not.
6903        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
6904            .parse()
6905            .unwrap();
6906
6907        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
6908        assert!(parse(&spec, &input(&["ex"])).is_err());
6909    }
6910
6911    #[test]
6912    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
6913        // The two halves of a group are two kinds of rule, and they read a default
6914        // differently on purpose. Requiredness asks whether a member has a value, and a
6915        // default is a value — the rule `requires` follows. Exclusivity asks what the
6916        // user supplied, because a defaulted member counted as supplied would collide
6917        // with the sibling they actually typed and refuse a correct command line.
6918        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6919            .parse()
6920            .unwrap();
6921
6922        parse(&spec, &input(&["ex"])).expect("the default fills the group");
6923        parse(&spec, &input(&["ex", "--url", "u"]))
6924            .expect("the default must not conflict with the flag the user typed");
6925    }
6926
6927    #[test]
6928    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
6929        // `-f` and `--file` are one flag. Counted by selector, giving it once would
6930        // report it as conflicting with itself.
6931        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
6932            .parse()
6933            .unwrap();
6934
6935        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
6936        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
6937
6938        // A genuine collision is still one.
6939        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
6940        assert!(err.to_string().contains("group input"), "{err}");
6941    }
6942
6943    #[test]
6944    fn a_group_reads_the_environment_as_given() {
6945        // The environment does count, which is the same asymmetry `conflicts` has: an
6946        // env var is somebody saying something, a default is nobody saying anything.
6947        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6948            .parse()
6949            .unwrap();
6950
6951        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
6952    }
6953
6954    #[test]
6955    fn a_requirement_names_the_flag_that_is_missing() {
6956        // Reported as the missing flag rather than as something wrong with `--out`,
6957        // which is what clap says for an unmet `requires` and what a user can act on.
6958        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6959            .parse()
6960            .unwrap();
6961
6962        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6963        assert!(
6964            err.to_string().contains("format"),
6965            "the missing flag should be named: {err}"
6966        );
6967
6968        // Satisfied, in either order.
6969        for words in [
6970            &["ex", "--out", "a.txt", "--format", "json"][..],
6971            &["ex", "--format", "json", "--out", "a.txt"][..],
6972        ] {
6973            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6974        }
6975
6976        // Nothing happens when the flag that imposes the rule is absent: a requirement
6977        // is a consequence of using the flag, not a rule about the command line.
6978        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6979    }
6980
6981    #[test]
6982    fn a_value_activates_only_its_conditional_requirement() {
6983        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"
6984            .parse()
6985            .unwrap();
6986
6987        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6988            .expect("an unrelated value requires nothing");
6989
6990        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6991        assert!(key.to_string().contains("key"), "{key}");
6992        parse(
6993            &spec,
6994            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6995        )
6996        .expect("the matching requirement is satisfied");
6997
6998        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6999        assert!(token.to_string().contains("token"), "{token}");
7000    }
7001
7002    #[test]
7003    fn conditional_requirements_read_explicit_env_but_not_defaults() {
7004        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"
7005            .parse()
7006            .unwrap();
7007        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7008        assert!(err.to_string().contains("key"), "{err}");
7009
7010        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"
7011            .parse()
7012            .unwrap();
7013        parse(&from_default, &input(&["ex"]))
7014            .expect("a default is not an explicit conditional value");
7015    }
7016
7017    #[test]
7018    fn command_line_values_override_env_for_conditional_requirements() {
7019        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7020            .parse()
7021            .unwrap();
7022
7023        parse_with_env(
7024            &spec,
7025            &["ex", "--config", "ordinary.toml"],
7026            &[("EX_CONFIG", "special.toml")],
7027        )
7028        .expect("the command-line value takes precedence over the environment");
7029    }
7030
7031    #[test]
7032    fn conditional_requirements_normalize_boolean_env_values() {
7033        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7034            .parse()
7035            .unwrap();
7036
7037        for value in ["1", "true", "True", "TRUE"] {
7038            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7039            assert!(err.to_string().contains("key"), "{value}: {err}");
7040        }
7041        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7042            .expect("a false environment value does not activate a true condition");
7043    }
7044
7045    #[test]
7046    fn a_default_satisfies_a_requirement() {
7047        // The flag it names has a value, which is the question a requirement asks. Read
7048        // any other way, `--format` would be missing here and present ten lines further
7049        // down, where plain required-ness reads the same default as filling it.
7050        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7051            .parse()
7052            .unwrap();
7053
7054        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7055            .expect("a defaulted flag is not a missing one");
7056    }
7057
7058    #[test]
7059    fn a_present_flag_binds_a_conditional_default() {
7060        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7061            .parse()
7062            .unwrap();
7063
7064        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7065        assert_eq!(
7066            with.as_env().get("usage_bin_names").map(String::as_str),
7067            Some("true")
7068        );
7069
7070        let without = parse(&spec, &input(&["ex"])).unwrap();
7071        assert!(
7072            !without.as_env().contains_key("usage_bin_names"),
7073            "IsPresent does nothing when the selector is absent"
7074        );
7075    }
7076
7077    #[test]
7078    fn an_equals_condition_binds_a_conditional_default() {
7079        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7080            .parse()
7081            .unwrap();
7082
7083        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7084        assert_eq!(
7085            json.as_env().get("usage_style").map(String::as_str),
7086            Some("pretty")
7087        );
7088        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7089        assert!(!yaml.as_env().contains_key("usage_style"));
7090    }
7091
7092    #[test]
7093    fn an_equals_condition_reads_a_negated_flag() {
7094        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7095            .parse()
7096            .unwrap();
7097
7098        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7099        assert_eq!(
7100            off.as_env().get("usage_pretty").map(String::as_str),
7101            Some("true")
7102        );
7103        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7104        assert!(
7105            !on.as_env().contains_key("usage_pretty"),
7106            "--json is true, so when=false should miss"
7107        );
7108    }
7109
7110    #[test]
7111    fn the_first_matching_conditional_default_wins() {
7112        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"
7113            .parse()
7114            .unwrap();
7115
7116        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7117        assert_eq!(
7118            out.as_env().get("usage_style").map(String::as_str),
7119            Some("compact")
7120        );
7121    }
7122
7123    #[test]
7124    fn argv_and_env_suppress_a_conditional_default() {
7125        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7126            .parse()
7127            .unwrap();
7128
7129        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7130        assert_eq!(
7131            from_env.as_env().get("usage_bin_names").map(String::as_str),
7132            Some("false"),
7133            "the target's environment wins over default_if"
7134        );
7135    }
7136
7137    #[test]
7138    fn a_sibling_env_activates_a_conditional_default() {
7139        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7140            .parse()
7141            .unwrap();
7142
7143        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7144        assert_eq!(
7145            out.as_env().get("usage_bin_names").map(String::as_str),
7146            Some("true")
7147        );
7148    }
7149
7150    #[test]
7151    fn a_default_does_not_activate_a_conditional_default() {
7152        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7153        // bind would put json's default into `out.flags` and then treat it as
7154        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7155        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7156            .parse()
7157            .unwrap();
7158
7159        let out = parse(&spec, &input(&["ex"])).unwrap();
7160        assert_eq!(
7161            out.as_env().get("usage_json").map(String::as_str),
7162            Some("true")
7163        );
7164        assert!(
7165            !out.as_env().contains_key("usage_pretty"),
7166            "a default is not an explicit value for default_if"
7167        );
7168    }
7169
7170    #[test]
7171    fn a_conditional_default_does_not_activate_requires_if() {
7172        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"
7173            .parse()
7174            .unwrap();
7175
7176        parse(&spec, &input(&["ex", "--json"]))
7177            .expect("a default_if value is not explicit for requires_if");
7178        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7179    }
7180
7181    #[test]
7182    fn a_conditional_default_satisfies_a_requirement() {
7183        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7184            .parse()
7185            .unwrap();
7186
7187        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7188            .expect("default_if fills the required flag");
7189        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7190    }
7191
7192    #[test]
7193    fn an_environment_value_satisfies_a_requirement() {
7194        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7195            .parse()
7196            .unwrap();
7197
7198        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7199        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7200            .expect("the environment supplies it");
7201    }
7202
7203    #[test]
7204    fn a_requirement_is_satisfied_by_a_short_form() {
7205        // The selector may spell the other flag any way it answers to, so the check
7206        // resolves it the way every other selector is resolved rather than matching
7207        // text. The error names the flag, not the selector.
7208        let spec: Spec =
7209            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7210                .parse()
7211                .unwrap();
7212
7213        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7214
7215        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7216        assert!(err.to_string().contains("key"), "{err}");
7217    }
7218
7219    #[test]
7220    fn conflicting_flags_are_rejected_in_either_order() {
7221        // Declared once, on `--file`, which is all clap exposes — so the check has to
7222        // be order-independent by looking at every flag that was given rather than at
7223        // the one that declared the conflict.
7224        let spec: Spec =
7225            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7226                .parse()
7227                .unwrap();
7228
7229        for words in [
7230            &["ex", "--file", "a.txt", "--stdin"][..],
7231            &["ex", "--stdin", "--file", "a.txt"][..],
7232        ] {
7233            let err = parse(&spec, &input(words)).unwrap_err();
7234            assert!(
7235                err.to_string().contains("conflicts with --stdin"),
7236                "{words:?} should be refused: {err}"
7237            );
7238        }
7239
7240        // Either one alone is fine.
7241        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7242        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7243    }
7244
7245    #[test]
7246    fn unknown_flags_are_values_by_default() {
7247        // The default, and the reason it is the default: a spec often parses a
7248        // command line whose flags belong to something else.
7249        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7250            .parse()
7251            .unwrap();
7252        let out = parse(
7253            &spec,
7254            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7255        )
7256        .unwrap();
7257        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7258        assert_eq!(out.args[rest].to_string(), "--wat x");
7259    }
7260
7261    #[test]
7262    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7263        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7264            .parse()
7265            .unwrap();
7266        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7267            .expect("a repeat is a correction by default");
7268        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7269        assert_eq!(out.flags[jobs].to_string(), "2");
7270        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7271            .expect("switches use the same default");
7272
7273        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7274            .parse()
7275            .unwrap();
7276        for words in [
7277            &["ex", "--jobs", "1", "--jobs", "2"][..],
7278            &["ex", "--verbose", "--verbose"][..],
7279        ] {
7280            let err = parse(&strict, &input(words)).unwrap_err();
7281            assert!(
7282                err.to_string().contains("cannot be used multiple times"),
7283                "{err}"
7284            );
7285        }
7286
7287        let reparsed: Spec = strict.to_string().parse().unwrap();
7288        assert!(!reparsed.cmd.args_override_self);
7289    }
7290
7291    #[test]
7292    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7293        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7294            .parse()
7295            .unwrap();
7296
7297        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7298            .expect("opposite forms override each other");
7299        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7300        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7301
7302        for words in [
7303            &["ex", "--color", "--color"][..],
7304            &["ex", "--no-color", "--no-color"][..],
7305        ] {
7306            let err = parse(&spec, &input(words)).unwrap_err();
7307            assert!(err.to_string().contains("cannot be used multiple times"));
7308        }
7309    }
7310
7311    #[test]
7312    fn strict_global_flags_may_repeat_across_command_levels() {
7313        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"
7314            .parse()
7315            .unwrap();
7316
7317        let out = parse(
7318            &spec,
7319            &input(&[
7320                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7321            ]),
7322        )
7323        .expect("an inherited global is allowed once at each command level");
7324        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7325        assert_eq!(out.flags[jobs].to_string(), "2");
7326
7327        for words in [
7328            &["ex", "--color", "--color", "run", "--no-color"][..],
7329            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7330        ] {
7331            let err = parse(&spec, &input(words)).unwrap_err();
7332            assert!(err.to_string().contains("cannot be used multiple times"));
7333        }
7334    }
7335
7336    #[test]
7337    fn a_subcommand_can_negate_only_its_parents_requirements() {
7338        let base = r#"name "ex"
7339bin "ex"
7340flag "--config" required=#true
7341flag "--mode" requires="--config"
7342flag "--other"
7343arg "<input>"
7344group "source" "--config" "--other" required=#true
7345cmd "run" { flag "--child" required=#true }
7346"#;
7347        let strict: Spec = base.parse().unwrap();
7348        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7349        let message = err.to_string();
7350        assert!(
7351            message.contains("input") || message.contains("config"),
7352            "{message}"
7353        );
7354
7355        let negated: Spec = base
7356            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7357            .parse()
7358            .unwrap();
7359        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7360        assert!(
7361            err.to_string().contains("child"),
7362            "the selected command keeps its own requirements: {err}"
7363        );
7364
7365        let mut child_optional = negated.clone();
7366        child_optional.cmd.subcommands["run"].flags[0].required = false;
7367        parse(&child_optional, &input(&["ex", "run"]))
7368            .expect("the child selection satisfies all parent requirements");
7369        parse(&child_optional, &input(&["ex", "--mode", "run"]))
7370            .expect("parent requires relationships are negated too");
7371    }
7372
7373    #[test]
7374    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7375        let spec: Spec = r#"name "ex"
7376bin "ex"
7377args_conflicts_with_subcommands #true
7378flag "--verbose"
7379cmd "run"
7380"#
7381        .parse()
7382        .unwrap();
7383
7384        parse(&spec, &input(&["ex", "run"]))
7385            .expect("the subcommand is valid without a parent argument");
7386        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7387        assert!(
7388            err.to_string().contains("cannot be used with arguments"),
7389            "{err}"
7390        );
7391    }
7392
7393    #[test]
7394    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7395        let base = r#"name "ex"
7396bin "ex"
7397flag "--values <value>..."
7398cmd "run"
7399"#;
7400        let plain: Spec = base.parse().unwrap();
7401        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7402        assert_eq!(out.cmd.name, "ex");
7403
7404        let precedence: Spec = base
7405            .replacen(
7406                "bin \"ex\"",
7407                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7408                1,
7409            )
7410            .parse()
7411            .unwrap();
7412        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7413        assert_eq!(out.cmd.name, "run");
7414    }
7415
7416    #[test]
7417    fn a_required_positional_can_follow_an_unfilled_optional_one() {
7418        let base = r#"name "ex"
7419bin "ex"
7420arg "[optional]"
7421arg "<required>"
7422"#;
7423        let plain: Spec = base.parse().unwrap();
7424        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7425        assert!(err.to_string().contains("required"), "{err}");
7426
7427        let enabled: Spec = base
7428            .replacen(
7429                "bin \"ex\"",
7430                "bin \"ex\"\nallow_missing_positional #true",
7431                1,
7432            )
7433            .parse()
7434            .unwrap();
7435        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7436        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7437        let value = &out
7438            .args
7439            .iter()
7440            .find(|(arg, _)| arg.name == "required")
7441            .unwrap()
7442            .1;
7443        assert!(matches!(value, ParseValue::String(value) if value == "value"));
7444    }
7445
7446    #[test]
7447    fn sigil_args_do_not_block_optional_positional_skipping() {
7448        let spec: Spec = r#"name "ex"
7449bin "ex"
7450allow_missing_positional #true
7451arg "[optional]"
7452arg "[tool]" sigil="@"
7453arg "<required>"
7454"#
7455        .parse()
7456        .unwrap();
7457
7458        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7459        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7460        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7461        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7462        assert_eq!(out.args[tool].to_string(), "node");
7463        assert_eq!(out.args[required].to_string(), "value");
7464    }
7465
7466    #[test]
7467    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7468        let spec: Spec =
7469            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7470                .parse()
7471                .unwrap();
7472        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7473        assert!(
7474            err.to_string().contains("--wat"),
7475            "the message should name the token: {err}"
7476        );
7477
7478        // The positional opts into the narrower negative-number carve-out without
7479        // accepting arbitrary unknown flags.
7480        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
7481        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7482        assert_eq!(out.args[rest].to_string(), "-1");
7483
7484        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
7485        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7486        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7487    }
7488
7489    #[test]
7490    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
7491        let spec: Spec = r#"
7492name "ex"
7493bin "ex"
7494unknown_flags "error"
7495flag "-0 --print0" global=#true
7496cmd "run" {
7497  flag "--force"
7498}
7499"#
7500        .parse()
7501        .unwrap();
7502        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
7503        assert_eq!(out.cmd.name, "run");
7504        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7505        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
7506        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7507        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
7508    }
7509
7510    #[test]
7511    fn a_command_may_override_the_cli_wide_setting() {
7512        // Strict overall, lenient for the one command that forwards options.
7513        let spec: Spec = r#"
7514name "ex"
7515bin "ex"
7516unknown_flags "error"
7517cmd "exec" unknown_flags="value" {
7518  arg "[rest]..."
7519}
7520cmd "build" {
7521  arg "[rest]..."
7522}
7523"#
7524        .parse()
7525        .unwrap();
7526
7527        let out = parse(
7528            &spec,
7529            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
7530        )
7531        .unwrap();
7532        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7533        assert_eq!(out.args[rest].to_string(), "--wat");
7534
7535        assert!(
7536            parse(
7537                &spec,
7538                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
7539            )
7540            .is_err(),
7541            "a command that says nothing inherits the CLI's choice"
7542        );
7543    }
7544
7545    #[test]
7546    fn the_setting_survives_a_round_trip() {
7547        let spec: Spec =
7548            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7549                .parse()
7550                .unwrap();
7551        let reparsed: Spec = spec.to_string().parse().unwrap();
7552        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7553        assert_eq!(
7554            reparsed.cmd.subcommands["x"].unknown_flags,
7555            Some(UnknownFlags::Value)
7556        );
7557    }
7558
7559    #[test]
7560    fn test_default_subcommand() {
7561        // Test that default_subcommand routes to the specified subcommand
7562        let run_cmd = SpecCommand::builder()
7563            .name("run")
7564            .arg(SpecArg::builder().name("task").build())
7565            .build();
7566        let mut cmd = SpecCommand::builder().name("test").build();
7567        cmd.subcommands.insert("run".to_string(), run_cmd);
7568
7569        let spec = Spec {
7570            name: "test".to_string(),
7571            bin: "test".to_string(),
7572            cmd,
7573            default_subcommand: Some("run".to_string()),
7574            ..Default::default()
7575        };
7576
7577        // "test mytask" should be parsed as if it were "test run mytask"
7578        let input = vec!["test".to_string(), "mytask".to_string()];
7579        let parsed = parse(&spec, &input).unwrap();
7580
7581        // Should have two commands: root and "run"
7582        assert_eq!(parsed.cmds.len(), 2);
7583        assert_eq!(parsed.cmds[1].name, "run");
7584
7585        // Should have parsed the task argument
7586        assert_eq!(parsed.args.len(), 1);
7587        let arg = parsed.args.keys().next().unwrap();
7588        assert_eq!(arg.name, "task");
7589        let value = parsed.args.values().next().unwrap();
7590        assert_eq!(value.to_string(), "mytask");
7591    }
7592
7593    #[test]
7594    fn default_subcommand_outranks_root_sigil_arg() {
7595        let spec: Spec = r#"
7596name "test"
7597bin "test"
7598default_subcommand "run"
7599arg "[tools]..." sigil="+"
7600cmd "run" { arg "<task>" }
7601"#
7602        .parse()
7603        .unwrap();
7604
7605        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7606        assert_eq!(parsed.cmd.name, "run");
7607        let value = |name| {
7608            parsed
7609                .args
7610                .iter()
7611                .find(|(arg, _)| arg.name == name)
7612                .map(|(_, value)| value.to_string())
7613                .unwrap()
7614        };
7615        assert_eq!(value("tools"), "node");
7616        assert_eq!(value("task"), "node");
7617    }
7618
7619    #[test]
7620    fn test_default_subcommand_explicit_still_works() {
7621        // Test that explicit subcommand takes precedence
7622        let run_cmd = SpecCommand::builder()
7623            .name("run")
7624            .arg(SpecArg::builder().name("task").build())
7625            .build();
7626        let other_cmd = SpecCommand::builder()
7627            .name("other")
7628            .arg(SpecArg::builder().name("other_arg").build())
7629            .build();
7630        let mut cmd = SpecCommand::builder().name("test").build();
7631        cmd.subcommands.insert("run".to_string(), run_cmd);
7632        cmd.subcommands.insert("other".to_string(), other_cmd);
7633
7634        let spec = Spec {
7635            name: "test".to_string(),
7636            bin: "test".to_string(),
7637            cmd,
7638            default_subcommand: Some("run".to_string()),
7639            ..Default::default()
7640        };
7641
7642        // "test other foo" should use "other" subcommand, not default
7643        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7644        let parsed = parse(&spec, &input).unwrap();
7645
7646        // Should have used "other" subcommand
7647        assert_eq!(parsed.cmds.len(), 2);
7648        assert_eq!(parsed.cmds[1].name, "other");
7649    }
7650
7651    #[test]
7652    fn test_default_subcommand_applies_only_at_the_root() {
7653        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
7654        // was being looked up wherever the parser happened to be standing, so a command with
7655        // an unrelated subcommand of the same name acquired a default of its own: with
7656        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
7657        // `zzz` there. Nothing declared that, and nothing could have.
7658        let mut config_ls = SpecCommand::builder().name("ls").build();
7659        config_ls.args.push(SpecArg::builder().name("what").build());
7660        let mut config_cmd = SpecCommand::builder().name("config").build();
7661        config_cmd.subcommands.insert("ls".to_string(), config_ls);
7662
7663        // The root's own `ls`, which is what its default points at. It takes an argument so
7664        // that a routed word has somewhere to land.
7665        let mut root_ls = SpecCommand::builder().name("ls").build();
7666        root_ls.args.push(SpecArg::builder().name("what").build());
7667        let mut cmd = SpecCommand::builder().name("ex").build();
7668        cmd.subcommands.insert("ls".to_string(), root_ls);
7669        cmd.subcommands.insert("config".to_string(), config_cmd);
7670
7671        let spec = Spec {
7672            name: "ex".to_string(),
7673            bin: "ex".to_string(),
7674            cmd,
7675            default_subcommand: Some("ls".to_string()),
7676            ..Default::default()
7677        };
7678
7679        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
7680        // own business — and `config` takes no argument, so this is an error rather than a
7681        // silent descent.
7682        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7683        assert!(
7684            parse(&spec, &input).is_err(),
7685            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7686        );
7687
7688        // At the root, where it is declared, it still applies.
7689        let input = vec!["ex".to_string(), "zzz".to_string()];
7690        let parsed = parse(&spec, &input).expect("the root's default applies");
7691        assert_eq!(
7692            parsed
7693                .cmds
7694                .iter()
7695                .map(|c| c.name.as_str())
7696                .collect::<Vec<_>>(),
7697            ["ex", "ls"]
7698        );
7699        assert_eq!(
7700            parsed.args.values().next().map(|v| v.to_string()),
7701            Some("zzz".to_string()),
7702            "and the word binds inside the command it reached"
7703        );
7704    }
7705
7706    #[test]
7707    fn test_default_subcommand_with_nested_subcommands() {
7708        // Test that default_subcommand works when the default subcommand has nested subcommands.
7709        // This is the mise use case: "mise say" should be parsed as "mise run say"
7710        // where "say" is a subcommand of "run" (a task).
7711        let say_cmd = SpecCommand::builder()
7712            .name("say")
7713            .arg(SpecArg::builder().name("name").build())
7714            .build();
7715        let mut run_cmd = SpecCommand::builder().name("run").build();
7716        run_cmd.subcommands.insert("say".to_string(), say_cmd);
7717
7718        let mut cmd = SpecCommand::builder().name("test").build();
7719        cmd.subcommands.insert("run".to_string(), run_cmd);
7720
7721        let spec = Spec {
7722            name: "test".to_string(),
7723            bin: "test".to_string(),
7724            cmd,
7725            default_subcommand: Some("run".to_string()),
7726            ..Default::default()
7727        };
7728
7729        // "test say hello" should be parsed as "test run say hello"
7730        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
7731        let parsed = parse(&spec, &input).unwrap();
7732
7733        // Should have three commands: root, "run", and "say"
7734        assert_eq!(parsed.cmds.len(), 3);
7735        assert_eq!(parsed.cmds[0].name, "test");
7736        assert_eq!(parsed.cmds[1].name, "run");
7737        assert_eq!(parsed.cmds[2].name, "say");
7738
7739        // Should have parsed the "name" argument
7740        assert_eq!(parsed.args.len(), 1);
7741        let arg = parsed.args.keys().next().unwrap();
7742        assert_eq!(arg.name, "name");
7743        let value = parsed.args.values().next().unwrap();
7744        assert_eq!(value.to_string(), "hello");
7745    }
7746
7747    /// Build a spec equivalent to the post-mount structure produced by mise's
7748    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
7749    /// subcommand that re-declares the same flag as NON-global, and a mounted task
7750    /// (`sample:run`) carrying a positional arg with `choices`.
7751    ///
7752    /// We construct the merged structure directly instead of executing a real mount so the
7753    /// test stays hermetic and cross-platform while still exercising the parser defect.
7754    fn mounted_global_flag_spec() -> Spec {
7755        let task_cmd = SpecCommand::builder()
7756            .name("sample:run")
7757            .arg(
7758                SpecArg::builder()
7759                    .name("profile")
7760                    .choices(["alpha", "beta", "gamma"])
7761                    .build(),
7762            )
7763            .build();
7764        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
7765        let mut run_cmd = SpecCommand::builder()
7766            .name("run")
7767            .flag(
7768                SpecFlag::builder()
7769                    .name("cd")
7770                    .short('C')
7771                    .long("cd")
7772                    .arg(SpecArg::builder().name("dir").build())
7773                    .global(false)
7774                    .build(),
7775            )
7776            .build();
7777        run_cmd
7778            .subcommands
7779            .insert("sample:run".to_string(), task_cmd);
7780
7781        let mut cmd = SpecCommand::builder()
7782            .name("test")
7783            .flag(
7784                SpecFlag::builder()
7785                    .name("cd")
7786                    .short('C')
7787                    .long("cd")
7788                    .arg(SpecArg::builder().name("dir").build())
7789                    .global(true)
7790                    .build(),
7791            )
7792            .build();
7793        cmd.subcommands.insert("run".to_string(), run_cmd);
7794
7795        Spec {
7796            name: "test".to_string(),
7797            bin: "test".to_string(),
7798            cmd,
7799            ..Default::default()
7800        }
7801    }
7802
7803    #[test]
7804    fn test_prefix_global_flag_does_not_pollute_choices() {
7805        // Regression for the parser-side root cause referenced by jdx/mise#10069.
7806        //
7807        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
7808        // then into the mounted `sample:run`) used to drop the inherited global flag from
7809        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
7810        // mis-validated against the task's `choices` positional arg.
7811        let spec = mounted_global_flag_spec();
7812
7813        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
7814        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
7815        for words in [
7816            &["test", "-C", "/tmp", "run", "sample:run"][..],
7817            // Embedded-value form must behave identically.
7818            &["test", "--cd=/tmp", "run", "sample:run"][..],
7819        ] {
7820            let parsed = parse_partial(&spec, &input(words)).unwrap();
7821            assert_eq!(
7822                parsed
7823                    .cmds
7824                    .iter()
7825                    .map(|c| c.name.as_str())
7826                    .collect::<Vec<_>>(),
7827                vec!["test", "run", "sample:run"],
7828            );
7829            // No positional arg should have been consumed by the leftover global-flag tokens.
7830            assert!(
7831                parsed.args.is_empty(),
7832                "args should be empty, got {:?}",
7833                parsed.args
7834            );
7835
7836            // Fix (B): the inherited global flag survives the descent even though `run`
7837            // re-declares `-C/--cd` as non-global.
7838            let cd = parsed
7839                .available_flags
7840                .get("--cd")
7841                .expect("--cd should remain available after descending into the subcommand");
7842            assert!(cd.global, "--cd must stay global after descent");
7843            assert!(
7844                parsed.available_flags.get("-C").is_some_and(|f| f.global),
7845                "-C must stay global after descent",
7846            );
7847
7848            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
7849            // for normal execution and for the env passed to mount scripts. (Removing the
7850            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
7851            assert_eq!(
7852                parsed.as_env().get("usage_cd").map(String::as_str),
7853                Some("/tmp"),
7854                "global flag value must survive in as_env(), got {:?}",
7855                parsed.as_env(),
7856            );
7857        }
7858
7859        // A real, valid choice still parses through the global flag prefix.
7860        let parsed = parse_partial(
7861            &spec,
7862            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
7863        )
7864        .unwrap();
7865        assert_eq!(parsed.args.len(), 1);
7866        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7867
7868        // And genuinely invalid choices are still rejected (we didn't disable validation).
7869        assert_parse_err(
7870            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
7871            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7872        );
7873    }
7874
7875    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
7876    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
7877    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
7878    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
7879    fn mounted_orphan_short_spec() -> Spec {
7880        let task_cmd = SpecCommand::builder()
7881            .name("sample:run")
7882            .arg(
7883                SpecArg::builder()
7884                    .name("profile")
7885                    .choices(["alpha", "beta", "gamma"])
7886                    .build(),
7887            )
7888            .build();
7889        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
7890        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
7891        let mut run_cmd = SpecCommand::builder()
7892            .name("run")
7893            .flag(
7894                SpecFlag::builder()
7895                    .name("raw")
7896                    .short('r')
7897                    .long("raw")
7898                    .global(false)
7899                    .build(),
7900            )
7901            .flag(
7902                SpecFlag::builder()
7903                    .name("force")
7904                    .short('f')
7905                    .long("force")
7906                    .global(false)
7907                    .build(),
7908            )
7909            .build();
7910        run_cmd
7911            .subcommands
7912            .insert("sample:run".to_string(), task_cmd);
7913
7914        // Root global is LONG-ONLY: `--raw` with no short.
7915        let mut cmd = SpecCommand::builder()
7916            .name("test")
7917            .flag(
7918                SpecFlag::builder()
7919                    .name("raw")
7920                    .long("raw")
7921                    .global(true)
7922                    .build(),
7923            )
7924            .build();
7925        cmd.subcommands.insert("run".to_string(), run_cmd);
7926
7927        Spec {
7928            name: "test".to_string(),
7929            bin: "test".to_string(),
7930            cmd,
7931            ..Default::default()
7932        }
7933    }
7934
7935    #[test]
7936    fn test_orphan_short_alias_survives_merge() {
7937        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
7938        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
7939        // added short `-r` must be unioned onto the surviving inherited global flag instead of
7940        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
7941        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
7942        let spec = mounted_orphan_short_spec();
7943
7944        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
7945        assert_eq!(
7946            parsed
7947                .cmds
7948                .iter()
7949                .map(|c| c.name.as_str())
7950                .collect::<Vec<_>>(),
7951            vec!["test", "run", "sample:run"],
7952        );
7953
7954        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
7955        // and the original long `--raw` is still global too.
7956        assert!(
7957            parsed.available_flags.get("-r").is_some_and(|f| f.global),
7958            "-r must be merged onto the inherited global flag and stay global after descent",
7959        );
7960        assert!(
7961            parsed
7962                .available_flags
7963                .get("--raw")
7964                .is_some_and(|f| f.global),
7965            "--raw must stay global after descent",
7966        );
7967
7968        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
7969        assert!(
7970            parsed.args.is_empty(),
7971            "args should be empty, got {:?}",
7972            parsed.args
7973        );
7974
7975        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
7976        assert_eq!(
7977            parsed.as_env().get("usage_raw").map(String::as_str),
7978            Some("true"),
7979            "merged short's value must survive in as_env(), got {:?}",
7980            parsed.as_env(),
7981        );
7982
7983        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
7984        // promoted/merged — it is correctly dropped when descending into the mount.
7985        assert!(
7986            !parsed.available_flags.contains_key("-f"),
7987            "purely-local -f must not be promoted onto a global",
7988        );
7989        assert!(
7990            !parsed.available_flags.contains_key("--force"),
7991            "purely-local --force must not be promoted onto a global",
7992        );
7993
7994        // A real, valid choice still parses through the merged short prefix.
7995        let parsed =
7996            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
7997        assert_eq!(parsed.args.len(), 1);
7998        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7999
8000        // And genuinely invalid choices are still rejected.
8001        assert_parse_err(
8002            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8003            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8004        );
8005    }
8006
8007    #[test]
8008    fn test_orphan_short_does_not_clobber_unrelated_global() {
8009        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
8010        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
8011        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
8012        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
8013        let run_cmd = SpecCommand::builder()
8014            .name("run")
8015            .flag(
8016                SpecFlag::builder()
8017                    .name("raw")
8018                    .short('r')
8019                    .long("raw")
8020                    .global(false)
8021                    .build(),
8022            )
8023            .build();
8024        let mut cmd = SpecCommand::builder()
8025            .name("test")
8026            .flag(
8027                SpecFlag::builder()
8028                    .name("raw")
8029                    .long("raw")
8030                    .global(true)
8031                    .build(),
8032            )
8033            .flag(
8034                SpecFlag::builder()
8035                    .name("restrict")
8036                    .short('r')
8037                    .long("restrict")
8038                    .global(true)
8039                    .build(),
8040            )
8041            .build();
8042        cmd.subcommands.insert("run".to_string(), run_cmd);
8043        let spec = Spec {
8044            name: "test".to_string(),
8045            bin: "test".to_string(),
8046            cmd,
8047            ..Default::default()
8048        };
8049
8050        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8051        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8052        assert_eq!(
8053            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8054            Some("restrict"),
8055            "-r must remain owned by the unrelated global it already belonged to",
8056        );
8057        // Both globals are still recognized and global after the descent.
8058        assert!(parsed
8059            .available_flags
8060            .get("--raw")
8061            .is_some_and(|f| f.global));
8062        assert!(parsed
8063            .available_flags
8064            .get("--restrict")
8065            .is_some_and(|f| f.global));
8066    }
8067
8068    #[test]
8069    fn test_redeclared_global_aliases_share_one_flag() {
8070        // A global declared with BOTH a short and a long, re-declared non-globally by a
8071        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8072        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8073        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8074        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8075        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8076        let spec = r#"
8077flag "-y --yes" global=#true effect="write"
8078cmd "run" {
8079    flag "-y --yes --assume-yes"
8080}
8081"#
8082        .parse::<Spec>()
8083        .unwrap();
8084
8085        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8086
8087        for key in ["-y", "--yes", "--assume-yes"] {
8088            let flag = parsed
8089                .available_flags
8090                .get(key)
8091                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8092            assert!(flag.global, "{key} must stay global after the descent");
8093            assert_eq!(
8094                flag.long,
8095                vec!["yes".to_string(), "assume-yes".to_string()],
8096                "{key} must resolve to the flag carrying every alias",
8097            );
8098            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8099        }
8100
8101        // One logical flag means one object: all three keys share a single `Arc`.
8102        assert_eq!(
8103            unique_flags(parsed.available_flags.values()).count(),
8104            1,
8105            "all aliases must point at one flag object, got {:?}",
8106            parsed.available_flags,
8107        );
8108
8109        // The global's effect survives the merge, so `-y` still marks the command as writing.
8110        assert_eq!(
8111            parsed.available_flags["-y"].effect,
8112            Some(crate::SpecCommandEffect::Write),
8113        );
8114    }
8115
8116    #[test]
8117    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8118        let spec = r#"
8119flag "--yes" global=#true {
8120    alias "-q" "--quietly" hide=#true
8121}
8122cmd "run" {
8123    flag "--yes --assume-yes" {
8124        alias "-s" "--secret" hide=#true
8125    }
8126}
8127"#
8128        .parse::<Spec>()
8129        .unwrap();
8130
8131        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8132        let merged = &parsed.available_flags["--yes"];
8133        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8134        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8135        for key in ["-q", "-s", "--quietly", "--secret"] {
8136            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8137        }
8138    }
8139
8140    #[test]
8141    fn test_redeclared_global_can_promote_hidden_aliases() {
8142        let spec = r#"
8143flag "--yes" global=#true {
8144    alias "-q" "--quietly" hide=#true
8145}
8146cmd "run" {
8147    flag "-q --yes --quietly"
8148}
8149"#
8150        .parse::<Spec>()
8151        .unwrap();
8152
8153        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8154        let merged = &parsed.available_flags["--yes"];
8155        assert!(merged.hidden_short_aliases.is_empty());
8156        assert!(merged.hidden_aliases.is_empty());
8157        for key in ["-q", "--quietly"] {
8158            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8159        }
8160    }
8161
8162    #[test]
8163    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8164        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8165        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8166        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8167        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8168        // pre-merge global and miss the added `assume-yes`.
8169        let spec = r#"
8170flag "-y --yes --confirm" global=#true
8171cmd "run" {
8172    flag "--yes --assume-yes"
8173}
8174"#
8175        .parse::<Spec>()
8176        .unwrap();
8177
8178        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8179
8180        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8181            let flag = parsed
8182                .available_flags
8183                .get(key)
8184                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8185            assert!(flag.global, "{key} must stay global after the descent");
8186            assert_eq!(
8187                flag.long,
8188                vec![
8189                    "yes".to_string(),
8190                    "confirm".to_string(),
8191                    "assume-yes".to_string()
8192                ],
8193                "{key} must resolve to the flag carrying every alias",
8194            );
8195        }
8196
8197        assert_eq!(
8198            unique_flags(parsed.available_flags.values()).count(),
8199            1,
8200            "all aliases must point at one flag object, got {:?}",
8201            parsed.available_flags,
8202        );
8203    }
8204
8205    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8206    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8207    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8208    ///
8209    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8210    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8211    fn mounted_task_flag_spec() -> Spec {
8212        let mut task_cmd = SpecCommand::builder()
8213            .name("mytask")
8214            .flag(
8215                SpecFlag::builder()
8216                    .name("env")
8217                    .long("env")
8218                    .arg(
8219                        SpecArg::builder()
8220                            .name("name")
8221                            .choices(["dev", "stage", "prod"])
8222                            .build(),
8223                    )
8224                    .global(false)
8225                    .build(),
8226            )
8227            .flag(
8228                SpecFlag::builder()
8229                    .name("bump")
8230                    .long("bump")
8231                    .arg(
8232                        SpecArg::builder()
8233                            .name("type")
8234                            .choices(["auto", "major"])
8235                            .build(),
8236                    )
8237                    .global(false)
8238                    .build(),
8239            )
8240            .build();
8241        task_cmd.mounted = true;
8242
8243        let mut run_cmd = SpecCommand::builder()
8244            .name("run")
8245            .flag(
8246                SpecFlag::builder()
8247                    .name("force")
8248                    .short('f')
8249                    .long("force")
8250                    .global(false)
8251                    .build(),
8252            )
8253            .build();
8254        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8255
8256        let mut cmd = SpecCommand::builder()
8257            .name("test")
8258            .flag(
8259                SpecFlag::builder()
8260                    .name("env")
8261                    .short('E')
8262                    .long("env")
8263                    .arg(SpecArg::builder().name("ENV").build())
8264                    .global(true)
8265                    .build(),
8266            )
8267            .flag(
8268                SpecFlag::builder()
8269                    .name("silent")
8270                    .long("silent")
8271                    .global(true)
8272                    .build(),
8273            )
8274            .build();
8275        cmd.subcommands.insert("run".to_string(), run_cmd);
8276
8277        Spec {
8278            name: "test".to_string(),
8279            bin: "test".to_string(),
8280            cmd,
8281            ..Default::default()
8282        }
8283    }
8284
8285    #[test]
8286    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8287        // The mounted program's own commands are ordinary commands relative to each other, so
8288        // descending *within* the mounted tree must follow the normal rules — including keeping
8289        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8290        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8291        // global, which the next descent's `retain(global)` then dropped entirely.
8292        let deep = SpecCommand::builder().name("deep").build();
8293        let mut sub = SpecCommand::builder()
8294            .name("sub")
8295            // Re-declares the mounted program's own global as non-global.
8296            .flag(
8297                SpecFlag::builder()
8298                    .name("cd")
8299                    .short('C')
8300                    .long("cd")
8301                    .arg(SpecArg::builder().name("dir").build())
8302                    .global(false)
8303                    .build(),
8304            )
8305            .build();
8306        sub.subcommands.insert("deep".to_string(), deep);
8307        let mut task = SpecCommand::builder()
8308            .name("task")
8309            .flag(
8310                SpecFlag::builder()
8311                    .name("cd")
8312                    .short('C')
8313                    .long("cd")
8314                    .arg(SpecArg::builder().name("dir").build())
8315                    .global(true)
8316                    .build(),
8317            )
8318            .build();
8319        task.subcommands.insert("sub".to_string(), sub);
8320        task.mark_mounted();
8321
8322        let mut run_cmd = SpecCommand::builder().name("run").build();
8323        run_cmd.subcommands.insert("task".to_string(), task);
8324        let mut cmd = SpecCommand::builder().name("test").build();
8325        cmd.subcommands.insert("run".to_string(), run_cmd);
8326        let spec = Spec {
8327            name: "test".to_string(),
8328            bin: "test".to_string(),
8329            cmd,
8330            ..Default::default()
8331        };
8332
8333        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8334        assert!(
8335            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8336            "the mounted program's own global must survive descents inside the mounted tree",
8337        );
8338        assert!(
8339            parsed.completion_flags().contains_key("--cd"),
8340            "and must still be offered there: it belongs to the mounted program",
8341        );
8342        assert!(
8343            parsed.completion_flags().contains_key("-C"),
8344            "including the short the nested command re-declared",
8345        );
8346    }
8347
8348    #[test]
8349    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8350        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
8351        // into the command the mount sits on. They belong to the mounted program, so they must
8352        // be offered inside the mounted commands rather than filtered out with the mounting
8353        // CLI's own flags.
8354        let mut task = SpecCommand::builder()
8355            .name("task")
8356            .flag(
8357                SpecFlag::builder()
8358                    .name("bump")
8359                    .long("bump")
8360                    .global(false)
8361                    .build(),
8362            )
8363            .build();
8364        task.mark_mounted();
8365
8366        let mut run_cmd = SpecCommand::builder().name("run").build();
8367        run_cmd.subcommands.insert("task".to_string(), task);
8368        // What `mount()` leaves behind when the mounted spec's root declares flags.
8369        run_cmd.flags = vec![
8370            SpecFlag::builder()
8371                .name("tglobal")
8372                .long("tglobal")
8373                .global(true)
8374                .build(),
8375            SpecFlag::builder()
8376                .name("tlocal")
8377                .long("tlocal")
8378                .global(false)
8379                .build(),
8380        ];
8381        run_cmd.flags_from_mount = true;
8382
8383        let mut cmd = SpecCommand::builder()
8384            .name("test")
8385            .flag(
8386                SpecFlag::builder()
8387                    .name("silent")
8388                    .long("silent")
8389                    .global(true)
8390                    .build(),
8391            )
8392            .build();
8393        cmd.subcommands.insert("run".to_string(), run_cmd);
8394        let spec = Spec {
8395            name: "test".to_string(),
8396            bin: "test".to_string(),
8397            cmd,
8398            ..Default::default()
8399        };
8400
8401        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8402        assert_eq!(
8403            parsed.completion_flags().keys().collect::<Vec<_>>(),
8404            vec!["--bump", "--tglobal"],
8405            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8406             `--silent` does not, and the mount's non-global root flag is not inherited",
8407        );
8408    }
8409
8410    #[test]
8411    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8412        // Regression for jdx/mise#11282. A mounted command describes another program, which
8413        // does not accept the mounting CLI's globals (mise forwards everything after a task
8414        // name to the task). They must stay recognized — they may appear before the mounted
8415        // command — but must not be offered in completions there.
8416        let spec = mounted_task_flag_spec();
8417        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8418
8419        // Still recognized for parsing...
8420        assert!(parsed.available_flags.contains_key("--silent"));
8421        assert!(parsed.available_flags.contains_key("-E"));
8422        // ...but belonging to a command above the mount, so not offered.
8423        assert_eq!(
8424            parsed.completion_flags().keys().collect::<Vec<_>>(),
8425            vec!["--bump", "--env"],
8426            "only the mounted command's own flags may be offered",
8427        );
8428
8429        // `run`'s own non-global flag is dropped on descent, as it always was.
8430        assert!(!parsed.available_flags.contains_key("--force"));
8431    }
8432
8433    #[test]
8434    fn test_mounted_cmd_flag_wins_over_inherited_global() {
8435        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
8436        // by the root's `--env` global, so completing its value fell back to file completion.
8437        let spec = mounted_task_flag_spec();
8438        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8439
8440        let awaiting = parsed
8441            .flag_awaiting_value
8442            .first()
8443            .expect("--env should await a value");
8444        assert_eq!(
8445            awaiting
8446                .arg
8447                .as_ref()
8448                .and_then(|a| a.choices.as_ref())
8449                .map(|c| c.choices.clone()),
8450            Some(vec![
8451                "dev".to_string(),
8452                "stage".to_string(),
8453                "prod".to_string()
8454            ]),
8455            "the mounted command's own --env must win over the inherited global",
8456        );
8457
8458        // The global's short is not declared by the mounted command, so it keeps pointing at
8459        // the global and a value passed before the mounted command still parses.
8460        let parsed =
8461            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8462        assert!(
8463            parsed.args.is_empty(),
8464            "prefix global tokens must not be consumed as positionals, got {:?}",
8465            parsed.args
8466        );
8467        assert_eq!(
8468            parsed.as_env().get("usage_env").map(String::as_str),
8469            Some("anything"),
8470        );
8471    }
8472
8473    #[test]
8474    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
8475        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
8476        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
8477        // global's value would be validated against the mounted flag's choices and a legitimate
8478        // value would be rejected.
8479        let spec = mounted_task_flag_spec();
8480        let parsed = parse_partial(
8481            &spec,
8482            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
8483        )
8484        .unwrap();
8485        assert!(
8486            parsed.errors.is_empty(),
8487            "prefix global value must not be validated against the mounted flag: {:?}",
8488            parsed
8489                .errors
8490                .iter()
8491                .map(|e| e.to_string())
8492                .collect::<Vec<_>>(),
8493        );
8494        assert_eq!(
8495            parsed.as_env().get("usage_env").map(String::as_str),
8496            Some("not-a-task-choice"),
8497        );
8498
8499        // The embedded-value form binds the same way.
8500        let parsed = parse_partial(
8501            &spec,
8502            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
8503        )
8504        .unwrap();
8505        assert!(parsed.errors.is_empty());
8506        assert_eq!(
8507            parsed.as_env().get("usage_env").map(String::as_str),
8508            Some("not-a-task-choice"),
8509        );
8510
8511        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
8512        // the same name was already used before it.
8513        let parsed = parse_partial(
8514            &spec,
8515            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
8516        )
8517        .unwrap();
8518        let awaiting = parsed
8519            .flag_awaiting_value
8520            .first()
8521            .expect("--env should await a value");
8522        assert_eq!(
8523            awaiting
8524                .arg
8525                .as_ref()
8526                .and_then(|a| a.choices.as_ref())
8527                .map(|c| c.choices.clone()),
8528            Some(vec![
8529                "dev".to_string(),
8530                "stage".to_string(),
8531                "prod".to_string()
8532            ]),
8533            "the mounted command's --env must own the name after the mounted command",
8534        );
8535    }
8536
8537    #[test]
8538    fn test_non_global_flag_does_not_hide_subcommand() {
8539        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
8540        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
8541        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
8542        let spec = mounted_task_flag_spec();
8543
8544        for words in [
8545            // `run` declares `-f/--force` as non-global.
8546            &["test", "run", "--force", "mytask"][..],
8547            &["test", "run", "-f", "mytask"][..],
8548            // Mixed with a global before the subcommand.
8549            &["test", "-E", "prod", "run", "--force", "mytask"][..],
8550        ] {
8551            let parsed = parse_partial(&spec, &input(words)).unwrap();
8552            assert_eq!(
8553                parsed
8554                    .cmds
8555                    .iter()
8556                    .map(|c| c.name.as_str())
8557                    .collect::<Vec<_>>(),
8558                vec!["test", "run", "mytask"],
8559                "{words:?} should descend into the mounted command",
8560            );
8561            assert!(
8562                parsed.args.is_empty(),
8563                "{words:?} should not consume a positional, got {:?}",
8564                parsed.args,
8565            );
8566            assert_eq!(
8567                parsed.as_env().get("usage_force").map(String::as_str),
8568                Some("true"),
8569                "the non-global flag must still be recorded for {words:?}",
8570            );
8571        }
8572
8573        // A non-global flag that takes a value consumes it, rather than reading the value as the
8574        // subcommand.
8575        let mut run_cmd = SpecCommand::builder()
8576            .name("run")
8577            .flag(
8578                SpecFlag::builder()
8579                    .name("output")
8580                    .short('o')
8581                    .long("output")
8582                    .arg(SpecArg::builder().name("mode").build())
8583                    .global(false)
8584                    .build(),
8585            )
8586            .build();
8587        run_cmd.subcommands.insert(
8588            "task".to_string(),
8589            SpecCommand::builder().name("task").build(),
8590        );
8591        let mut cmd = SpecCommand::builder().name("test").build();
8592        cmd.subcommands.insert("run".to_string(), run_cmd);
8593        let spec = Spec {
8594            name: "test".to_string(),
8595            bin: "test".to_string(),
8596            cmd,
8597            ..Default::default()
8598        };
8599
8600        let parsed =
8601            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8602        assert_eq!(
8603            parsed
8604                .cmds
8605                .iter()
8606                .map(|c| c.name.as_str())
8607                .collect::<Vec<_>>(),
8608            vec!["test", "run", "task"],
8609        );
8610        assert_eq!(
8611            parsed.as_env().get("usage_output").map(String::as_str),
8612            Some("quiet"),
8613        );
8614
8615        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
8616        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
8617        assert_parse_err(
8618            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8619            "unexpected word: --nope",
8620        );
8621    }
8622
8623    #[test]
8624    fn test_non_mounted_subcommand_offers_inherited_globals() {
8625        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
8626        // still both recognized and offered.
8627        let mut run_cmd = SpecCommand::builder().name("run").build();
8628        run_cmd.subcommands.insert(
8629            "nested".to_string(),
8630            SpecCommand::builder().name("nested").build(),
8631        );
8632        let mut cmd = SpecCommand::builder()
8633            .name("test")
8634            .flag(
8635                SpecFlag::builder()
8636                    .name("silent")
8637                    .long("silent")
8638                    .global(true)
8639                    .build(),
8640            )
8641            .build();
8642        cmd.subcommands.insert("run".to_string(), run_cmd);
8643        let spec = Spec {
8644            name: "test".to_string(),
8645            bin: "test".to_string(),
8646            cmd,
8647            ..Default::default()
8648        };
8649
8650        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8651        assert_eq!(
8652            parsed.completion_flags().keys().collect::<Vec<_>>(),
8653            parsed.available_flags.keys().collect::<Vec<_>>(),
8654        );
8655        assert!(parsed.completion_flags().contains_key("--silent"));
8656    }
8657
8658    #[test]
8659    fn test_subcommand_alias_collision_keeps_last_owner() {
8660        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
8661        // share an alias are resolved. Historically the flattened flag map gave the shared
8662        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
8663        let run_cmd = SpecCommand::builder()
8664            .name("run")
8665            .flag(
8666                SpecFlag::builder()
8667                    .name("alpha")
8668                    .short('x')
8669                    .long("alpha")
8670                    .global(false)
8671                    .build(),
8672            )
8673            .flag(
8674                SpecFlag::builder()
8675                    .name("beta")
8676                    .short('x')
8677                    .long("beta")
8678                    .global(false)
8679                    .build(),
8680            )
8681            .build();
8682        let mut cmd = SpecCommand::builder().name("test").build();
8683        cmd.subcommands.insert("run".to_string(), run_cmd);
8684        let spec = Spec {
8685            name: "test".to_string(),
8686            bin: "test".to_string(),
8687            cmd,
8688            ..Default::default()
8689        };
8690
8691        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8692        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
8693        assert_eq!(
8694            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8695            Some("beta"),
8696            "the last-declared flag must keep a shared short alias",
8697        );
8698        // Both distinct long aliases remain recognized and point to their own flag.
8699        assert_eq!(
8700            parsed
8701                .available_flags
8702                .get("--alpha")
8703                .map(|f| f.name.as_str()),
8704            Some("alpha"),
8705        );
8706        assert_eq!(
8707            parsed
8708                .available_flags
8709                .get("--beta")
8710                .map(|f| f.name.as_str()),
8711            Some("beta"),
8712        );
8713    }
8714
8715    #[test]
8716    fn test_default_subcommand_same_name_child() {
8717        // Test that default_subcommand doesn't cause issues when the default subcommand
8718        // has a child with the same name (e.g., "run" has a task named "run").
8719        // This verifies we don't switch multiple times or get stuck in a loop.
8720        let run_task = SpecCommand::builder()
8721            .name("run")
8722            .arg(SpecArg::builder().name("args").build())
8723            .build();
8724        let mut run_cmd = SpecCommand::builder().name("run").build();
8725        run_cmd.subcommands.insert("run".to_string(), run_task);
8726
8727        let mut cmd = SpecCommand::builder().name("test").build();
8728        cmd.subcommands.insert("run".to_string(), run_cmd);
8729
8730        let spec = Spec {
8731            name: "test".to_string(),
8732            bin: "test".to_string(),
8733            cmd,
8734            default_subcommand: Some("run".to_string()),
8735            ..Default::default()
8736        };
8737
8738        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
8739        let input = vec!["test".to_string(), "run".to_string()];
8740        let parsed = parse(&spec, &input).unwrap();
8741
8742        // Should have two commands: root and "run"
8743        assert_eq!(parsed.cmds.len(), 2);
8744        assert_eq!(parsed.cmds[0].name, "test");
8745        assert_eq!(parsed.cmds[1].name, "run");
8746
8747        // "test run run" should descend into the "run" task (child of "run" subcommand)
8748        let input = vec![
8749            "test".to_string(),
8750            "run".to_string(),
8751            "run".to_string(),
8752            "hello".to_string(),
8753        ];
8754        let parsed = parse(&spec, &input).unwrap();
8755
8756        assert_eq!(parsed.cmds.len(), 3);
8757        assert_eq!(parsed.cmds[0].name, "test");
8758        assert_eq!(parsed.cmds[1].name, "run");
8759        assert_eq!(parsed.cmds[2].name, "run");
8760        assert_eq!(parsed.args.len(), 1);
8761        let value = parsed.args.values().next().unwrap();
8762        assert_eq!(value.to_string(), "hello");
8763
8764        // Key test case: "test other" should switch to default subcommand "run"
8765        // and treat "other" as a positional arg (not try to switch again because
8766        // "run" also has a "run" child).
8767        let mut run_cmd = SpecCommand::builder()
8768            .name("run")
8769            .arg(SpecArg::builder().name("task").build())
8770            .build();
8771        let run_task = SpecCommand::builder().name("run").build();
8772        run_cmd.subcommands.insert("run".to_string(), run_task);
8773
8774        let mut cmd = SpecCommand::builder().name("test").build();
8775        cmd.subcommands.insert("run".to_string(), run_cmd);
8776
8777        let spec = Spec {
8778            name: "test".to_string(),
8779            bin: "test".to_string(),
8780            cmd,
8781            default_subcommand: Some("run".to_string()),
8782            ..Default::default()
8783        };
8784
8785        let input = vec!["test".to_string(), "other".to_string()];
8786        let parsed = parse(&spec, &input).unwrap();
8787
8788        // Should have two commands: root and "run" (the default)
8789        // We should NOT have switched again to the "run" task child
8790        assert_eq!(parsed.cmds.len(), 2);
8791        assert_eq!(parsed.cmds[0].name, "test");
8792        assert_eq!(parsed.cmds[1].name, "run");
8793
8794        // "other" should be parsed as a positional arg
8795        assert_eq!(parsed.args.len(), 1);
8796        let value = parsed.args.values().next().unwrap();
8797        assert_eq!(value.to_string(), "other");
8798    }
8799
8800    #[test]
8801    fn test_restart_token() {
8802        // Test that restart_token resets argument parsing
8803        let run_cmd = SpecCommand::builder()
8804            .name("run")
8805            .arg(SpecArg::builder().name("task").build())
8806            .restart_token(":::".to_string())
8807            .build();
8808        let mut cmd = SpecCommand::builder().name("test").build();
8809        cmd.subcommands.insert("run".to_string(), run_cmd);
8810
8811        let spec = Spec {
8812            name: "test".to_string(),
8813            bin: "test".to_string(),
8814            cmd,
8815            ..Default::default()
8816        };
8817
8818        // "test run task1 ::: task2" - should end up with task2 as the arg
8819        let input = vec![
8820            "test".to_string(),
8821            "run".to_string(),
8822            "task1".to_string(),
8823            ":::".to_string(),
8824            "task2".to_string(),
8825        ];
8826        let parsed = parse(&spec, &input).unwrap();
8827
8828        // After restart, args were cleared and task2 was parsed
8829        assert_eq!(parsed.args.len(), 1);
8830        let value = parsed.args.values().next().unwrap();
8831        assert_eq!(value.to_string(), "task2");
8832    }
8833
8834    #[test]
8835    fn test_restart_token_multiple() {
8836        // Test multiple restart tokens
8837        let run_cmd = SpecCommand::builder()
8838            .name("run")
8839            .arg(SpecArg::builder().name("task").build())
8840            .restart_token(":::".to_string())
8841            .build();
8842        let mut cmd = SpecCommand::builder().name("test").build();
8843        cmd.subcommands.insert("run".to_string(), run_cmd);
8844
8845        let spec = Spec {
8846            name: "test".to_string(),
8847            bin: "test".to_string(),
8848            cmd,
8849            ..Default::default()
8850        };
8851
8852        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
8853        let input = vec![
8854            "test".to_string(),
8855            "run".to_string(),
8856            "task1".to_string(),
8857            ":::".to_string(),
8858            "task2".to_string(),
8859            ":::".to_string(),
8860            "task3".to_string(),
8861        ];
8862        let parsed = parse(&spec, &input).unwrap();
8863
8864        // After multiple restarts, args were cleared and task3 was parsed
8865        assert_eq!(parsed.args.len(), 1);
8866        let value = parsed.args.values().next().unwrap();
8867        assert_eq!(value.to_string(), "task3");
8868    }
8869
8870    #[test]
8871    fn test_restart_token_clears_flag_awaiting_value() {
8872        // Test that restart_token clears pending flag values
8873        let run_cmd = SpecCommand::builder()
8874            .name("run")
8875            .arg(SpecArg::builder().name("task").build())
8876            .flag(
8877                SpecFlag::builder()
8878                    .name("jobs")
8879                    .long("jobs")
8880                    .arg(SpecArg::builder().name("count").build())
8881                    .build(),
8882            )
8883            .restart_token(":::".to_string())
8884            .build();
8885        let mut cmd = SpecCommand::builder().name("test").build();
8886        cmd.subcommands.insert("run".to_string(), run_cmd);
8887
8888        let spec = Spec {
8889            name: "test".to_string(),
8890            bin: "test".to_string(),
8891            cmd,
8892            ..Default::default()
8893        };
8894
8895        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
8896        let input = vec![
8897            "test".to_string(),
8898            "run".to_string(),
8899            "task1".to_string(),
8900            "--jobs".to_string(),
8901            ":::".to_string(),
8902            "task2".to_string(),
8903        ];
8904        let parsed = parse(&spec, &input).unwrap();
8905
8906        // task2 should be parsed as the task arg, not as --jobs value
8907        assert_eq!(parsed.args.len(), 1);
8908        let value = parsed.args.values().next().unwrap();
8909        assert_eq!(value.to_string(), "task2");
8910        // --jobs should not have a value
8911        assert!(parsed.flag_awaiting_value.is_empty());
8912    }
8913
8914    #[test]
8915    fn test_restart_token_resets_double_dash() {
8916        // Test that restart_token resets the -- separator effect
8917        let run_cmd = SpecCommand::builder()
8918            .name("run")
8919            .arg(SpecArg::builder().name("task").build())
8920            .arg(SpecArg::builder().name("extra_args").var(true).build())
8921            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
8922            .restart_token(":::".to_string())
8923            .build();
8924        let mut cmd = SpecCommand::builder().name("test").build();
8925        cmd.subcommands.insert("run".to_string(), run_cmd);
8926
8927        let spec = Spec {
8928            name: "test".to_string(),
8929            bin: "test".to_string(),
8930            cmd,
8931            ..Default::default()
8932        };
8933
8934        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
8935        let input = vec![
8936            "test".to_string(),
8937            "run".to_string(),
8938            "task1".to_string(),
8939            "--".to_string(),
8940            "extra".to_string(),
8941            ":::".to_string(),
8942            "--verbose".to_string(),
8943            "task2".to_string(),
8944        ];
8945        let parsed = parse(&spec, &input).unwrap();
8946
8947        // --verbose should be parsed as a flag (not an arg) after the restart
8948        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
8949        // task2 should be the arg after restart
8950        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8951        let value = parsed.args.get(task_arg).unwrap();
8952        assert_eq!(value.to_string(), "task2");
8953    }
8954
8955    #[test]
8956    fn test_double_dashes_without_preserve() {
8957        // Only the first `--` is a separator; a later one is a value, because flag
8958        // parsing has already stopped and there is nothing left for it to do.
8959        // `preserve` is about the *first* one — see the test below, where none is
8960        // consumed at all.
8961        let run_cmd = SpecCommand::builder()
8962            .name("run")
8963            .arg(SpecArg::builder().name("args").var(true).build())
8964            .build();
8965        let mut cmd = SpecCommand::builder().name("test").build();
8966        cmd.subcommands.insert("run".to_string(), run_cmd);
8967
8968        let spec = Spec {
8969            name: "test".to_string(),
8970            bin: "test".to_string(),
8971            cmd,
8972            ..Default::default()
8973        };
8974
8975        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
8976        let input = vec![
8977            "test".to_string(),
8978            "run".to_string(),
8979            "arg1".to_string(),
8980            "--".to_string(),
8981            "arg2".to_string(),
8982            "--".to_string(),
8983            "arg3".to_string(),
8984        ];
8985        let parsed = parse(&spec, &input).unwrap();
8986
8987        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8988        let value = parsed.args.get(args_arg).unwrap();
8989        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
8990    }
8991
8992    #[test]
8993    fn test_double_dashes_with_preserve() {
8994        // Test that variadic args WITH `preserve` keep all double dashes
8995        let run_cmd = SpecCommand::builder()
8996            .name("run")
8997            .arg(
8998                SpecArg::builder()
8999                    .name("args")
9000                    .var(true)
9001                    .double_dash(SpecDoubleDashChoices::Preserve)
9002                    .build(),
9003            )
9004            .build();
9005        let mut cmd = SpecCommand::builder().name("test").build();
9006        cmd.subcommands.insert("run".to_string(), run_cmd);
9007
9008        let spec = Spec {
9009            name: "test".to_string(),
9010            bin: "test".to_string(),
9011            cmd,
9012            ..Default::default()
9013        };
9014
9015        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
9016        let input = vec![
9017            "test".to_string(),
9018            "run".to_string(),
9019            "arg1".to_string(),
9020            "--".to_string(),
9021            "arg2".to_string(),
9022            "--".to_string(),
9023            "arg3".to_string(),
9024        ];
9025        let parsed = parse(&spec, &input).unwrap();
9026
9027        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9028        let value = parsed.args.get(args_arg).unwrap();
9029        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9030    }
9031
9032    #[test]
9033    fn test_double_dashes_with_preserve_only_dashes() {
9034        // Test that variadic args WITH `preserve` keep all double dashes even
9035        // if the values are just double dashes
9036        let run_cmd = SpecCommand::builder()
9037            .name("run")
9038            .arg(
9039                SpecArg::builder()
9040                    .name("args")
9041                    .var(true)
9042                    .double_dash(SpecDoubleDashChoices::Preserve)
9043                    .build(),
9044            )
9045            .build();
9046        let mut cmd = SpecCommand::builder().name("test").build();
9047        cmd.subcommands.insert("run".to_string(), run_cmd);
9048
9049        let spec = Spec {
9050            name: "test".to_string(),
9051            bin: "test".to_string(),
9052            cmd,
9053            ..Default::default()
9054        };
9055
9056        // "test run -- --" - all double dashes should be preserved
9057        let input = vec![
9058            "test".to_string(),
9059            "run".to_string(),
9060            "--".to_string(),
9061            "--".to_string(),
9062        ];
9063        let parsed = parse(&spec, &input).unwrap();
9064
9065        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9066        let value = parsed.args.get(args_arg).unwrap();
9067        assert_eq!(value.to_string(), "-- --");
9068    }
9069
9070    #[test]
9071    fn test_double_dashes_with_preserve_multiple_args() {
9072        // Test with multiple args where only the second has has `preserve`
9073        let run_cmd = SpecCommand::builder()
9074            .name("run")
9075            .arg(SpecArg::builder().name("task").build())
9076            .arg(
9077                SpecArg::builder()
9078                    .name("extra_args")
9079                    .var(true)
9080                    .double_dash(SpecDoubleDashChoices::Preserve)
9081                    .build(),
9082            )
9083            .build();
9084        let mut cmd = SpecCommand::builder().name("test").build();
9085        cmd.subcommands.insert("run".to_string(), run_cmd);
9086
9087        let spec = Spec {
9088            name: "test".to_string(),
9089            bin: "test".to_string(),
9090            cmd,
9091            ..Default::default()
9092        };
9093
9094        // The first arg "task1" is captured normally
9095        // Then extra_args with `preserve` captures everything, including the "--" tokens
9096        let input = vec![
9097            "test".to_string(),
9098            "run".to_string(),
9099            "task1".to_string(),
9100            "--".to_string(),
9101            "arg1".to_string(),
9102            "--".to_string(),
9103            "--foo".to_string(),
9104        ];
9105        let parsed = parse(&spec, &input).unwrap();
9106
9107        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9108        let task_value = parsed.args.get(task_arg).unwrap();
9109        assert_eq!(task_value.to_string(), "task1");
9110
9111        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9112        let extra_value = parsed.args.get(extra_arg).unwrap();
9113        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9114    }
9115
9116    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9117        let cmd = SpecCommand::builder().name("test").args(args).build();
9118        Spec {
9119            name: "test".to_string(),
9120            bin: "test".to_string(),
9121            cmd,
9122            ..Default::default()
9123        }
9124    }
9125
9126    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9127        let arg = parsed
9128            .args
9129            .keys()
9130            .find(|a| a.name == name)
9131            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9132        parsed.args.get(arg).unwrap().to_string()
9133    }
9134
9135    fn required_arg(name: &str) -> SpecArg {
9136        SpecArg::builder()
9137            .name(name)
9138            .var(true)
9139            .required(false)
9140            .double_dash(SpecDoubleDashChoices::Required)
9141            .build()
9142    }
9143
9144    #[test]
9145    fn test_double_dash_required_reports_error_once_for_variadic() {
9146        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9147        let spec = spec_with_args([required_arg("files")]);
9148
9149        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9150
9151        assert!(parsed.args.is_empty());
9152        assert_eq!(parsed.errors.len(), 1);
9153        assert!(
9154            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9155        );
9156    }
9157
9158    #[test]
9159    fn test_double_dash_required_suppresses_missing_arg() {
9160        // The arg is never filled, so the end-of-parse check would also call it missing.
9161        let spec = spec_with_args([SpecArg::builder()
9162            .name("file")
9163            .required(true)
9164            .double_dash(SpecDoubleDashChoices::Required)
9165            .build()]);
9166
9167        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9168
9169        assert_eq!(parsed.errors.len(), 1);
9170        assert!(matches!(
9171            &parsed.errors[0],
9172            UsageErr::ArgRequiresDoubleDash(_)
9173        ));
9174        // The cursor stays put, so a completion keeps offering the same arg.
9175        assert_eq!(
9176            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9177            Some("file")
9178        );
9179        assert!(!parsed.double_dash_seen);
9180    }
9181
9182    #[test]
9183    fn test_double_dash_routes_to_required_arg() {
9184        // Everything after `--` belongs to the arg that requires it, even though the greedy
9185        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9186        let spec = spec_with_args([
9187            SpecArg::builder()
9188                .name("tool")
9189                .var(true)
9190                .required(false)
9191                .build(),
9192            required_arg("command"),
9193        ]);
9194
9195        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9196
9197        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9198        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9199        assert!(parsed.double_dash_seen);
9200    }
9201
9202    #[test]
9203    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9204        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9205        // "is it filled?" check cannot be a count of how many args were filled.
9206        let spec = spec_with_args([
9207            SpecArg::builder()
9208                .name("tool")
9209                .var(true)
9210                .required(true)
9211                .build(),
9212            required_arg("command"),
9213        ]);
9214
9215        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9216
9217        assert_eq!(arg_value(&parsed, "command"), "ls");
9218        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9219        assert!(parsed
9220            .errors
9221            .iter()
9222            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
9223    }
9224
9225    #[test]
9226    fn test_double_dash_gap_applies_defaults() {
9227        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9228        let spec = spec_with_args([
9229            SpecArg::builder()
9230                .name("tool")
9231                .var(true)
9232                .required(false)
9233                .default_value("node@20")
9234                .build(),
9235            required_arg("command"),
9236        ]);
9237
9238        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9239
9240        assert_eq!(arg_value(&parsed, "command"), "ls");
9241        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9242    }
9243
9244    fn spec_with_restart_token_and_required_arg() -> Spec {
9245        let run_cmd = SpecCommand::builder()
9246            .name("run")
9247            .arg(SpecArg::builder().name("task").build())
9248            .arg(required_arg("run_args"))
9249            .restart_token(":::".to_string())
9250            .build();
9251        let mut cmd = SpecCommand::builder().name("test").build();
9252        cmd.subcommands.insert("run".to_string(), run_cmd);
9253        Spec {
9254            name: "test".to_string(),
9255            bin: "test".to_string(),
9256            cmd,
9257            ..Default::default()
9258        }
9259    }
9260
9261    #[test]
9262    fn test_double_dash_required_restart_token_resets_separator() {
9263        // The `--` before `:::` belongs to the previous invocation only.
9264        let spec = spec_with_restart_token_and_required_arg();
9265
9266        let parsed = parse_partial(
9267            &spec,
9268            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9269        )
9270        .unwrap();
9271
9272        assert_eq!(arg_value(&parsed, "task"), "task2");
9273        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9274        // Reported once even though the arg was violated after already succeeding once.
9275        assert_eq!(
9276            parsed
9277                .errors
9278                .iter()
9279                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9280                .count(),
9281            1
9282        );
9283    }
9284
9285    #[test]
9286    fn test_double_dash_required_restart_token_accepts_new_separator() {
9287        let spec = spec_with_restart_token_and_required_arg();
9288
9289        let parsed = parse(
9290            &spec,
9291            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9292        )
9293        .unwrap();
9294
9295        assert_eq!(arg_value(&parsed, "task"), "task2");
9296        assert_eq!(arg_value(&parsed, "run_args"), "c");
9297    }
9298
9299    #[test]
9300    fn test_double_dash_preserve_is_not_a_separator() {
9301        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9302        // arg that requires a separator. Deliberate: one token cannot be both.
9303        let spec = spec_with_args([
9304            SpecArg::builder()
9305                .name("kept")
9306                .var(true)
9307                .var_max(1)
9308                .required(false)
9309                .double_dash(SpecDoubleDashChoices::Preserve)
9310                .build(),
9311            required_arg("rest"),
9312        ]);
9313
9314        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9315
9316        assert_eq!(arg_value(&parsed, "kept"), "--");
9317        assert!(parsed.args.keys().all(|a| a.name != "rest"));
9318        assert!(!parsed.double_dash_seen);
9319        assert_eq!(parsed.errors.len(), 1);
9320    }
9321
9322    #[test]
9323    fn test_double_dash_required_does_not_bail_in_parse_partial() {
9324        // Completions parse half-typed command lines; they must still get a result.
9325        let spec = spec_with_args([required_arg("file")]);
9326
9327        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9328        assert!(parse(&spec, &input(&["test", "x"])).is_err());
9329    }
9330
9331    #[test]
9332    fn test_double_dash_without_required_arg_does_not_move_cursor() {
9333        // Specs with no `double_dash="required"` arg are untouched by the jump.
9334        let spec = spec_with_args([
9335            SpecArg::builder().name("first").required(false).build(),
9336            SpecArg::builder().name("second").required(false).build(),
9337        ]);
9338
9339        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9340
9341        assert_eq!(arg_value(&parsed, "first"), "a");
9342        assert_eq!(arg_value(&parsed, "second"), "b");
9343        assert!(parsed.next_arg.is_none());
9344    }
9345
9346    #[test]
9347    fn test_parser_with_custom_env_for_required_arg() {
9348        let spec = spec_with_arg(
9349            SpecArg::builder()
9350                .name("name")
9351                .env("NAME")
9352                .required(true)
9353                .build(),
9354        );
9355        std::env::remove_var("NAME");
9356
9357        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9358            .expect("parse should succeed with custom env");
9359        assert_eq!(parsed.args.len(), 1);
9360        assert_eq!(first_string_value(&parsed), "john");
9361    }
9362
9363    #[test]
9364    fn test_parser_with_custom_env_for_required_flag() {
9365        let spec = spec_with_flag(
9366            SpecFlag::builder()
9367                .long("name")
9368                .env("NAME")
9369                .required(true)
9370                .arg(SpecArg::builder().name("name").build())
9371                .build(),
9372        );
9373        std::env::remove_var("NAME");
9374
9375        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9376            .expect("parse should succeed with custom env");
9377        assert_eq!(parsed.flags.len(), 1);
9378        assert_eq!(first_string_value(&parsed), "jane");
9379    }
9380
9381    #[test]
9382    fn test_flag_environment_fallbacks_preserve_declaration_order() {
9383        let spec = spec_with_flag(
9384            SpecFlag::builder()
9385                .long("name")
9386                .env("NAME")
9387                .env_fallback("OLD_NAME")
9388                .env_fallback("OLDER_NAME")
9389                .deprecated_env("DEPRECATED_NAME")
9390                .arg(SpecArg::builder().name("name").build())
9391                .build(),
9392        );
9393
9394        let parsed = parse_with_env(
9395            &spec,
9396            &["test"],
9397            &[
9398                ("NAME", "canonical"),
9399                ("OLD_NAME", "fallback"),
9400                ("DEPRECATED_NAME", "deprecated"),
9401            ],
9402        )
9403        .unwrap();
9404        assert_eq!(first_string_value(&parsed), "canonical");
9405
9406        let parsed = parse_with_env(
9407            &spec,
9408            &["test"],
9409            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9410        )
9411        .unwrap();
9412        assert_eq!(first_string_value(&parsed), "old");
9413
9414        let parsed =
9415            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9416        assert_eq!(first_string_value(&parsed), "deprecated");
9417    }
9418
9419    #[test]
9420    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9421        let spec = spec_with_flag(
9422            SpecFlag::builder()
9423                .long("name")
9424                .env("NAME")
9425                .deprecated_env("DEPRECATED_NAME")
9426                .arg(SpecArg::builder().name("name").build())
9427                .build(),
9428        );
9429
9430        // The current name is not a deprecated one, and says nothing.
9431        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9432        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9433
9434        let parsed =
9435            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9436        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9437        assert_eq!(
9438            parsed.warnings[0].kind,
9439            crate::warn::WarningKind::DeprecatedEnv
9440        );
9441        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9442        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9443        // Reported, not printed, and the value still arrives.
9444        assert_eq!(first_string_value(&parsed), "deprecated");
9445    }
9446
9447    #[test]
9448    fn a_deprecated_flag_reports_only_when_it_was_used() {
9449        let spec = spec_with_flag(
9450            SpecFlag::builder()
9451                .long("output")
9452                .deprecated("use --out")
9453                .deprecated_remove_at("3.0.0")
9454                .arg(SpecArg::builder().name("output").build())
9455                .build(),
9456        );
9457
9458        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9459        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9460
9461        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9462        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9463        assert_eq!(
9464            parsed.warnings[0].kind,
9465            crate::warn::WarningKind::DeprecatedFlag
9466        );
9467        // Named the way it was typed, dashes and all.
9468        assert_eq!(parsed.warnings[0].name, "--output");
9469        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
9470        assert_eq!(
9471            parsed.warnings[0].render(),
9472            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
9473        );
9474    }
9475
9476    #[test]
9477    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
9478        let flag = SpecFlag::builder()
9479            .long("output")
9480            .deprecated("use --out")
9481            .deprecated_warn_at("9.0.0")
9482            .arg(SpecArg::builder().name("output").build())
9483            .build();
9484        let mut spec = spec_with_flag(flag);
9485        spec.version = Some("2.0.0".to_string());
9486
9487        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9488        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9489
9490        // And once the CLI is the release that was named, it speaks up.
9491        spec.version = Some("9.0.0".to_string());
9492        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9493        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9494    }
9495
9496    #[test]
9497    fn test_parser_with_custom_env_still_fails_when_missing() {
9498        let spec = spec_with_arg(
9499            SpecArg::builder()
9500                .name("name")
9501                .env("NAME")
9502                .required(true)
9503                .build(),
9504        );
9505        std::env::remove_var("NAME");
9506        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
9507    }
9508
9509    #[test]
9510    fn test_parser_does_not_treat_env_choice_value_as_help() {
9511        let spec = spec_with_arg(
9512            SpecArg::builder()
9513                .name("env")
9514                .env("CURRENT_ENV")
9515                .choices(["dev", "staging"])
9516                .required(false)
9517                .build(),
9518        );
9519
9520        assert_parse_err(
9521            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
9522            "Invalid choice for arg env: --help, expected one of dev, staging",
9523        );
9524    }
9525
9526    #[test]
9527    fn test_parser_does_not_treat_default_choice_value_as_help() {
9528        let spec = spec_with_flag(
9529            SpecFlag::builder()
9530                .long("env")
9531                .arg(
9532                    SpecArg::builder()
9533                        .name("env")
9534                        .choices(["dev", "staging"])
9535                        .build(),
9536                )
9537                .default_value("--help")
9538                .build(),
9539        );
9540
9541        assert_parse_err(
9542            parse_with_env(&spec, &["test"], &[]),
9543            "Invalid choice for option env: --help, expected one of dev, staging",
9544        );
9545    }
9546
9547    /// argv as `parse` wants it, program name included.
9548    fn words(of: &[&str]) -> Vec<String> {
9549        of.iter().map(|s| s.to_string()).collect()
9550    }
9551
9552    #[test]
9553    fn a_command_that_needs_a_subcommand_says_so() {
9554        // The spec has carried `subcommand_required` since the derive needed it, and this parser
9555        // never read it — so `mise generate`, which declares it, parsed as a complete
9556        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
9557        let spec: Spec = r#"
9558name "ex"
9559bin "ex"
9560cmd "gen" subcommand_required=#true {
9561    cmd "two" {}
9562    cmd "one" {}
9563    cmd "secret" hide=#true {}
9564    alias "g"
9565}
9566cmd "open" {
9567    cmd "sub" {}
9568}
9569"#
9570        .parse()
9571        .unwrap();
9572
9573        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9574        // Sorted, so the message does not depend on map order; hidden commands left out,
9575        // because a message telling someone to type a hidden name is worse than a vague one;
9576        // and the alias not listed beside the name it points at.
9577        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9578
9579        // Reached through its alias, and still about the command rather than the spelling.
9580        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9581        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9582
9583        // Given one: fine.
9584        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9585
9586        // And a command that has subcommands without declaring them required is untouched —
9587        // this is the half that keeps the check from being "any command with children".
9588        parse(&spec, &words(&["ex", "open"])).unwrap();
9589        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9590    }
9591
9592    #[test]
9593    fn arg_required_else_help_observes_the_selected_commands_argv() {
9594        let spec: Spec = r#"
9595name "ex"
9596bin "ex"
9597flag "--verbose" global=#true
9598cmd "run" arg_required_else_help=#true {
9599    flag "--all"
9600}
9601"#
9602        .parse()
9603        .unwrap();
9604        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9605
9606        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9607        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9608
9609        // A global before the command belongs to the ancestor. It selected `run`, but did not
9610        // give `run` an argument of its own.
9611        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9612        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9613
9614        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9615    }
9616
9617    #[test]
9618    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9619        let spec: Spec = r#"
9620name "ex"
9621bin "ex"
9622unknown_flags "error"
9623external_subcommand #true
9624cmd "install"
9625flag "-v --verbose" global=#true
9626"#
9627        .parse()
9628        .unwrap();
9629
9630        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9631        assert_eq!(
9632            parsed.external,
9633            Some(vec!["foo".into(), "--help".into(), "bar".into()])
9634        );
9635        assert!(parsed.flags.is_empty());
9636
9637        // Known subcommands still win.
9638        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9639        assert_eq!(parsed.cmd.name, "install");
9640        assert!(parsed.external.is_none());
9641
9642        // A global flag before the unmatched word still binds on the parent.
9643        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9644        assert_eq!(
9645            parsed.external,
9646            Some(vec!["foo".into(), "--verbose".into()])
9647        );
9648        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9649
9650        // An unknown flag on the parent is still an error, which is what clap does.
9651        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9652
9653        // A negative number is a value, not a flag, so it can be the unmatched word.
9654        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
9655        // token as a flag and never reach the catch-all.
9656        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9657        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9658    }
9659
9660    #[test]
9661    fn an_external_subcommand_satisfies_subcommand_required() {
9662        let mut spec: Spec = r#"
9663name "ex"
9664bin "ex"
9665external_subcommand #true
9666cmd "install"
9667"#
9668        .parse()
9669        .unwrap();
9670        spec.cmd.subcommand_required = true;
9671
9672        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9673        assert!(parse(&spec, &input(&["ex"])).is_err());
9674    }
9675
9676    #[test]
9677    fn a_default_subcommand_outranks_an_external_one() {
9678        let spec: Spec = r#"
9679name "ex"
9680bin "ex"
9681default_subcommand "run"
9682external_subcommand #true
9683cmd "run" {
9684    arg "[task]"
9685}
9686"#
9687        .parse()
9688        .unwrap();
9689
9690        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
9691        assert_eq!(parsed.cmd.name, "run");
9692        assert!(parsed.external.is_none());
9693        assert_eq!(first_string_value(&parsed), "build");
9694    }
9695
9696    #[test]
9697    fn multicall_basename_strips_a_path_and_exe() {
9698        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
9699        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
9700        assert_eq!(multicall_basename("LS.EXE"), "LS");
9701        assert_eq!(multicall_basename("busybox"), "busybox");
9702    }
9703
9704    #[test]
9705    fn a_multicall_applet_is_the_first_word() {
9706        let spec: Spec = r#"
9707name "busybox"
9708bin "busybox"
9709multicall #true
9710cmd "ls" {
9711    arg "[ARGS]" var=#true
9712}
9713cmd "cat"
9714"#
9715        .parse()
9716        .unwrap();
9717
9718        // A symlink: argv[0] is the applet.
9719        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
9720        assert_eq!(parsed.cmd.name, "ls");
9721        match parsed.args.values().next() {
9722            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
9723            other => panic!("expected ARGS to collect -l, got {other:?}"),
9724        }
9725
9726        // A dispatcher invocation still skips argv[0].
9727        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
9728        assert_eq!(parsed.cmd.name, "ls");
9729
9730        // Configured dispatcher values receive the same path and extension normalization.
9731        let mut configured = spec.clone();
9732        configured.name = "BusyBox".to_string();
9733        configured.bin = "/opt/bin/busybox.exe".to_string();
9734        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
9735        assert_eq!(parsed.cmd.name, "ls");
9736
9737        // `.exe` is stripped so Windows and Unix agree.
9738        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
9739        assert_eq!(parsed.cmd.name, "ls");
9740
9741        // Without the property, argv[0] is discarded as usual.
9742        let mut plain = spec.clone();
9743        plain.multicall = false;
9744        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
9745        assert_eq!(parsed.cmd.name, "ls");
9746    }
9747
9748    #[test]
9749    fn a_multicall_unknown_applet_can_be_external() {
9750        let spec: Spec = r#"
9751name "busybox"
9752bin "busybox"
9753multicall #true
9754unknown_flags "error"
9755external_subcommand #true
9756cmd "ls"
9757"#
9758        .parse()
9759        .unwrap();
9760
9761        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
9762        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
9763
9764        let mut closed = spec.clone();
9765        closed.cmd.external_subcommand = false;
9766        assert!(parse(&closed, &input(&["wat"])).is_err());
9767    }
9768
9769    #[cfg(feature = "unstable_choices_env")]
9770    #[test]
9771    fn test_parser_arg_choices_from_custom_env() {
9772        let spec = spec_arg_choices_env("DEPLOY_ENVS");
9773
9774        let parsed =
9775            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
9776        assert_eq!(first_string_value(&parsed), "bar");
9777
9778        assert_parse_err(
9779            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
9780            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
9781        );
9782        assert_parse_err(
9783            parse_with_env(&spec, &["test", "prod"], &[]),
9784            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
9785        );
9786    }
9787
9788    #[cfg(feature = "unstable_choices_env")]
9789    #[test]
9790    fn test_parser_validates_flag_choices_from_custom_env() {
9791        let spec = spec_flag_choices_env("DEPLOY_ENVS");
9792        let parsed = parse_with_env(
9793            &spec,
9794            &["test", "--env", "baz"],
9795            &[("DEPLOY_ENVS", "foo,bar baz")],
9796        )
9797        .unwrap();
9798        assert_eq!(first_string_value(&parsed), "baz");
9799    }
9800
9801    #[cfg(feature = "unstable_choices_env")]
9802    #[test]
9803    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
9804        let arg_env_spec = spec_with_arg(
9805            SpecArg::builder()
9806                .name("env")
9807                .env("CURRENT_ENV")
9808                .choices_env("DEPLOY_ENVS")
9809                .build(),
9810        );
9811        assert_parse_err(
9812            parse_with_env(
9813                &arg_env_spec,
9814                &["test"],
9815                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
9816            ),
9817            "Invalid choice for arg env: prod, expected one of dev, staging",
9818        );
9819
9820        let flag_default_spec = spec_with_flag(
9821            SpecFlag::builder()
9822                .long("env")
9823                .arg(
9824                    SpecArg::builder()
9825                        .name("env")
9826                        .choices_env("DEPLOY_ENVS")
9827                        .build(),
9828                )
9829                .default_value("prod")
9830                .build(),
9831        );
9832        assert_parse_err(
9833            parse_with_env(
9834                &flag_default_spec,
9835                &["test"],
9836                &[("DEPLOY_ENVS", "dev,staging")],
9837            ),
9838            "Invalid choice for option env: prod, expected one of dev, staging",
9839        );
9840    }
9841
9842    #[test]
9843    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
9844        let spec: Spec = r#"
9845            flag "-v --verbose" var=#true
9846            arg "[database]" default="myapp_dev"
9847            arg "[args...]"
9848        "#
9849        .parse()
9850        .unwrap();
9851        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9852            .into_iter()
9853            .map(String::from)
9854            .collect();
9855        let parsed = parse(&spec, &input).unwrap();
9856        let env = parsed.as_env();
9857        assert_eq!(env.get("usage_database").unwrap(), "mydb");
9858        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
9859    }
9860
9861    #[test]
9862    fn test_variadic_arg_captures_unknown_flags() {
9863        let cmd = SpecCommand::builder()
9864            .name("test")
9865            .flag(SpecFlag::builder().short('v').long("verbose").build())
9866            .arg(SpecArg::builder().name("database").required(false).build())
9867            .arg(
9868                SpecArg::builder()
9869                    .name("args")
9870                    .required(false)
9871                    .var(true)
9872                    .build(),
9873            )
9874            .build();
9875        let spec = Spec {
9876            name: "test".to_string(),
9877            bin: "test".to_string(),
9878            cmd,
9879            ..Default::default()
9880        };
9881
9882        // Unknown --host flag and its value should be captured by [args...]
9883        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9884            .into_iter()
9885            .map(String::from)
9886            .collect();
9887        let parsed = parse(&spec, &input).unwrap();
9888        assert_eq!(parsed.args.len(), 2);
9889        let args_val = parsed
9890            .args
9891            .iter()
9892            .find(|(a, _)| a.name == "args")
9893            .unwrap()
9894            .1;
9895        match args_val {
9896            ParseValue::MultiString(v) => {
9897                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9898            }
9899            _ => panic!("Expected MultiString, got {:?}", args_val),
9900        }
9901    }
9902
9903    #[test]
9904    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
9905        let cmd = SpecCommand::builder()
9906            .name("test")
9907            .flag(SpecFlag::builder().short('v').long("verbose").build())
9908            .arg(SpecArg::builder().name("database").required(false).build())
9909            .arg(
9910                SpecArg::builder()
9911                    .name("args")
9912                    .required(false)
9913                    .var(true)
9914                    .build(),
9915            )
9916            .build();
9917        let spec = Spec {
9918            name: "test".to_string(),
9919            bin: "test".to_string(),
9920            cmd,
9921            ..Default::default()
9922        };
9923
9924        // With explicit -- separator
9925        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
9926            .into_iter()
9927            .map(String::from)
9928            .collect();
9929        let parsed = parse(&spec, &input).unwrap();
9930        assert_eq!(parsed.args.len(), 2);
9931        let args_val = parsed
9932            .args
9933            .iter()
9934            .find(|(a, _)| a.name == "args")
9935            .unwrap()
9936            .1;
9937        match args_val {
9938            ParseValue::MultiString(v) => {
9939                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9940            }
9941            _ => panic!("Expected MultiString, got {:?}", args_val),
9942        }
9943    }
9944
9945    #[test]
9946    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
9947        // Regression: --flag=value should be treated as a single positional token when
9948        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
9949        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
9950
9951        // Single unknown --flag=value: must not produce a stray "3" positional.
9952        // as_env() shell-joins values, so "=" gets quoted.
9953        let input: Vec<String> = vec!["test", "--option=3"]
9954            .into_iter()
9955            .map(String::from)
9956            .collect();
9957        let parsed = parse(&spec, &input).unwrap();
9958        let env = parsed.as_env();
9959        assert_eq!(
9960            env.get("usage_other_args").map(String::as_str),
9961            Some("'--option=3'"),
9962            "expected a single --option=3 token, got {:?}",
9963            env.get("usage_other_args"),
9964        );
9965
9966        // Multiple unknown --flag=value args should each be kept intact
9967        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
9968            .into_iter()
9969            .map(String::from)
9970            .collect();
9971        let parsed2 = parse(&spec, &input2).unwrap();
9972        let env2 = parsed2.as_env();
9973        assert_eq!(
9974            env2.get("usage_other_args").map(String::as_str),
9975            Some("'--foo=bar' '--baz=qux'"),
9976            "expected two intact tokens, got {:?}",
9977            env2.get("usage_other_args"),
9978        );
9979
9980        // Mix of plain positional args and unknown --flag=value tokens
9981        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
9982            .into_iter()
9983            .map(String::from)
9984            .collect();
9985        let parsed3 = parse(&spec, &input3).unwrap();
9986        let env3 = parsed3.as_env();
9987        assert_eq!(
9988            env3.get("usage_other_args").map(String::as_str),
9989            Some("positional1 '--option=3' positional2"),
9990            "expected positional args and intact flag token, got {:?}",
9991            env3.get("usage_other_args"),
9992        );
9993    }
9994
9995    #[test]
9996    fn test_allow_hyphen_values_consumes_short_flag_collision() {
9997        let spec = r#"
9998flag "-d --working-dir <DIR>"
9999flag "-a --args <ARGS>" allow_hyphen_values=#true
10000"#
10001        .parse::<Spec>()
10002        .unwrap();
10003
10004        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10005
10006        assert_eq!(parsed.flags.len(), 1);
10007        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10008    }
10009
10010    #[test]
10011    fn test_allow_hyphen_values_consumes_embedded_long_value() {
10012        let spec = r#"
10013flag "-d --working-dir <DIR>"
10014flag "-a --args <ARGS>" allow_hyphen_values=#true
10015"#
10016        .parse::<Spec>()
10017        .unwrap();
10018
10019        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10020
10021        assert_eq!(parsed.flags.len(), 1);
10022        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10023    }
10024
10025    #[test]
10026    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10027        // The flag is declared to accept a token that looks like a flag, and `--` looks
10028        // like one, so it binds — which is what clap does with the same declaration.
10029        // Letting the separator arm run first consumed it and left the flag hungry, and
10030        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10031        let spec = r#"
10032flag "-a --args <ARGS>" allow_hyphen_values=#true
10033arg "[rest]..."
10034"#
10035        .parse::<Spec>()
10036        .unwrap();
10037
10038        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10039
10040        assert_eq!(flag_string_value(&parsed, "args"), "--");
10041        let rest = parsed
10042            .args
10043            .values()
10044            .next()
10045            .expect("expected the word after the separator to reach the argument");
10046        assert_eq!(rest.to_string(), "-x");
10047    }
10048
10049    #[test]
10050    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10051        // Which token supplied the first value says nothing about how many the argument
10052        // takes, so collection carries on from a hyphenated one exactly as from a plain
10053        // one. It still stops at the next flag-like token, which is what keeps a second
10054        // occurrence of the flag from being eaten as a value.
10055        let spec = r#"
10056flag "-a --args <ARGS>..." allow_hyphen_values=#true
10057"#
10058        .parse::<Spec>()
10059        .unwrap();
10060
10061        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10062
10063        let flag = parsed
10064            .flags
10065            .keys()
10066            .find(|flag| flag.name == "args")
10067            .expect("expected args flag");
10068        match parsed.flags.get(flag).expect("expected args value") {
10069            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10070            other => panic!("expected a list of values, got {other:?}"),
10071        }
10072    }
10073
10074    #[test]
10075    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10076        let spec = r#"
10077flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10078"#
10079        .parse::<Spec>()
10080        .unwrap();
10081
10082        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10083
10084        let flag = parsed
10085            .flags
10086            .keys()
10087            .find(|flag| flag.name == "args")
10088            .expect("expected args flag");
10089        let value = parsed.flags.get(flag).expect("expected args value");
10090        match value {
10091            ParseValue::MultiString(values) => {
10092                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10093            }
10094            _ => panic!("expected MultiString, got {value:?}"),
10095        }
10096    }
10097
10098    #[test]
10099    fn test_require_equals_accepts_attached_and_refuses_detached() {
10100        let spec = r#"
10101flag "--inspect <PORT>" require_equals=#true
10102"#
10103        .parse::<Spec>()
10104        .unwrap();
10105
10106        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10107        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10108
10109        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10110        let msg = format!("{err}");
10111        assert!(
10112            msg.contains("requires an argument") || msg.contains("inspect"),
10113            "detached value must be refused: {msg}"
10114        );
10115    }
10116
10117    #[test]
10118    fn boolean_flags_can_accept_attached_values_when_enabled() {
10119        let spec: Spec = r#"
10120name "ex"
10121bin "ex"
10122flag "--color" negate="--no-color" bool_value=#true
10123arg "[rest]"
10124"#
10125        .parse()
10126        .unwrap();
10127
10128        for (token, expected) in [
10129            ("--color", true),
10130            ("--color=true", true),
10131            ("--color=false", false),
10132            ("--no-color", false),
10133            ("--no-color=false", true),
10134        ] {
10135            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10136            assert!(
10137                matches!(
10138                    parsed.flags.get(&spec.cmd.flags[0]),
10139                    Some(ParseValue::Bool(value)) if *value == expected
10140                ),
10141                "{token}"
10142            );
10143        }
10144
10145        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10146        assert!(matches!(
10147            parsed.args.get(&spec.cmd.args[0]),
10148            Some(ParseValue::String(value)) if value == "word"
10149        ));
10150        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10151        assert!(err.to_string().contains("expected `true` or `false`"));
10152
10153        let strict: Spec = r#"
10154name "ex"
10155bin "ex"
10156args_override_self #false
10157flag "--color" negate="--no-color" bool_value=#true
10158"#
10159        .parse()
10160        .unwrap();
10161        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10162        let parsed = parse(
10163            &strict,
10164            &input(&["ex", "--color=false", "--no-color=false"]),
10165        )
10166        .unwrap();
10167        assert!(matches!(
10168            parsed.flags.get(&strict.cmd.flags[0]),
10169            Some(ParseValue::Bool(true))
10170        ));
10171    }
10172
10173    #[test]
10174    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10175        let spec = r#"
10176flag "-a --all"
10177flag "-i --inspect <PORT>" require_equals=#true
10178"#
10179        .parse::<Spec>()
10180        .unwrap();
10181
10182        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10183        let msg = format!("{err}");
10184        assert!(
10185            msg.contains("requires an argument") || msg.contains("inspect"),
10186            "bundled short must refuse the following word: {msg}"
10187        );
10188    }
10189
10190    #[test]
10191    fn test_default_missing_binds_when_the_value_is_left_off() {
10192        let spec = r#"
10193flag "-c --color <WHEN>" default_missing="always"
10194flag "-v --verbose"
10195"#
10196        .parse::<Spec>()
10197        .unwrap();
10198
10199        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10200        assert_eq!(flag_string_value(&parsed, "color"), "always");
10201
10202        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10203        assert_eq!(flag_string_value(&parsed, "color"), "never");
10204
10205        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10206        assert_eq!(flag_string_value(&parsed, "color"), "never");
10207
10208        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10209        assert_eq!(flag_string_value(&parsed, "color"), "always");
10210        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10211
10212        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10213        assert_eq!(flag_string_value(&parsed, "color"), "");
10214
10215        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10216        assert_eq!(flag_string_value(&parsed, "color"), "never");
10217
10218        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10219        assert_eq!(flag_string_value(&parsed, "color"), "always");
10220        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10221    }
10222
10223    #[test]
10224    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10225        let spec = r#"
10226flag "--apps <N>"
10227flag "--jobs <N>" default_missing="default missing"
10228flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10229"#
10230        .parse::<Spec>()
10231        .unwrap();
10232
10233        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10234        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10235
10236        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10237        assert!(
10238            err.to_string().contains("unexpected word: -1"),
10239            "default_missing must keep an unopted negative value separate: {err}"
10240        );
10241
10242        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10243        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10244
10245        let external_spec = r#"
10246external_subcommand #true
10247flag "--apps <N>"
10248"#
10249        .parse::<Spec>()
10250        .unwrap();
10251        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10252        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10253        assert!(parsed.external.is_none());
10254
10255        for words in [
10256            &["test", "--apps", "--jobs", "1"][..],
10257            &["test", "--apps", "--kids", "-1"][..],
10258        ] {
10259            let err = parse(&spec, &input(words)).unwrap_err();
10260            let message = err.to_string();
10261            assert!(
10262                message.contains("--apps") && message.contains("requires an argument"),
10263                "the earlier flag must report its missing value for {words:?}: {message}"
10264            );
10265        }
10266    }
10267
10268    #[test]
10269    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10270        let spec = r#"
10271flag "--bump [LEVEL]" value_optional=#true
10272flag "--verbose"
10273arg "[FILE]"
10274"#
10275        .parse::<Spec>()
10276        .unwrap();
10277
10278        let absent = parse(&spec, &input(&["test"])).unwrap();
10279        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10280
10281        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10282        let bump = bare
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        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10290        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10291
10292        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10293        assert_eq!(flag_string_value(&explicit, "bump"), "");
10294
10295        let corrected = parse(
10296            &spec,
10297            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10298        )
10299        .unwrap();
10300        let bump = corrected
10301            .flags
10302            .iter()
10303            .find(|(flag, _)| flag.name == "bump")
10304            .map(|(_, value)| value)
10305            .unwrap();
10306        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10307
10308        let collecting = r#"
10309flag "--tag [TAG]..." value_optional=#true
10310flag "--verbose"
10311"#
10312        .parse::<Spec>()
10313        .unwrap();
10314        let valued = parse(
10315            &collecting,
10316            &input(&["test", "--tag", "one", "two", "--verbose"]),
10317        )
10318        .unwrap();
10319        let tag = valued
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 == &["one", "two"]));
10326    }
10327
10328    #[test]
10329    fn test_repeatable_bare_optional_values_count_each_occurrence() {
10330        let spec = r#"
10331flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10332"#
10333        .parse::<Spec>()
10334        .unwrap();
10335
10336        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10337        let tag = parsed
10338            .flags
10339            .iter()
10340            .find(|(flag, _)| flag.name == "tag")
10341            .map(|(_, value)| value)
10342            .unwrap();
10343        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10344
10345        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10346        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10347    }
10348
10349    #[test]
10350    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10351        let spec = r#"
10352flag "--tag [TAG]..." var=#true value_optional=#true
10353flag "--verbose"
10354"#
10355        .parse::<Spec>()
10356        .unwrap();
10357
10358        for argv in [
10359            &["test", "--tag", "one", "two"][..],
10360            &["test", "--tag", "one", "two", "--verbose"][..],
10361            &["test", "--tag", "one", "--tag", "two"][..],
10362        ] {
10363            let parsed = parse(&spec, &input(argv)).unwrap();
10364            let tag = parsed
10365                .flags
10366                .iter()
10367                .find(|(flag, _)| flag.name == "tag")
10368                .map(|(_, value)| value)
10369                .unwrap();
10370            assert!(
10371                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10372                "argv={argv:?}: {tag:?}"
10373            );
10374        }
10375
10376        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10377        let tag = bare
10378            .flags
10379            .iter()
10380            .find(|(flag, _)| flag.name == "tag")
10381            .map(|(_, value)| value)
10382            .unwrap();
10383        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10384    }
10385
10386    #[test]
10387    fn test_default_missing_with_require_equals_refuses_the_following_word() {
10388        let spec = r#"
10389flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10390arg "[rest]"
10391"#
10392        .parse::<Spec>()
10393        .unwrap();
10394
10395        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10396        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10397
10398        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10399        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10400
10401        // The following word is not the value; the missing value is, and 80 is a positional.
10402        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10403        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10404        assert_eq!(
10405            parsed
10406                .args
10407                .values()
10408                .next()
10409                .map(|v| v.to_string())
10410                .as_deref(),
10411            Some("80")
10412        );
10413
10414        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10415        assert_eq!(flag_string_value(&parsed, "inspect"), "");
10416    }
10417
10418    #[test]
10419    fn test_default_missing_must_be_a_choice() {
10420        let spec = r#"
10421flag "--color <WHEN>" default_missing="always" {
10422    choices "auto" "always" "never"
10423}
10424"#
10425        .parse::<Spec>()
10426        .unwrap();
10427
10428        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10429        assert_eq!(flag_string_value(&parsed, "color"), "always");
10430
10431        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10432        assert_eq!(flag_string_value(&parsed, "color"), "never");
10433
10434        let spec = r#"
10435flag "--color <WHEN>" default_missing="wat" {
10436    choices "auto" "always" "never"
10437}
10438"#
10439        .parse::<Spec>()
10440        .unwrap();
10441
10442        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10443        let msg = format!("{err}");
10444        assert!(
10445            msg.contains("Invalid choice for option color: wat"),
10446            "missing default has to pass choices the same way a typed value does: {msg}"
10447        );
10448
10449        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
10450        let msg = format!("{err}");
10451        assert!(
10452            msg.contains("Invalid choice for option color: wat"),
10453            "an attached value that is not a choice is still refused: {msg}"
10454        );
10455
10456        let spec = r#"
10457flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
10458    choices "9229" "80"
10459}
10460arg "[rest]"
10461"#
10462        .parse::<Spec>()
10463        .unwrap();
10464
10465        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
10466        let msg = format!("{err}");
10467        assert!(
10468            msg.contains("Invalid choice for option inspect: wat"),
10469            "require_equals still binds the missing string, so the error is the choice: {msg}"
10470        );
10471    }
10472
10473    #[test]
10474    fn test_hyphen_values_still_start_short_flag_parsing() {
10475        let spec = r#"
10476flag "-d --working-dir <DIR>"
10477flag "-a --args <ARGS>"
10478"#
10479        .parse::<Spec>()
10480        .unwrap();
10481
10482        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
10483        let message = err.to_string();
10484        assert!(
10485            message.contains("--args") && message.contains("requires an argument"),
10486            "the recognized -d must leave the earlier -a missing: {message}"
10487        );
10488    }
10489
10490    /// `available_flags` has to agree with what an actual parse accepts, since
10491    /// its whole reason to exist is answering that question without one.
10492    mod available_flags {
10493        use super::*;
10494
10495        fn spec() -> Spec {
10496            r#"
10497bin "test"
10498flag "-v --verbose" global=#true
10499flag "--raw" global=#true effect="write"
10500flag "--local-only"
10501cmd "run" {
10502    flag "-r --raw"
10503    flag "-w --watch"
10504    cmd "once"
10505}
10506"#
10507            .parse::<Spec>()
10508            .unwrap()
10509        }
10510
10511        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
10512            let mut chain = vec![&spec.cmd];
10513            for segment in path {
10514                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
10515            }
10516            chain
10517        }
10518
10519        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
10520            let mut names: Vec<_> = available_flags(&chain(spec, path))
10521                .iter()
10522                .map(|f| f.name.clone())
10523                .collect();
10524            names.sort();
10525            names
10526        }
10527
10528        #[test]
10529        fn an_empty_chain_yields_nothing() {
10530            assert!(available_flags(&[]).is_empty());
10531        }
10532
10533        #[test]
10534        fn the_root_gets_its_own_flags() {
10535            let spec = spec();
10536            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
10537        }
10538
10539        #[test]
10540        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
10541            let spec = spec();
10542            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10543        }
10544
10545        #[test]
10546        fn a_re_declared_global_is_listed_once() {
10547            // The merge can leave the long key on the merged flag and the short
10548            // key on the pre-merge one. Same flag; it must not be listed twice.
10549            let spec = r#"
10550bin "test"
10551flag "-y --yes" global=#true effect="write"
10552cmd "rm" {
10553    flag "-y --yes"
10554}
10555"#
10556            .parse::<Spec>()
10557            .unwrap();
10558            let flags = available_flags(&chain(&spec, &["rm"]));
10559            assert_eq!(flags.len(), 1, "{flags:?}");
10560            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10561        }
10562
10563        #[test]
10564        fn a_re_declared_global_keeps_the_globals_declaration() {
10565            // `run` re-declares the long-only global `--raw` as `-r --raw`
10566            // without `global`. That is the same flag: the global's `effect`
10567            // survives, the orphan short is unioned in, and it stays global.
10568            let spec = spec();
10569            let flags = available_flags(&chain(&spec, &["run"]));
10570            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10571            assert!(raw.global);
10572            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10573            assert_eq!(raw.short, ['r']);
10574        }
10575
10576        #[test]
10577        fn it_matches_what_a_parse_accepts() {
10578            // The invariant. If these ever disagree, one of them is lying to a
10579            // caller about which flags a command takes.
10580            let spec = spec();
10581            for path in [vec![], vec!["run"], vec!["run", "once"]] {
10582                let argv = std::iter::once("test".to_string())
10583                    .chain(path.iter().map(|s| s.to_string()))
10584                    .collect::<Vec<_>>();
10585                let parsed = parse_partial(&spec, &argv).unwrap();
10586
10587                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10588                    .map(|f| f.name.clone())
10589                    .collect();
10590                from_parse.sort();
10591                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10592            }
10593        }
10594    }
10595
10596    // Provenance: which token bound what, and where a value came from when no token did.
10597
10598    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
10599    /// assert on the whole picture rather than on one field at a time.
10600    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10601        parsed
10602            .tokens
10603            .iter()
10604            .find(|token| token.index == index)
10605            .unwrap_or_else(|| panic!("no token at {index}"))
10606            .roles
10607            .iter()
10608            .map(render_role)
10609            .collect()
10610    }
10611
10612    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10613        parsed
10614            .flag_origins
10615            .iter()
10616            .find(|(f, _)| f.name == flag)
10617            .map(|(_, origins)| origins.clone())
10618            .unwrap_or_default()
10619    }
10620
10621    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10622        let env = env
10623            .iter()
10624            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10625            .collect();
10626        Parser::new(spec)
10627            .with_env(env)
10628            .explain(&input(words))
10629            .unwrap()
10630    }
10631
10632    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10633        explain_with_env(spec, words, &[])
10634    }
10635
10636    #[test]
10637    fn an_attached_long_value_is_recorded_on_the_flag_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, 0), ["program"]);
10645        assert_eq!(
10646            roles(&parsed, 1),
10647            ["flag env as --env", "value of env = [\"prod\"], attached"]
10648        );
10649        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
10650        // form while the detached one worked, and nothing could show the difference.
10651        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10652    }
10653
10654    #[test]
10655    fn a_detached_long_value_is_recorded_on_its_own_token() {
10656        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10657            .parse()
10658            .unwrap();
10659
10660        let parsed = explain(&spec, &["ex", "--env", "prod"]);
10661
10662        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10663        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10664    }
10665
10666    #[test]
10667    fn a_short_bundle_is_attributed_to_the_bundle_token() {
10668        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10669            .parse()
10670            .unwrap();
10671
10672        let parsed = explain(&spec, &["ex", "-abj8"]);
10673
10674        // One word the caller wrote, four things it did — and the re-queued tails are
10675        // folded back onto it rather than appearing as tokens nobody typed.
10676        assert_eq!(
10677            roles(&parsed, 1),
10678            [
10679                "flag a as -a",
10680                "flag b as -b",
10681                "flag j as -j",
10682                "value of j = [\"8\"], attached",
10683            ]
10684        );
10685        assert_eq!(parsed.tokens.len(), 2);
10686    }
10687
10688    #[test]
10689    fn a_delimiter_splits_one_token_into_several_values() {
10690        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
10691            .parse()
10692            .unwrap();
10693
10694        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
10695
10696        assert_eq!(
10697            roles(&parsed, 2),
10698            ["value of tags = [\"a\", \"b\", \"c\"]"],
10699            "the values meant, not the word typed"
10700        );
10701    }
10702
10703    #[test]
10704    fn a_separator_and_the_words_after_it_are_distinguished() {
10705        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
10706            .parse()
10707            .unwrap();
10708
10709        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
10710
10711        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
10712        assert_eq!(roles(&parsed, 2), ["separator"]);
10713        // Past the separator `-x` is data, not an unknown flag.
10714        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
10715    }
10716
10717    #[test]
10718    fn a_second_separator_is_data() {
10719        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
10720            .parse()
10721            .unwrap();
10722
10723        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
10724
10725        assert_eq!(roles(&parsed, 1), ["separator"]);
10726        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
10727    }
10728
10729    #[test]
10730    fn an_unknown_flag_says_what_took_it() {
10731        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10732            .parse()
10733            .unwrap();
10734
10735        let parsed = explain(&spec, &["ex", "--wat"]);
10736
10737        // The default is lax, so the word became data. Which is the useful thing to be
10738        // told: the alternative reading is "you have a typo".
10739        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
10740    }
10741
10742    #[test]
10743    fn a_subcommand_word_is_not_a_positional() {
10744        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
10745            .parse()
10746            .unwrap();
10747
10748        let parsed = explain(&spec, &["ex", "build", "a"]);
10749
10750        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
10751        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
10752    }
10753
10754    #[test]
10755    fn a_multicall_applet_is_read_at_argv0() {
10756        let spec: Spec =
10757            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
10758                .parse()
10759                .unwrap();
10760
10761        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
10762
10763        // argv[0] is both the program and the word that selected the applet, and the word
10764        // read there is not the word the caller wrote.
10765        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
10766        assert!(parsed.tokens[0].synthesized);
10767        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
10768    }
10769
10770    #[test]
10771    fn words_the_parse_never_reached_say_so() {
10772        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10773            .parse()
10774            .unwrap();
10775
10776        let parsed = Parser::new(&spec)
10777            .explain(&input(&["ex", "--help", "a"]))
10778            .unwrap();
10779
10780        assert_eq!(roles(&parsed, 2), ["unread"]);
10781    }
10782
10783    #[test]
10784    fn an_env_origin_names_the_variable_that_fired() {
10785        let spec: Spec =
10786            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
10787                .parse()
10788                .unwrap();
10789
10790        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
10791        assert_eq!(
10792            origins(&primary, "token"),
10793            [ValueOrigin::Env("EX_TOKEN".to_string())]
10794        );
10795
10796        // The fallback firing is a different fact from the primary firing, and which one it
10797        // was is what says which declaration to delete.
10798        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
10799        assert_eq!(
10800            origins(&fallback, "token"),
10801            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
10802        );
10803    }
10804
10805    #[test]
10806    fn a_default_origin_is_recorded_for_flags_and_args() {
10807        let spec: Spec =
10808            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
10809                .parse()
10810                .unwrap();
10811
10812        let parsed = explain(&spec, &["ex"]);
10813
10814        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
10815        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
10816        assert_eq!(arg.name, "src");
10817        assert_eq!(origins, &[ValueOrigin::Default]);
10818    }
10819
10820    #[test]
10821    fn a_default_if_origin_carries_the_condition_that_fired() {
10822        let spec: Spec = r#"
10823name "ex"
10824bin "ex"
10825flag "--profile <p>"
10826flag "--strict" {
10827    default_if "--profile" "prod" "true"
10828}
10829        "#
10830        .parse()
10831        .unwrap();
10832
10833        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
10834
10835        // The selector alone is ambiguous: several conditions may name it with different
10836        // `when` values, so the report has to say which one matched.
10837        assert_eq!(
10838            origins(&parsed, "strict"),
10839            [ValueOrigin::DefaultIf {
10840                selector: "--profile".to_string(),
10841                when: Some("prod".to_string()),
10842            }]
10843        );
10844    }
10845
10846    #[test]
10847    fn a_bare_optional_value_flag_records_default_missing() {
10848        let spec: Spec =
10849            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
10850                .parse()
10851                .unwrap();
10852
10853        let parsed = explain(&spec, &["ex", "--color", "-v"]);
10854
10855        // The flag was typed and the value was not, which is the distinction a spec author
10856        // is asking about when they ask why `--color` came out `always`.
10857        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
10858        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10859        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
10860    }
10861
10862    #[test]
10863    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
10864        let spec: Spec =
10865            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
10866                .parse()
10867                .unwrap();
10868
10869        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
10870
10871        // Why origins are a list: one declaration, two occurrences, two different answers.
10872        assert_eq!(
10873            roles(&parsed, 1),
10874            [
10875                "flag color as --color",
10876                "value of color = [\"red\"], attached"
10877            ]
10878        );
10879        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10880    }
10881
10882    #[test]
10883    fn an_override_names_the_flag_that_did_it() {
10884        let spec: Spec =
10885            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
10886                .parse()
10887                .unwrap();
10888
10889        let parsed = explain(&spec, &["ex", "--loud"]);
10890
10891        // Without the overriding name, "`--quiet` is unset despite its default" has no
10892        // answer: the fallback phase silently declines to fill an overridden flag.
10893        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
10894        assert!(origins(&parsed, "quiet").is_empty());
10895    }
10896
10897    #[test]
10898    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
10899        let spec: Spec = r#"
10900name "ex"
10901bin "ex"
10902cmd "run" restart_token=":::" {
10903    arg "<task>" default="build"
10904}
10905        "#
10906        .parse()
10907        .unwrap();
10908
10909        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
10910
10911        // The values belong to the last invocation, so provenance must too — but the words
10912        // of the first were still read, and a report that dropped them would show a command
10913        // line with a hole in it.
10914        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
10915        // And the token that did the resetting says so: without a role of its own it reads
10916        // as a word that did nothing, next to a `lint` that filled an arg now empty.
10917        assert_eq!(roles(&parsed, 3), ["restart"]);
10918        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
10919        assert!(parsed.arg_origins.is_empty());
10920    }
10921
10922    #[test]
10923    fn a_value_terminator_says_which_run_it_ended() {
10924        let spec: Spec = r#"
10925name "ex"
10926bin "ex"
10927flag "--exec <cmd>..." value_terminator=";"
10928arg "<src>"
10929        "#
10930        .parse()
10931        .unwrap();
10932
10933        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
10934
10935        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
10936        // The terminator is consumed and is not one of the values, which is the whole reason
10937        // it was declared — so it needs a row saying that rather than an empty one.
10938        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
10939        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
10940    }
10941
10942    #[test]
10943    fn an_args_value_terminator_says_which_run_it_ended() {
10944        let spec: Spec = r#"
10945name "ex"
10946bin "ex"
10947arg "<files>..." value_terminator=";"
10948arg "[dest]"
10949        "#
10950        .parse()
10951        .unwrap();
10952
10953        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
10954
10955        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
10956        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
10957        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
10958    }
10959
10960    #[test]
10961    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
10962        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
10963            .parse()
10964            .unwrap();
10965
10966        let parsed = Parser::new(&spec)
10967            .explain(&input(&["ex", "--env=prod"]))
10968            .unwrap();
10969
10970        // `parse` reports "missing required <src>" and nothing else, which is the report the
10971        // caller already had. This is the case the whole thing exists for.
10972        assert!(Parser::new(&spec)
10973            .parse(&input(&["ex", "--env=prod"]))
10974            .is_err());
10975        assert_eq!(
10976            roles(&parsed, 1),
10977            ["flag env as --env", "value of env = [\"prod\"], attached"]
10978        );
10979        assert!(
10980            parsed.errors.iter().any(|e| e.to_string().contains("src")),
10981            "{:?}",
10982            parsed.errors
10983        );
10984    }
10985
10986    #[test]
10987    fn an_external_subcommand_forwards_whole_tokens() {
10988        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
10989            .parse()
10990            .unwrap();
10991
10992        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
10993
10994        assert_eq!(roles(&parsed, 1), ["external"]);
10995        assert_eq!(roles(&parsed, 2), ["external"]);
10996    }
10997
10998    #[test]
10999    fn a_view_keeps_the_callers_argv_positions() {
11000        let spec: Spec = r#"
11001bin "ex"
11002view "runner" root="run"
11003cmd "run" {
11004    flag "--token <token>"
11005}
11006        "#
11007        .parse()
11008        .unwrap();
11009
11010        let parsed = explain(&spec, &["runner", "--token", "secret"]);
11011
11012        // A view re-enters the parse with the same argv, so the positions still mean what
11013        // the caller wrote.
11014        assert_eq!(roles(&parsed, 0), ["program"]);
11015        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11016    }
11017}