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`].
34///
35/// Returns each flag it built by merging a re-declaration into an inherited global, with the
36/// address of the global it stands for, so a caller tracking where flags came from can carry
37/// that over. See [`ParseOutput::is_mounted_flag`].
38fn merge_subcommand_flags(
39    available: &mut BTreeMap<String, Arc<SpecFlag>>,
40    new_flags: BTreeMap<String, Arc<SpecFlag>>,
41    crossing_mount: bool,
42) -> Vec<(Arc<SpecFlag>, usize)> {
43    let mut merges = Vec::new();
44    // Keep only inherited global flags from the parent.
45    available.retain(|_, f| f.global);
46
47    if crossing_mount {
48        // A mounted command owns its flags outright, including names an inherited global also
49        // uses: a word after the mounted command belongs to the mounted program. Words before
50        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
51        // mounted command does not declare (e.g. a global's short) stay inherited.
52        for (key, flag) in new_flags {
53            available.insert(key, flag);
54        }
55        return merges;
56    }
57
58    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
59    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
60    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
61    // Maps each merged flag produced below back to the inherited global it was merged from, so
62    // the collision check can compare *origins*: a flag this loop already merged is not a
63    // different global, even though it is a different `Arc`.
64    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
65    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
66    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
67        let ptr = Arc::as_ptr(flag) as usize;
68        *merged_origin.get(&ptr).unwrap_or(&ptr)
69    }
70
71    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
72    // map's existing intra-subcommand collision resolution: when two flags in the same command
73    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
74    // to its last-declared owner, and we must not change which flag owns it.
75    for (key, flag) in new_flags {
76        if flag.global {
77            // A child that re-declares (or adds) a global flag stays recognized everywhere.
78            available.insert(key, flag);
79            continue;
80        }
81
82        // A non-global re-declaration that shares a LONG name with an inherited global flag is
83        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
84        // global). Keep the global flag (global precedence, so it survives the next descent's
85        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
86        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
87        // deliberate: a re-declaration sharing only a short letter with an unrelated global
88        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
89        // handled by the `contains_key` skip below instead.
90        let inherited_global = flag.long.iter().find_map(|l| {
91            available
92                .get(&format!("--{l}"))
93                .filter(|f| f.global)
94                .cloned()
95        });
96        if let Some(global_flag) = inherited_global {
97            // Never clobber a *different* inherited global's alias. If this re-declaration's
98            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
99            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
100            // precedence dictates, instead of stealing the alias for the merged flag.
101            //
102            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
103            // earlier key of this same child already replaced some of them with the merged flag,
104            // which the lookups above may now resolve to. That is the same logical flag, so it
105            // must not read as a collision and leave this key on the pre-merge global.
106            let global_origin = origin_of(&merged_origin, &global_flag);
107            if available.get(&key).is_some_and(|existing| {
108                existing.global && origin_of(&merged_origin, existing) != global_origin
109            }) {
110                continue;
111            }
112            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
113                Some(merged) => merged.clone(),
114                None => {
115                    let mut merged = (*global_flag).clone();
116                    // `exclusive` is deliberately *not* reconciled here, in either direction.
117                    // One object now answers to two alias sets that may disagree: the child
118                    // owns the spellings it declared, the ancestor keeps the ones only it
119                    // declared. A single bool cannot hold both, so the merged flag carries the
120                    // ancestor's and validation resolves the occurrence by the spelling that
121                    // was typed — the ledger it already consults to decide whether selecting
122                    // the child is company.
123                    for s in &flag.short {
124                        if !merged.short.contains(s) {
125                            merged.short.push(*s);
126                        }
127                    }
128                    for l in &flag.long {
129                        if !merged.long.contains(l) {
130                            merged.long.push(l.clone());
131                        }
132                    }
133                    // A child may deliberately promote one of the ancestor's hidden aliases.
134                    // Hidden lists are subsets of the accepted spellings, so a spelling present
135                    // on the child but absent from its hidden subset is visible at this level.
136                    merged.hidden_short_aliases.retain(|alias| {
137                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
138                    });
139                    merged.hidden_aliases.retain(|alias| {
140                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
141                    });
142                    for s in &flag.hidden_short_aliases {
143                        if !merged.hidden_short_aliases.contains(s) {
144                            merged.hidden_short_aliases.push(*s);
145                        }
146                    }
147                    for l in &flag.hidden_aliases {
148                        if !merged.hidden_aliases.contains(l) {
149                            merged.hidden_aliases.push(l.clone());
150                        }
151                    }
152                    let merged = Arc::new(merged);
153                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
154                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
155                    merges.push((Arc::clone(&merged), global_origin));
156                    // Rebind the global's *other* aliases onto the merged flag. The loop only
157                    // visits keys the child declared, so an alias the child left out (the `-y` of
158                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
159                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
160                    // logical flag must be one object under every key it answers to.
161                    for existing in available.values_mut() {
162                        if origin_of(&merged_origin, existing) == global_origin {
163                            *existing = Arc::clone(&merged);
164                        }
165                    }
166                    merged
167                }
168            };
169            available.insert(key, merged);
170            continue;
171        }
172
173        // Purely-local flag (shares nothing with an inherited global), or one that collides only
174        // on a short with an unrelated global. Insert this alias but never shadow an inherited
175        // global flag. Such non-global flags are dropped by the next descent's `retain`.
176        if available.contains_key(&key) {
177            continue;
178        }
179        available.insert(key, flag);
180    }
181    merges
182}
183
184/// Build the lookup keys a flag is registered under in `available_flags`:
185/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
186fn flag_keys(flag: &SpecFlag) -> Vec<String> {
187    let mut keys: Vec<String> = flag
188        .long
189        .iter()
190        .map(|l| format!("--{l}"))
191        .chain(flag.short.iter().map(|s| format!("-{s}")))
192        .collect();
193    if let Some(negate) = &flag.negate {
194        keys.push(negate.clone());
195    }
196    keys
197}
198
199/// The flags a command declares, keyed by each of their aliases.
200fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
201    cmd.flags
202        .iter()
203        .chain(cmd.clause.iter().flat_map(|clause| &clause.flags))
204        .flat_map(|f| {
205            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
206            flag_keys(&f)
207                .into_iter()
208                .map(|key| (key, Arc::clone(&f)))
209                .collect::<Vec<_>>()
210        })
211        .collect()
212}
213
214fn unique_flags<'a>(
215    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
216) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
217    let mut seen = HashSet::new();
218    flags
219        .into_iter()
220        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
221}
222
223/// Every flag a command accepts, resolved the way parsing an invocation of it
224/// resolves them.
225///
226/// `chain` runs from the root command (`spec.cmd`) down to the command in
227/// question; an empty chain yields no flags.
228///
229/// This is not "the command's flags plus its ancestors' globals". A subcommand
230/// that re-declares a global's long name is describing the *same* flag rather
231/// than a new one, so the global's help, argument and effect survive and only
232/// the re-declaration's extra aliases are added — see
233/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
234/// going through this will disagree with what the parser actually accepts.
235pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
236    let Some((root, rest)) = chain.split_first() else {
237        return vec![];
238    };
239    let mut available = gather_flags(root);
240    for cmd in rest {
241        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
242    }
243
244    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
245    // global that has both a short and a long, the merged flag is written under
246    // the long key while the short key keeps pointing at the pre-merge `Arc` —
247    // two objects for one logical flag. That is harmless for parsing, which
248    // looks flags up by key, but a caller listing flags would see it twice.
249    //
250    // Names break the tie because a long key always sorts before a short one
251    // (`--x` < `-y` at the second byte), so the merged declaration is the one
252    // reached first. Two genuinely distinct flags sharing a name is a spec bug
253    // that `usage lint` reports as a duplicate flag.
254    let mut seen_names = HashSet::new();
255    unique_flags(available.values())
256        .filter(|f| seen_names.insert(f.name.clone()))
257        .cloned()
258        .collect()
259}
260
261/// Extract the flag key from a flag word for lookup in available_flags map
262/// Handles both long flags (--flag, --flag=value) and short flags (-f)
263fn get_flag_key(word: &str) -> &str {
264    if word.starts_with("--") {
265        // Long flag: strip =value if present
266        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
267    } else if let Some((end, _)) = word.char_indices().nth(2) {
268        // Short flag: the dash and one letter, which is one character and not
269        // necessarily one byte.
270        &word[0..end]
271    } else {
272        word
273    }
274}
275
276/// Where a value came from, when it did not come from the command line.
277///
278/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
279/// token for the flag and none for the value, and that distinction is the whole question
280/// a spec author is asking when they ask why `--color` came out `always`. Values that were
281/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
282#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub enum ValueOrigin {
285    /// A flag that takes a value was given without one, so the declaration supplied it:
286    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
287    /// One variant for both, because from argv's side the same thing happened — the flag
288    /// was typed and the value was not.
289    DefaultMissing,
290    /// An environment variable, named.
291    ///
292    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
293    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
294    /// not say which declaration fired or which one to delete.
295    Env(String),
296    /// A declared `default`, on the flag or on the flag's argument.
297    ///
298    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
299    /// a fact about the value, and `usage lint` is the place to complain about declaring
300    /// both.
301    Default,
302    /// A `default_if` whose condition matched, with the condition that decided it. The
303    /// selector alone is ambiguous — several conditions may name it with different `when`
304    /// values.
305    DefaultIf {
306        selector: String,
307        when: Option<String>,
308    },
309}
310
311/// What one word of the command line became.
312///
313/// Several because a single token can do more than one thing: `-abc` sets three flags,
314/// `-j8` is a flag and its value.
315#[derive(Debug, Clone)]
316#[non_exhaustive]
317pub enum TokenRole {
318    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
319    Program,
320    /// Selected a subcommand.
321    Command { name: String },
322    /// Named a flag, in this spelling. `negated` for the `negate` form.
323    Flag {
324        flag: Arc<SpecFlag>,
325        spelling: String,
326        negated: bool,
327    },
328    /// Supplied a flag's value. Several values when a `delimiter` split the word.
329    Value {
330        flag: Arc<SpecFlag>,
331        values: Vec<String>,
332        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
333        /// rather than following it as its own word.
334        attached: bool,
335    },
336    /// Filled a positional argument. Several values when a `delimiter` split the word.
337    Arg {
338        arg: Arc<SpecArg>,
339        values: Vec<String>,
340    },
341    /// An explicit `--`, consumed as a separator.
342    Separator,
343    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
344    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
345    /// without a role the word reads as having done nothing while a whole help page arrives
346    /// in the error list.
347    Builtin { spelling: String },
348    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
349    /// declaration whose run it closed — the word is not one of that run's values, which is
350    /// the whole reason it was declared.
351    ValueTerminator { ends: String },
352    /// A declared `restart_token`: the positional cursor and the values it had filled start
353    /// over here. Recorded because the words before it are still in the report, and without
354    /// this row they look like they filled arguments that then came back empty.
355    Restart,
356    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
357    /// under `unknown_flags="value"`, and `None` when the word was refused.
358    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
359    /// The word reached a declaration that would not take it, and was dropped. Without this
360    /// the token reads as having done nothing, which is the one thing it did not do.
361    Refused { reason: String },
362    /// Forwarded to an external subcommand.
363    External,
364    /// The parser stopped before this word — a help request, a refused value.
365    Unread,
366    /// Filled a sigil-classified positional after removing its declared prefix.
367    Sigil {
368        arg: Arc<SpecArg>,
369        sigil: String,
370        values: Vec<String>,
371    },
372    /// Ended one instance of a repeatable clause and began the next.
373    ClauseSeparator { name: String },
374}
375
376/// One word of the command line, and what it became.
377#[derive(Debug, Clone)]
378#[non_exhaustive]
379pub struct TokenBinding {
380    /// Position in the argv slice the parse was given, argv[0] included.
381    pub index: usize,
382    pub word: String,
383    /// Roles a word the parser made up contributed, folded onto the token it was derived
384    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
385    /// argv[0]. `word` is what the caller wrote, not what the parser read.
386    pub synthesized: bool,
387    pub roles: Vec<TokenRole>,
388}
389
390#[non_exhaustive]
391pub struct ParseOutput {
392    pub cmd: SpecCommand,
393    pub cmds: Vec<SpecCommand>,
394    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
395    /// Separator-delimited positional instances, keyed by clause name.
396    pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
397    /// Per-instance flags belonging to a repeatable clause, keyed by clause name.
398    pub clause_flags: IndexMap<String, Vec<IndexMap<Arc<SpecFlag>, ParseValue>>>,
399    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
400    /// What each word of the command line became, in argv order, one entry per word.
401    ///
402    /// The token half of provenance; [`ParseOutput::flag_origins`] and
403    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
404    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
405    /// a token that bound to nothing, so both exist.
406    pub tokens: Vec<TokenBinding>,
407    /// Where a flag's value came from when it did not come from argv, in the order the
408    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
409    ///
410    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
411    /// `default_missing` value, so one flag can have several.
412    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
413    /// Where an argument's value came from when it did not come from argv. Keyed as
414    /// [`ParseOutput::args`] is.
415    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
416    /// Flags a later occurrence removed, and the flag that removed them.
417    ///
418    /// The overriding name is the half a caller needs: the fallback phase silently declines
419    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
420    /// otherwise" has no answer without it.
421    pub overridden_flags: BTreeMap<String, String>,
422    /// Every flag the parser recognizes at this point, keyed by each of its aliases
423    /// (`--long`, `-s`, negations).
424    ///
425    /// This includes flags that only remain recognized because they may appear *before* a
426    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
427    /// should offer.
428    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
429    /// Every flag object the parse brought in from a mounted program, by identity. Kept
430    /// rather than derived, because the descent merges, re-declares and shadows flags and no
431    /// later reading of the command chain can tell whose a merged flag is. See
432    /// [`ParseOutput::is_mounted_flag`].
433    mounted_flags: Vec<Arc<SpecFlag>>,
434    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
435    pub errors: Vec<UsageErr>,
436    /// Deprecated declarations this command line used, for the caller to render when its
437    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
438    /// not used anything yet.
439    pub warnings: Vec<Warning>,
440    /// The positional argument the next word would have filled, i.e. where the parser's
441    /// cursor stopped. `None` once every argument is satisfied.
442    ///
443    /// Completions need exactly this: the parser already accounts for `var_max`, for
444    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
445    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
446    pub next_arg: Option<Arc<SpecArg>>,
447    /// Whether an explicit `--` was consumed *as a separator*.
448    ///
449    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
450    /// of the variadic argument collecting it, not a separator, so it does not unlock a
451    /// `double_dash="required"` argument.
452    pub double_dash_seen: bool,
453    /// Remaining argv captured when an unmatched word was forwarded as an external
454    /// subcommand: the command name first, then every token after it.
455    ///
456    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
457    pub external: Option<Vec<String>>,
458}
459
460impl ParseOutput {
461    /// Whether `flag`, a flag this parse recognized, belongs to a mounted program rather than
462    /// to the CLI that mounted it.
463    ///
464    /// Answered from the descent itself: a flag a mounted command declared (or the root flags a
465    /// mount merged into the command it sits on) is the mounted program's, and a flag the
466    /// parser built by merging a re-declaration into an inherited global is whoever's that
467    /// global was. Compared by identity, so pass a flag this parse handed out, such as one in
468    /// [`ParseOutput::flag_awaiting_value`] or [`ParseOutput::available_flags`].
469    pub fn is_mounted_flag(&self, flag: &Arc<SpecFlag>) -> bool {
470        self.mounted_flags
471            .iter()
472            .any(|known| Arc::ptr_eq(known, flag))
473    }
474
475    /// Bring `cmd`'s flags into [`ParseOutput::available_flags`] the way a descent does,
476    /// remembering which of them came from a mounted program.
477    fn merge_command_flags(&mut self, cmd: &SpecCommand, crossing_mount: bool) {
478        let flags = gather_flags(cmd);
479        if cmd.mounted || cmd.flags_from_mount {
480            self.mounted_flags.extend(flags.values().cloned());
481        }
482        for (merged, origin) in
483            merge_subcommand_flags(&mut self.available_flags, flags, crossing_mount)
484        {
485            if self
486                .mounted_flags
487                .iter()
488                .any(|known| Arc::as_ptr(known) as usize == origin)
489            {
490                self.mounted_flags.push(merged);
491            }
492        }
493    }
494
495    /// The flags a completion should offer for the parsed command.
496    ///
497    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
498    /// command has been reached, though, the commands above it belong to the mounting CLI and
499    /// their flags are not accepted there — mise, for example, forwards everything after a task
500    /// name to the task itself — so only the flags declared from the mount boundary down are
501    /// offered. Those globals stay in `available_flags` because they may legitimately appear
502    /// *before* the mounted command.
503    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
504        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
505            return self.available_flags.clone();
506        };
507        // A mount can also merge flags from its spec's root into the command it is mounted on
508        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
509        // replay starts one level up to inherit its globals.
510        let start = match boundary.checked_sub(1) {
511            Some(prev) if self.cmds[prev].flags_from_mount => prev,
512            _ => boundary,
513        };
514        // Re-run the descent from there, which starts with no inherited flags. Below the
515        // boundary the mounted program's commands are ordinary commands, so the descents use
516        // the same merge as the real parse.
517        let mut offered = gather_flags(&self.cmds[start]);
518        for cmd in &self.cmds[start + 1..] {
519            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
520        }
521        offered
522    }
523}
524
525#[derive(Debug, Clone)]
526pub enum ParseValue {
527    Bool(bool),
528    String(String),
529    MultiBool(Vec<bool>),
530    MultiString(Vec<String>),
531}
532
533impl ParseValue {
534    pub fn try_as_bool(self) -> Option<bool> {
535        match self {
536            Self::Bool(value) => Some(value),
537            _ => None,
538        }
539    }
540
541    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
542        match self {
543            Self::Bool(value) => Some(value),
544            _ => None,
545        }
546    }
547
548    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
549        match self {
550            Self::Bool(value) => Some(value),
551            _ => None,
552        }
553    }
554
555    pub fn try_as_string(self) -> Option<String> {
556        match self {
557            Self::String(value) => Some(value),
558            _ => None,
559        }
560    }
561
562    pub const fn try_as_string_ref(&self) -> Option<&String> {
563        match self {
564            Self::String(value) => Some(value),
565            _ => None,
566        }
567    }
568
569    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
570        match self {
571            Self::String(value) => Some(value),
572            _ => None,
573        }
574    }
575
576    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
577        match self {
578            Self::MultiBool(value) => Some(value),
579            _ => None,
580        }
581    }
582
583    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
584        match self {
585            Self::MultiBool(value) => Some(value),
586            _ => None,
587        }
588    }
589
590    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
591        match self {
592            Self::MultiBool(value) => Some(value),
593            _ => None,
594        }
595    }
596
597    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
598        match self {
599            Self::MultiString(value) => Some(value),
600            _ => None,
601        }
602    }
603
604    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
605        match self {
606            Self::MultiString(value) => Some(value),
607            _ => None,
608        }
609    }
610
611    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
612        match self {
613            Self::MultiString(value) => Some(value),
614            _ => None,
615        }
616    }
617}
618
619/// The deprecated declarations argv itself named: the commands it descended through, and the
620/// flags it bound.
621///
622/// Called before the environment and defaults have filled anything, because afterwards nothing
623/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
624/// differently, at the point where each is applied.
625///
626/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
627/// including `--help`, and the compiled parser reports selected commands rather than the one the
628/// process already is.
629fn collect_deprecations(out: &mut ParseOutput) {
630    for cmd in out.cmds.iter().skip(1) {
631        if cmd.deprecated.is_none()
632            && cmd.deprecated_warn_at.is_none()
633            && cmd.deprecated_remove_at.is_none()
634        {
635            continue;
636        }
637        out.warnings.push(Warning::command(
638            cmd.name.clone(),
639            cmd.deprecated.clone(),
640            cmd.deprecated_warn_at.clone(),
641            cmd.deprecated_remove_at.clone(),
642        ));
643    }
644    for flag in out.flags.keys() {
645        if let Some(warning) = flag_deprecation(flag) {
646            out.warnings.push(warning);
647        }
648    }
649}
650
651/// A warning for a flag that was used, if its declaration is deprecated at all.
652fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
653    if flag.deprecated.is_none()
654        && flag.deprecated_warn_at.is_none()
655        && flag.deprecated_remove_at.is_none()
656    {
657        return None;
658    }
659    Some(Warning::flag(
660        flag_spelling(flag),
661        flag.deprecated.clone(),
662        flag.deprecated_warn_at.clone(),
663        flag.deprecated_remove_at.clone(),
664    ))
665}
666
667/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
668/// about `old-flag` would be about a word nobody typed.
669fn flag_spelling(flag: &SpecFlag) -> String {
670    flag.long
671        .first()
672        .map(|long| format!("--{long}"))
673        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
674        .unwrap_or_else(|| flag.name.clone())
675}
676
677/// The name this flag reads first, which is what to use instead of a deprecated alias.
678fn flag_current_env(flag: &SpecFlag) -> Option<String> {
679    flag.env
680        .clone()
681        .or_else(|| flag.env_fallback.first().cloned())
682}
683
684fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
685    flag.deprecated_env.iter().any(|declared| declared == name)
686}
687
688/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
689fn arg_current_env(arg: &SpecArg) -> Option<String> {
690    arg.env
691        .clone()
692        .or_else(|| arg.env_fallback.first().cloned())
693}
694
695fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
696    arg.deprecated_env.iter().any(|declared| declared == name)
697}
698
699/// The first of `names` that is set, and which one it was.
700///
701/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
702/// aliases, so the winner's identity is what says whether a value arrived through an alias.
703/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
704/// disagree with it.
705fn first_set_env<'a>(
706    mut names: impl Iterator<Item = &'a str>,
707    get_env: &impl Fn(&str) -> Option<String>,
708) -> Option<(&'a str, String)> {
709    names.find_map(|name| get_env(name).map(|value| (name, value)))
710}
711
712/// Builder for parsing command-line arguments with custom options.
713///
714/// Use this when you need to customize parsing behavior, such as providing
715/// a custom environment variable map instead of using the process environment.
716///
717/// # Example
718/// ```
719/// use std::collections::HashMap;
720/// use usage::Spec;
721/// use usage::parse::Parser;
722///
723/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
724/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
725///
726/// let result = Parser::new(&spec)
727///     .with_env(env)
728///     .parse(&["cmd".into()])
729///     .unwrap();
730/// ```
731#[non_exhaustive]
732pub struct Parser<'a> {
733    spec: &'a Spec,
734    env: Option<HashMap<String, String>>,
735    mount_outputs: Option<HashMap<String, String>>,
736    run_commands: bool,
737}
738
739impl<'a> Parser<'a> {
740    /// Create a new parser for the given spec.
741    pub fn new(spec: &'a Spec) -> Self {
742        Self {
743            spec,
744            env: None,
745            mount_outputs: None,
746            run_commands: true,
747        }
748    }
749
750    /// Use a custom environment variable map instead of the process environment.
751    ///
752    /// This is useful when parsing for tasks in a monorepo where the env vars
753    /// come from a child config file rather than the current process environment.
754    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
755        self.env = Some(env);
756        self
757    }
758
759    /// Inject deterministic outputs for mount commands instead of executing them.
760    ///
761    /// Keys are the exact `run` strings declared by mount nodes and values are the
762    /// usage specs those commands would print. When this is set, every encountered
763    /// mount must have an entry. Production parsing remains process-backed unless a
764    /// caller explicitly opts into injection.
765    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
766        self.mount_outputs = Some(outputs);
767        self
768    }
769
770    /// Never run a `choices run=` command while parsing.
771    ///
772    /// For a caller that inspects a command line rather than acting on it. A value that the
773    /// declared choices do not accept is let through unchecked instead of being checked
774    /// against the command's output, and a help page describes the command rather than
775    /// listing what it prints. Mounts are unaffected; see [`Parser::with_mount_outputs`].
776    pub fn without_running_commands(mut self) -> Self {
777        self.run_commands = false;
778        self
779    }
780
781    /// Parse the input arguments.
782    ///
783    /// Returns the parsed arguments and flags, with defaults and env vars applied.
784    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
785        let out = self.parse_collecting(input)?;
786        if let Some(err) = out
787            .errors
788            .iter()
789            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
790        {
791            bail!("{err}");
792        }
793        if !out.errors.is_empty() {
794            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
795        }
796        Ok(out)
797    }
798
799    /// Everything the parse learned, whether or not it succeeded.
800    ///
801    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
802    /// caller about to act on a command line. A caller that wants to *explain* one wants
803    /// the opposite: the bindings that worked and every complaint about the rest, since a
804    /// report saying only "missing required <src>" is the report you already had.
805    ///
806    /// Failures that stop the parse dead — a mount that will not run, a word no
807    /// declaration can take — still come back as `Err`. There is no output to describe in
808    /// those cases; see [`Parser::explain`] for what to do about it.
809    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
810        self.parse_collecting(input)
811    }
812
813    /// The binding phase's own answer for a line [`Parser::explain`] refused.
814    ///
815    /// `Ok` when the binding phase finished and the failure came after it — a flag left
816    /// waiting for a value, say. Everything argv supplied is there and only the
817    /// environment-and-defaults phase is missing.
818    ///
819    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
820    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
821    /// is more useful next to the three tokens that did bind than on its own. The word that
822    /// caused the failure carries a role saying so, and everything still queued behind it is
823    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
824    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
825    /// than in the line, such as a mount that will not run, stops the trace where it stopped
826    /// and the words past it carry no role.
827    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
828        let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
829        let mut trace = Trace::new(input);
830        match parse_partial_traced(
831            self.spec,
832            input,
833            self.env.as_ref(),
834            self.mount_outputs.as_ref(),
835            MountTiming::WhenAWordIsUnknown,
836            true,
837            &mut trace,
838        ) {
839            // A parse that got as far as stopping normally already moved its tokens onto the
840            // output, which is where a caller should read them from.
841            Ok((out, _)) => Ok(out),
842            Err(_) => Err(trace.tokens),
843        }
844    }
845
846    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
847        // Held across both phases: defaults are checked against the same `choices run=`
848        // output the words were.
849        let _runs = crate::spec::choices::RunScope::enter();
850        let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
851        let custom_env = self.env.as_ref();
852        let (mut out, overridden_flags) = parse_partial_with_env(
853            self.spec,
854            input,
855            custom_env,
856            self.mount_outputs.as_ref(),
857            MountTiming::WhenAWordIsUnknown,
858            false,
859        )?;
860        restore_current_clause(&mut out);
861        trace!("{out:?}");
862
863        // A flag still waiting for a value never got one, so the command line ended
864        // mid-flag. `parse_partial` leaves this for completions to look at — a
865        // half-typed `--jobs ` is exactly what a completion is asked about — but a
866        // full parse has nothing left to wait for, and dropping the flag silently
867        // made a forgotten value look like a working command.
868        while try_bind_default_missing(
869            &mut out.flags,
870            &mut out.flag_awaiting_value,
871            custom_env,
872            &mut out.flag_origins,
873        )? {}
874        if let Some(flag) = out.flag_awaiting_value.first() {
875            let token = flag
876                .long
877                .first()
878                .map(|l| format!("--{l}"))
879                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
880                .unwrap_or_else(|| flag.name.clone());
881            let rendered = input.join(" ");
882            let span = rendered
883                .rfind(&token)
884                .map(|at| (at, token.len()))
885                .unwrap_or((0, 0));
886            return Err(UsageErr::InvalidFlag {
887                token,
888                reason: "requires an argument".to_string(),
889                span: span.into(),
890                input: rendered,
891            }
892            .into());
893        }
894
895        // Before the environment and defaults have their turn, because both mark a field as
896        // filled and only argv can be reported as something the user typed. Env is reported
897        // where it is applied, below; a default is nobody's request and reports nothing.
898        collect_deprecations(&mut out);
899
900        let get_env = |key: &str| -> Option<String> {
901            if let Some(env_map) = custom_env {
902                env_map.get(key).cloned()
903            } else {
904                std::env::var(key).ok()
905            }
906        };
907
908        // Apply env vars and defaults for args
909        //
910        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
911        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
912        for arg in active_args(&out.cmd) {
913            // Clause instances contain argv only: defaults and environment values do not
914            // manufacture fields inside a repeated group.
915            if out.cmd.clause.is_some() {
916                break;
917            }
918            if out.args.contains_key(arg) {
919                continue;
920            }
921            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
922                if arg_env_is_deprecated(arg, env_name) {
923                    out.warnings
924                        .push(Warning::env(env_name, arg_current_env(arg)));
925                }
926                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
927                validate_choice_values(
928                    ChoiceTarget::arg(arg),
929                    &values,
930                    arg.choices.as_ref(),
931                    custom_env,
932                )?;
933                let parsed = if arg.var {
934                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
935                    ParseValue::MultiString(values)
936                } else {
937                    ParseValue::String(values.into_iter().next().unwrap_or_default())
938                };
939                out.args.insert(Arc::new(arg.clone()), parsed);
940                out.arg_origins
941                    .entry(Arc::new(arg.clone()))
942                    .or_default()
943                    .push(ValueOrigin::Env(env_name.to_string()));
944                continue;
945            }
946            if !arg.default.is_empty() {
947                // Consider var when deciding the type of default return value
948                if arg.var {
949                    let values = split_fallback_values(&arg.default, arg.delimiter);
950                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
951                    validate_choice_values(
952                        ChoiceTarget::arg(arg),
953                        &values,
954                        arg.choices.as_ref(),
955                        custom_env,
956                    )?;
957                    // For var=true, always return a vec (MultiString)
958                    out.args
959                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
960                    out.arg_origins
961                        .entry(Arc::new(arg.clone()))
962                        .or_default()
963                        .push(ValueOrigin::Default);
964                } else {
965                    validate_choice_value(
966                        ChoiceTarget::arg(arg),
967                        &arg.default[0],
968                        arg.choices.as_ref(),
969                        custom_env,
970                    )?;
971                    // For var=false, return the first default value as String
972                    out.args.insert(
973                        Arc::new(arg.clone()),
974                        ParseValue::String(arg.default[0].clone()),
975                    );
976                    out.arg_origins
977                        .entry(Arc::new(arg.clone()))
978                        .or_default()
979                        .push(ValueOrigin::Default);
980                }
981            }
982        }
983
984        // Environment first, for every flag, so a `default_if` can see a sibling
985        // that was filled from env. Applying both in one pass would make the
986        // answer depend on declaration order: `--bin-names` before `--json`
987        // would miss `EX_JSON=1`.
988        let flags: Vec<Arc<SpecFlag>> = out
989            .available_flags
990            .values()
991            .filter(|flag| !is_clause_scoped_flag(&out, flag))
992            .cloned()
993            .collect();
994        for flag in &flags {
995            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
996                continue;
997            }
998            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
999                // The flag's own deprecation before the alias's, which is the order the
1000                // compiled parser reports them in: it walks a command's flags and then its
1001                // aliases. Using a deprecated flag through a variable is still using it.
1002                if let Some(warning) = flag_deprecation(flag) {
1003                    out.warnings.push(warning);
1004                }
1005                if flag_env_is_deprecated(flag, env_name) {
1006                    out.warnings
1007                        .push(Warning::env(env_name, flag_current_env(flag)));
1008                }
1009                if let Some(arg) = flag.arg.as_ref() {
1010                    let values =
1011                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
1012                    validate_choice_values(
1013                        ChoiceTarget::option(flag),
1014                        &values,
1015                        arg.choices.as_ref(),
1016                        custom_env,
1017                    )?;
1018                    let parsed = if flag.var || arg.var {
1019                        if flag.var {
1020                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
1021                        }
1022                        if arg.var {
1023                            validate_flag_arg_fallback_count(
1024                                flag,
1025                                arg,
1026                                values.len(),
1027                                &mut out.errors,
1028                            );
1029                        }
1030                        ParseValue::MultiString(values)
1031                    } else {
1032                        ParseValue::String(values.into_iter().next().unwrap_or_default())
1033                    };
1034                    out.flags.insert(Arc::clone(flag), parsed);
1035                } else {
1036                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
1037                    out.flags
1038                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
1039                }
1040                out.flag_origins
1041                    .entry(Arc::clone(flag))
1042                    .or_default()
1043                    .push(ValueOrigin::Env(env_name.to_string()));
1044            }
1045        }
1046        // Decide every `default_if` against argv+env only. Binding as we go would put
1047        // a default into `out.flags` and make the next flag's condition treat it as
1048        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
1049        // here, so an unconditional `default` on `--json` must not fire
1050        // `default_if "--json"`.
1051        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
1052        for flag in &flags {
1053            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1054                continue;
1055            }
1056            if let Some(condition) = flag.default_if.iter().find(|condition| {
1057                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
1058            }) {
1059                from_default_if.push((Arc::clone(flag), condition.clone()));
1060            }
1061        }
1062        for (flag, condition) in &from_default_if {
1063            // The whole condition, not just the value: several conditions may name the same
1064            // selector with different `when` values, so the selector alone does not say
1065            // which one fired.
1066            bind_flag_fallback(
1067                flag,
1068                std::slice::from_ref(&condition.value),
1069                &mut out,
1070                custom_env,
1071                ValueOrigin::DefaultIf {
1072                    selector: condition.selector.clone(),
1073                    when: condition.when.clone(),
1074                },
1075            )?;
1076        }
1077        for flag in &flags {
1078            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1079                continue;
1080            }
1081            if !flag.default.is_empty() {
1082                bind_flag_fallback(
1083                    flag,
1084                    &flag.default,
1085                    &mut out,
1086                    custom_env,
1087                    ValueOrigin::Default,
1088                )?;
1089                continue;
1090            }
1091            if let Some(arg) = flag.arg.as_ref()
1092                && !arg.default.is_empty()
1093            {
1094                bind_flag_fallback(
1095                    flag,
1096                    &arg.default,
1097                    &mut out,
1098                    custom_env,
1099                    ValueOrigin::Default,
1100                )?;
1101            }
1102        }
1103        // The binding phase leaves the last clause instance in `args`/`flags` so
1104        // completion can inspect it. A full parse closes it before applying scoped
1105        // fallbacks: defaults and environment values fill instances that argv made,
1106        // but never manufacture a repeated group on their own.
1107        finalize_current_clause(&mut out);
1108        let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1109        validate_clause_relationships(
1110            &mut out,
1111            &overridden_flags,
1112            custom_env,
1113            clause_explicit.as_deref(),
1114        );
1115        // Declarative value validation is deliberately post-binding. Defaults and
1116        // environment fallbacks have landed by here, and delimiters were already split
1117        // while binding. Like clap's value parsers, a declaration judges each resulting
1118        // raw value independently.
1119        for (arg, parsed) in &out.args {
1120            validate_expression(
1121                &arg.name,
1122                arg.validate.as_deref(),
1123                arg.validate_error.as_deref(),
1124                parsed,
1125                &mut out.errors,
1126            );
1127        }
1128        if let Some(clause) = &out.cmd.clause {
1129            let mut clause_errors = Vec::new();
1130            for (index, instance) in out
1131                .clauses
1132                .get(&clause.name)
1133                .into_iter()
1134                .flatten()
1135                .enumerate()
1136            {
1137                for arg in &clause.args {
1138                    let Some(value) = instance.get(arg) else {
1139                        if arg.required {
1140                            clause_errors.push(UsageErr::MissingClauseArg {
1141                                clause: clause.name.clone(),
1142                                instance: index + 1,
1143                                arg: arg.name.clone(),
1144                            });
1145                        }
1146                        continue;
1147                    };
1148                    if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1149                        if let Some(min) = arg.var_min
1150                            && values.len() < min
1151                        {
1152                            clause_errors.push(UsageErr::VarArgTooFew {
1153                                name: format!(
1154                                    "{} instance {}: {}",
1155                                    clause.name,
1156                                    index + 1,
1157                                    arg.name
1158                                ),
1159                                min,
1160                                got: values.len(),
1161                            });
1162                        }
1163                        if let Some(max) = arg.var_max
1164                            && values.len() > max
1165                        {
1166                            clause_errors.push(UsageErr::VarArgTooMany {
1167                                name: format!(
1168                                    "{} instance {}: {}",
1169                                    clause.name,
1170                                    index + 1,
1171                                    arg.name
1172                                ),
1173                                max,
1174                                got: values.len(),
1175                            });
1176                        }
1177                    }
1178                }
1179            }
1180            out.errors.extend(clause_errors);
1181        }
1182        let clause_flag_names = out
1183            .cmd
1184            .clause
1185            .iter()
1186            .flat_map(|clause| &clause.flags)
1187            .map(|flag| flag.name.as_str())
1188            .collect::<HashSet<_>>();
1189        for (flag, parsed) in out
1190            .flags
1191            .iter()
1192            .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1193        {
1194            if let Some(arg) = &flag.arg {
1195                validate_expression(
1196                    &flag.name,
1197                    arg.validate.as_deref(),
1198                    arg.validate_error.as_deref(),
1199                    parsed,
1200                    &mut out.errors,
1201                );
1202            }
1203        }
1204        // Against the environment this parse read from, so a caller that supplies its own map
1205        // is judged on that map and not on the process it happens to run in. The commands it
1206        // walked count as well as the spec's: a mount brings its commands in only now. A mount
1207        // this command line never needed is not run for the check — its own flags and timing
1208        // belong to the parse, not here — so what only it declares goes in `allow`.
1209        //
1210        // Not at all for a help or version request: the user asking how to fix the variable
1211        // should get the answer, and nothing should run on the way to it.
1212        let answering = out
1213            .errors
1214            .iter()
1215            .any(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)));
1216        if let Some(unknown_env) = self.spec.unknown_env.as_ref().filter(|_| !answering) {
1217            let process: Vec<std::ffi::OsString>;
1218            // A supplied map is read by exact key, so it is checked by exact key: on Windows a
1219            // map holding `ex_port` does not fill `EX_PORT`, and saying nothing about it would
1220            // hide the very mistake this check exists for. The process environment folds case
1221            // there, and so does its check.
1222            let (names, exact): (Vec<&std::ffi::OsStr>, bool) = match custom_env {
1223                Some(env) => (env.keys().map(std::ffi::OsStr::new).collect(), true),
1224                None => {
1225                    process = std::env::vars_os().map(|(name, _)| name).collect();
1226                    (process.iter().map(|n| n.as_os_str()).collect(), false)
1227                }
1228            };
1229            let unknown =
1230                unknown_env.unknown_with(self.spec, &out.cmds, names.iter().copied(), exact);
1231            if !unknown.is_empty() {
1232                out.errors.push(UsageErr::UnknownEnv {
1233                    prefix: unknown_env.prefix.clone(),
1234                    names: unknown,
1235                });
1236            }
1237        }
1238        // Applied once, here, because this is where the CLI's own version is known: a
1239        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1240        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1241        Ok(out)
1242    }
1243}
1244
1245/// Parse command-line arguments according to a spec.
1246///
1247/// Returns the parsed arguments and flags, with defaults and env vars applied.
1248/// Uses `std::env::var` for environment variable lookups.
1249///
1250/// For custom environment variable handling, use [`Parser`] instead.
1251#[must_use = "parsing result should be used"]
1252pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1253    Parser::new(spec).parse(input)
1254}
1255
1256/// Parse command-line arguments without applying defaults.
1257///
1258/// Use this for help text generation or when you need the raw parsed values.
1259#[must_use = "parsing result should be used"]
1260pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1261    parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1262}
1263
1264/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1265/// `.exe` stripped so Windows and Unix agree.
1266pub fn multicall_basename(argv0: &str) -> &str {
1267    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1268    match name.get(name.len().saturating_sub(4)..) {
1269        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1270        _ => name,
1271    }
1272}
1273
1274/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1275///
1276/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1277/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1278pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1279    let base = multicall_basename(argv0);
1280    if !name.is_empty() && base == multicall_basename(name) {
1281        return None;
1282    }
1283    if let Some(bin) = bin
1284        && !bin.is_empty()
1285        && base == multicall_basename(bin)
1286    {
1287        return None;
1288    }
1289    Some(base)
1290}
1291
1292/// Internal version of parse_partial that accepts an optional custom env map.
1293/// When a command's own `mount` runs, for the root — which nothing descends into.
1294///
1295/// A completion has to know every command before it can offer one, even with
1296/// nothing typed yet, so it resolves up front. An execution knows the word it was
1297/// given, so it only pays for discovery when that word matches nothing declared —
1298/// and a CLI that declares its commands and mounts a few more does not spawn a
1299/// process on every invocation.
1300#[derive(Clone, Copy, PartialEq, Eq)]
1301enum MountTiming {
1302    Eager,
1303    WhenAWordIsUnknown,
1304}
1305
1306/// One word on its way through the parser, with what the parser has learned about it.
1307///
1308/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1309/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1310/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1311/// the word itself there is nothing to keep aligned. The argv position is here for the
1312/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1313/// removed from the middle, so position in the queue stops meaning position in argv on the
1314/// first descent.
1315struct Token {
1316    word: String,
1317    /// Where in the caller's argv this word came from.
1318    ///
1319    /// A word the parser made up points at the token it was derived from — the tail of a
1320    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1321    /// token a reader would point at, and there is nothing else to point at.
1322    argv: usize,
1323    /// The flag Phase 1 read this word as, and the command level it read it at.
1324    ///
1325    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1326    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1327    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1328    /// then the recognized flags have changed, because each descent drops the parent's
1329    /// non-global flags and a mounted command may declare the same name as a global seen
1330    /// here. Recording the owner keeps a word bound to the flag it was read as.
1331    ///
1332    /// The level matters to strict parsing: clap permits an inherited global once on each
1333    /// side of a subcommand boundary.
1334    binding: Option<(Arc<SpecFlag>, usize)>,
1335}
1336
1337/// Flag scope for the one token straddling an implicit default-command boundary.
1338struct DefaultBundle {
1339    argv: usize,
1340    flags: BTreeMap<String, Arc<SpecFlag>>,
1341    parent_keys: HashSet<String>,
1342}
1343
1344impl DefaultBundle {
1345    /// Preserve the declaration and command level when a short tail is requeued.
1346    fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1347        self.flags.get(key).map(|flag| {
1348            (
1349                Arc::clone(flag),
1350                usize::from(!self.parent_keys.contains(key)),
1351            )
1352        })
1353    }
1354}
1355
1356impl Token {
1357    fn new(word: String, argv: usize) -> Self {
1358        Self {
1359            word,
1360            argv,
1361            binding: None,
1362        }
1363    }
1364}
1365
1366/// The token trace, while it is being built.
1367///
1368/// One row per word of the caller's argv, so a role can be recorded against a position
1369/// without the recorder having to know how many words came before it. Words the parser
1370/// made up have no row of their own and fold onto the row they were derived from.
1371struct Trace {
1372    tokens: Vec<TokenBinding>,
1373}
1374
1375impl Trace {
1376    fn new(input: &[String]) -> Self {
1377        Self {
1378            tokens: input
1379                .iter()
1380                .enumerate()
1381                .map(|(index, word)| TokenBinding {
1382                    index,
1383                    word: word.clone(),
1384                    synthesized: false,
1385                    roles: vec![],
1386                })
1387                .collect(),
1388        }
1389    }
1390
1391    fn record(&mut self, argv: usize, role: TokenRole) {
1392        if let Some(token) = self.tokens.get_mut(argv) {
1393            token.roles.push(role);
1394        }
1395    }
1396
1397    /// Note that what was read at this position is not what the caller wrote there.
1398    fn note_synthesized(&mut self, argv: usize) {
1399        if let Some(token) = self.tokens.get_mut(argv) {
1400            token.synthesized = true;
1401        }
1402    }
1403
1404    /// Every word the parse never reached, once it has stopped.
1405    fn close(&mut self, unread: &VecDeque<Token>) {
1406        for token in unread {
1407            self.record(token.argv, TokenRole::Unread);
1408        }
1409    }
1410}
1411
1412fn parse_partial_with_env(
1413    spec: &Spec,
1414    input: &[String],
1415    custom_env: Option<&HashMap<String, String>>,
1416    mount_outputs: Option<&HashMap<String, String>>,
1417    mount_timing: MountTiming,
1418    validate_clauses: bool,
1419) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1420    let mut trace = Trace::new(input);
1421    parse_partial_traced(
1422        spec,
1423        input,
1424        custom_env,
1425        mount_outputs,
1426        mount_timing,
1427        validate_clauses,
1428        &mut trace,
1429    )
1430}
1431
1432/// Locate the first default-only flag, while preserving an explicit sibling selector.
1433/// This only reads declarations: it neither binds values nor executes mount commands.
1434fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1435    let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1436    let parent = gather_flags(root);
1437    let child = gather_flags(default);
1438    let path = std::slice::from_ref(root);
1439    let mut at = None;
1440    let mut i = 0;
1441    while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1442        let scope = if at.is_some() { default } else { root };
1443        if token == "--"
1444            || token == "-"
1445            || scope
1446                .clause
1447                .as_ref()
1448                .is_some_and(|c| c.separator.as_deref() == Some(token))
1449        {
1450            return at;
1451        }
1452        if !is_flag_like(token) {
1453            if at.is_none()
1454                && (root.find_subcommand(token).is_some()
1455                    || (token == "help"
1456                        && spec.disable_help != Some(true)
1457                        && !root.disable_help_subcommand))
1458            {
1459                return None;
1460            }
1461            return at;
1462        }
1463        let mut value_flag = None;
1464        let mut attached = None;
1465        if token.starts_with("--") {
1466            let (name, value) = token
1467                .split_once('=')
1468                .map_or((token, None), |(n, v)| (n, Some(v)));
1469            if !parent.contains_key(name)
1470                && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1471            {
1472                i += 1;
1473                continue;
1474            }
1475            let flag = parent.get(name).or_else(|| child.get(name));
1476            let Some(flag) = flag else {
1477                return at;
1478            };
1479            if !parent.contains_key(name) {
1480                at.get_or_insert(i);
1481            }
1482            if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1483                value_flag = Some(flag);
1484                attached = value;
1485            }
1486        } else {
1487            for (offset, letter) in token.char_indices().skip(1) {
1488                let key = format!("-{letter}");
1489                if !parent.contains_key(&key) && supplies_short(spec, path, letter) {
1490                    continue;
1491                }
1492                let flag = parent.get(&key).or_else(|| child.get(&key));
1493                let Some(flag) = flag else {
1494                    return at;
1495                };
1496                if !parent.contains_key(&key) {
1497                    at.get_or_insert(i);
1498                }
1499                if flag.arg.is_some() {
1500                    value_flag = Some(flag);
1501                    let rest = &token[offset + letter.len_utf8()..];
1502                    attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1503                    break;
1504                }
1505            }
1506        }
1507        i += 1;
1508        if let Some(flag) = value_flag {
1509            let scope = if at.is_some() { default } else { root };
1510            let is_separator = |next: &str| {
1511                scope
1512                    .clause
1513                    .as_ref()
1514                    .is_some_and(|c| c.separator.as_deref() == Some(next))
1515            };
1516            let first = if attached.is_some() {
1517                attached
1518            } else {
1519                input
1520                    .get(i)
1521                    .map(|t| t.word.as_str())
1522                    .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1523                    .inspect(|_| i += 1)
1524            };
1525            if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1526                return at;
1527            }
1528            let arg = flag.arg.as_ref().expect("value flag");
1529            if arg.var {
1530                let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1531                let mut count = first.map_or(0, count_values);
1532                while arg.var_max.is_none_or(|max| count < max) {
1533                    let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1534                        break;
1535                    };
1536                    if is_separator(next) {
1537                        break;
1538                    }
1539                    if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1540                        if next != "--" {
1541                            i += 1;
1542                        }
1543                        break;
1544                    }
1545                    if is_flag_like(next)
1546                        && !(arg.allow_negative_numbers && is_negative_number(next))
1547                    {
1548                        break;
1549                    }
1550                    if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1551                        break;
1552                    }
1553                    count += count_values(next);
1554                    i += 1;
1555                }
1556            }
1557        }
1558    }
1559    at
1560}
1561
1562/// The binding phase, with the trace left somewhere the caller can still read it.
1563///
1564/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1565/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1566/// words read before the failure are most of what a report wants, so the caller owns the
1567/// trace instead and keeps them. See [`Parser::explain_refused`].
1568fn parse_partial_traced(
1569    spec: &Spec,
1570    input: &[String],
1571    custom_env: Option<&HashMap<String, String>>,
1572    mount_outputs: Option<&HashMap<String, String>>,
1573    mount_timing: MountTiming,
1574    validate_clauses: bool,
1575    trace: &mut Trace,
1576) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1577    // Each `choices run=` runs once per parse, however many values are checked against it.
1578    let _runs = crate::spec::choices::RunScope::enter();
1579    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1580        let viewed = spec.for_view(view)?;
1581        return parse_partial_traced(
1582            &viewed,
1583            input,
1584            custom_env,
1585            mount_outputs,
1586            mount_timing,
1587            validate_clauses,
1588            trace,
1589        );
1590    }
1591    trace!("parse_partial: {input:?}");
1592    let mut input = input
1593        .iter()
1594        .enumerate()
1595        .map(|(argv, word)| Token::new(word.clone(), argv))
1596        .collect::<VecDeque<_>>();
1597    let argv0 = input.pop_front();
1598    if let Some(argv0) = argv0.as_ref() {
1599        trace.record(argv0.argv, TokenRole::Program);
1600    }
1601    if spec.multicall
1602        && let Some(raw) = argv0
1603        && let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str()))
1604    {
1605        // A symlink invocation reads a word the caller never typed — the basename of
1606        // the program itself — so argv[0] is both the program and, below, whatever
1607        // that word selects.
1608        trace.note_synthesized(raw.argv);
1609        input.push_front(Token::new(applet.to_string(), raw.argv));
1610    }
1611    // The policy observes the selected command's own argv, not values eventually filled from
1612    // env/default. Start at the root, then reset on every explicit descent. A default
1613    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1614    let mut command_has_argv = !input.is_empty();
1615    // Unlike `out.args`, this survives clause finalization and restart rewinds. It records
1616    // whether the root actually consumed a positional token before empty-default routing.
1617    let mut root_positional_arg_found = false;
1618
1619    let mut out = ParseOutput {
1620        cmd: spec.cmd.clone(),
1621        cmds: vec![spec.cmd.clone()],
1622        args: IndexMap::new(),
1623        clauses: IndexMap::new(),
1624        clause_flags: IndexMap::new(),
1625        flags: IndexMap::new(),
1626        tokens: vec![],
1627        flag_origins: IndexMap::new(),
1628        arg_origins: IndexMap::new(),
1629        overridden_flags: BTreeMap::new(),
1630        available_flags: gather_flags(&spec.cmd),
1631        mounted_flags: vec![],
1632        flag_awaiting_value: vec![],
1633        errors: vec![],
1634        warnings: vec![],
1635        next_arg: None,
1636        double_dash_seen: false,
1637        external: None,
1638    };
1639    // A spec whose mounts were resolved ahead of time can start inside a mounted program.
1640    if spec.cmd.mounted || spec.cmd.flags_from_mount {
1641        out.mounted_flags = out.available_flags.values().cloned().collect();
1642    }
1643    // Keep this internal so adding relationship support remains semver-compatible. The full
1644    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1645    let mut overridden_flags = HashSet::new();
1646    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1647    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1648    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1649    // or an inherited `-c` belonged to the ancestor.
1650    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1651
1652    // Phase 1: Scan for subcommands and collect global flags
1653    //
1654    // This phase identifies subcommands early because they may have mount points
1655    // that need to be executed with the global flags that appeared before them.
1656    //
1657    // Example: "usage --verbose run task"
1658    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1659    //   -> then finds "task" as a subcommand of "run" (if it exists)
1660    //
1661    // We only collect global flags for mounts because:
1662    // - Non-global flags are specific to the current command, not subcommands
1663    // - Global flags affect all commands and should be passed to mount points
1664    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1665    // Which flag each word skipped here belongs to is recorded on the word — see
1666    // `Token::binding`.
1667    let mut command_arg_found = false;
1668    let mut variadic_flag_active = false;
1669    let mut idx = 0;
1670    // Track whether we've already applied the default_subcommand to prevent
1671    // multiple switches (e.g., if default is "run" and there's a task named "run")
1672    let mut used_default_subcommand = false;
1673    let mut default_bundle: Option<DefaultBundle> = None;
1674    // Whether the command in scope has had its own mounts run. A mount on the root
1675    // is the case that needs this: a subcommand's mounts are run when the parser
1676    // descends into it, but nothing descends into the root.
1677    let mut mounts_resolved = false;
1678    // A completion needs the whole command list before it can offer anything, and
1679    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1680    // mean a root mount never contributed to the very thing it exists for.
1681    //
1682    // The default-subcommand gate applies here too, and has to: offering a discovered
1683    // command that a real parse would hand to the default instead would be worse than
1684    // not offering it. A root mount under a `default_subcommand` that does not say
1685    // `overrides_default` therefore contributes nothing anywhere, which is what
1686    // "the default outranks discovery" means.
1687    let default_outranks_mounts =
1688        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1689    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1690    {
1691        mounts_resolved = true;
1692        let mut mounted = out.cmd.clone();
1693        mounted.mount(&[], mount_outputs)?;
1694        out.merge_command_flags(&mounted, false);
1695        if let Some(last) = out.cmds.last_mut() {
1696            *last = mounted.clone();
1697        }
1698        out.cmd = mounted;
1699    }
1700
1701    let default_flag_at = spec
1702        .default_subcommand_flags
1703        .then(|| default_flag_route(spec, &out.cmd, &input))
1704        .flatten();
1705
1706    while idx < input.len() {
1707        // Only for a word that could name a command, and only when it matches
1708        // nothing already declared. A CLI that declares its commands and mounts more
1709        // does not spawn a process for every invocation, and a flag — `--help`, or
1710        // anything unrecognized — never triggers discovery at all, which it would
1711        // otherwise do simply by not being a subcommand.
1712        // A declared `default_subcommand` already says what an unmatched word means,
1713        // and it costs nothing — so discovery waits behind it unless a mount asks to
1714        // outrank it. Without this, a task runner would spawn its discovery process
1715        // once per task invocation.
1716        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1717            default_accepts_word(&out.cmd, name, &input[idx].word)
1718                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1719        });
1720        if !mounts_resolved
1721            && !out.cmd.mounts.is_empty()
1722            && !default_catches_it
1723            && is_command_word(&input[idx].word)
1724            && !is_negative_number(&input[idx].word)
1725            && out.cmd.find_subcommand(&input[idx].word).is_none()
1726        {
1727            mounts_resolved = true;
1728            let mut mounted = out.cmd.clone();
1729            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1730            out.merge_command_flags(&mounted, false);
1731            if let Some(last) = out.cmds.last_mut() {
1732                *last = mounted.clone();
1733            }
1734            out.cmd = mounted;
1735        }
1736        if variadic_flag_active
1737            && out.cmd.find_subcommand(&input[idx].word).is_some()
1738            && !out.cmd.subcommand_precedence_over_arg
1739        {
1740            break;
1741        }
1742        let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1743        let selected_command = if implicit_default {
1744            out.cmd
1745                .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1746        } else {
1747            out.cmd.find_subcommand(&input[idx].word)
1748        };
1749        if let Some(subcommand) = selected_command {
1750            let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1751                && implicit_default
1752                && input[idx].word.starts_with('-')
1753                && !input[idx].word.starts_with("--")
1754                && {
1755                    let child_flags = gather_flags(subcommand);
1756                    let mut found = false;
1757                    for short in input[idx].word[1..].chars() {
1758                        let key = format!("-{short}");
1759                        if out.available_flags.contains_key(&key)
1760                            || supplies_short(spec, &out.cmds, short)
1761                        {
1762                            found = true;
1763                            break;
1764                        }
1765                        match child_flags.get(&key) {
1766                            Some(flag) if flag.arg.is_none() => {}
1767                            _ => break,
1768                        }
1769                    }
1770                    found
1771                };
1772            if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1773            {
1774                bail!(
1775                    "subcommand '{}' cannot be used with arguments on its parent command",
1776                    subcommand.name
1777                );
1778            }
1779            let boundary_parent = (implicit_default
1780                && input[idx].word.starts_with('-')
1781                && !input[idx].word.starts_with("--"))
1782            .then(|| out.available_flags.clone());
1783            let mut subcommand = subcommand.clone();
1784            // Pass prefix words (global flags before this subcommand) to mount
1785            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1786            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1787            // commands are ordinary commands relative to each other.
1788            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1789            out.merge_command_flags(&subcommand, crossing_mount);
1790            if let Some(parent) = boundary_parent {
1791                let parent_keys = parent.keys().cloned().collect();
1792                let mut flags = out.available_flags.clone();
1793                flags.extend(parent);
1794                default_bundle = Some(DefaultBundle {
1795                    argv: input[idx].argv,
1796                    flags,
1797                    parent_keys,
1798                });
1799            }
1800            // Remove subcommand from input
1801            let selected = if implicit_default {
1802                used_default_subcommand = true;
1803                None
1804            } else {
1805                input.remove(idx)
1806            };
1807            if let Some(selected) = selected {
1808                trace.record(
1809                    selected.argv,
1810                    TokenRole::Command {
1811                        name: subcommand.name.clone(),
1812                    },
1813                );
1814            }
1815            command_has_argv = idx < input.len();
1816            out.cmds.push(subcommand.clone());
1817            out.cmd = subcommand.clone();
1818            // A descent already ran the new command's mounts, above.
1819            mounts_resolved = true;
1820            prefix_flags.clear();
1821            command_arg_found = false;
1822            variadic_flag_active = false;
1823            // Continue from current position (don't reset to 0)
1824            // After remove(), idx now points to the next element
1825        } else if !is_command_word(&input[idx].word)
1826            || declared_numeric_short(&out.available_flags, &input[idx].word)
1827        {
1828            // Check if this is a known flag
1829            let word = input[idx].word.clone();
1830            let flag_key = get_flag_key(&word);
1831
1832            // A short token keys on its first letter, so `-az` would be recorded as
1833            // `-a` and its tail left over. Check the whole token here, where it is
1834            // first read: a token containing an unrecognized letter is not a bundle,
1835            // and recording it as one is what let `-a` be applied from a token that
1836            // never named it.
1837            let boundary = default_bundle
1838                .as_ref()
1839                .filter(|bundle| bundle.argv == input[idx].argv);
1840            let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1841            let is_bundle =
1842                word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1843            if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1844                command_arg_found = true;
1845                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1846                // Skip the flag and keep scanning. Both global and non-global flags may precede
1847                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1848                // stopping at one would hide the subcommand — and any mount on it — from the
1849                // parse, leaving the subcommand name to be mis-read as a positional argument.
1850                //
1851                // Only globals are forwarded to mounts: a non-global flag belongs to the
1852                // command that declared it, not to what is mounted below it.
1853                input[idx].binding = boundary
1854                    .and_then(|bundle| bundle.binding(flag_key))
1855                    .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1856                let mut forwarded = f.global.then(|| vec![word.clone()]);
1857                idx += 1;
1858
1859                // Only consume next word if flag takes an argument AND value isn't embedded
1860                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1861                if f.arg.is_some()
1862                    && !word.contains('=')
1863                    && idx < input.len()
1864                    && accepts_detached_flag_value(&f, &input[idx].word)
1865                {
1866                    if let Some(words) = forwarded.as_mut() {
1867                        words.push(input[idx].word.clone());
1868                    }
1869                    idx += 1;
1870                }
1871                if let Some(words) = forwarded {
1872                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1873                    prefix_flags.push((f, words));
1874                }
1875            } else {
1876                // Unknown flag - stop looking for subcommands
1877                // Let the main parsing phase handle the error
1878                break;
1879            }
1880        } else {
1881            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1882                idx += 1;
1883                continue;
1884            }
1885            // Found a word that's not a flag or subcommand
1886            // Check if we should use the default_subcommand (only once, and only at the
1887            // root, which is the only place a spec can declare one — `out.cmds` holds just
1888            // the root until something descends). Without that second condition the one
1889            // declared name is looked up wherever the parser happens to be standing, so an
1890            // unrelated command acquires a default because a name matched one level down:
1891            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1892            // `config ls`.
1893            if !used_default_subcommand
1894                && out.cmds.len() == 1
1895                && let Some(default_name) = &spec.default_subcommand
1896                && let Some(subcommand) = out
1897                    .cmd
1898                    .find_subcommand(default_name)
1899                    .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1900            {
1901                if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1902                    bail!(
1903                        "subcommand '{}' cannot be used with arguments on its parent command",
1904                        subcommand.name
1905                    );
1906                }
1907                let mut subcommand = subcommand.clone();
1908                // Pass prefix words (global flags before this) to mount
1909                subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1910                let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1911                out.merge_command_flags(&subcommand, crossing_mount);
1912                out.cmds.push(subcommand.clone());
1913                out.cmd = subcommand.clone();
1914                command_has_argv = true;
1915                prefix_flags.clear();
1916                command_arg_found = false;
1917                variadic_flag_active = false;
1918                // This descent ran the new command's mounts, so lazy
1919                // discovery must not run them a second time.
1920                mounts_resolved = true;
1921                used_default_subcommand = true;
1922                // Continue the loop to check if this word is a subcommand of the
1923                // default subcommand (e.g., a task name added via mount).
1924                // If it's not a subcommand, the next iteration will break and
1925                // Phase 2 will handle it as a positional arg.
1926                continue;
1927            }
1928            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1929            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1930            // default subcommand gets first refusal so interpreted and compiled routing agree
1931            // when the root sigil and the default command can both accept this word.
1932            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1933                idx += 1;
1934                continue;
1935            }
1936            // An unmatched word that names no subcommand is forwarded as an external
1937            // command: this word, then every token after it, including flags. Known
1938            // subcommands already won above, and a default_subcommand already caught.
1939            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1940            if out.cmd.external_subcommand {
1941                let rest: Vec<Token> = input.drain(idx..).collect();
1942                for token in &rest {
1943                    trace.record(token.argv, TokenRole::External);
1944                }
1945                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1946                break;
1947            }
1948            // This could be a positional argument, so stop subcommand search
1949            break;
1950        }
1951    }
1952
1953    // Phase 2: Main argument and flag parsing
1954    //
1955    // Now that we've identified all subcommands and executed their mounts,
1956    // we can parse the remaining arguments, flags, and their values.
1957
1958    // The cursor into the active positional arguments, kept as an index rather than a reference
1959    // because an explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"`
1960    // arm).
1961    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1962    // argument filled?" has to consult `out.args` by key instead of counting.
1963    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1964    let mut enable_flags = true;
1965    let mut grouped_flag = false;
1966    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1967    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1968    // words that come after it — see `report_double_dash_violation`.
1969    let mut seen_double_dash = false;
1970    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1971    // reopen sigil classification for this invocation.
1972    let mut restart_seen = false;
1973    // Args already reported as having been offered a word before the `--` they require, so a
1974    // variadic one does not report the same violation for every word it is offered.
1975    let mut double_dash_violations: HashSet<String> = HashSet::new();
1976    // A root clause separator is user-supplied syntax, so it must suppress the
1977    // empty-input default just like an explicit `--`. This is separate from
1978    // `seen_double_dash`, which resets for each clause instance.
1979    let mut root_clause_separator_seen = false;
1980    // Scalar occurrences are scoped to the command level where they were written. Inherited
1981    // globals may therefore appear once before and once after a subcommand under clap's strict
1982    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1983    // opposite forms may override one another, while repeating either spelling is an error.
1984    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1985
1986    while !input.is_empty() {
1987        let token = input.pop_front().unwrap();
1988        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1989        let binding = token.binding;
1990        let argv = token.argv;
1991        let mut w = token.word;
1992        // A short's attached value is re-queued with `grouped_flag` set, and that
1993        // continuation is not a following word. `require_equals` refuses only the
1994        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1995        // only when the value is actually missing, so `-cnever` is still `never`.
1996        let attached_continuation = grouped_flag;
1997        // The opt-in lookahead uses declared value arity, so binding must consume
1998        // the same attached value even after the default changed flag scope.
1999        // Other specs retain the reference parser's legacy short-bundle rules.
2000        if spec.default_subcommand_flags
2001            && attached_continuation
2002            && !out.flag_awaiting_value.is_empty()
2003        {
2004            grouped_flag = false;
2005            w.remove(0);
2006            if w.starts_with('=') {
2007                w.remove(0);
2008            }
2009            if bind_pending_flag_value(
2010                spec,
2011                &out.cmd,
2012                &mut out.errors,
2013                &mut out.flags,
2014                &mut out.flag_awaiting_value,
2015                &mut w,
2016                &mut input,
2017                custom_env,
2018                trace,
2019                argv,
2020                true,
2021            )? {
2022                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2023                return Ok((out, overridden_flags));
2024            }
2025            continue;
2026        }
2027
2028        // A clause boundary is syntax even after an automatic trailing argument disabled
2029        // flags. Only an explicit `--` protects a literal separator.
2030        if !seen_double_dash
2031            && let Some(clause) = out.cmd.clause.as_ref()
2032            && clause.separator.as_deref() == Some(w.as_str())
2033        {
2034            while try_bind_default_missing(
2035                &mut out.flags,
2036                &mut out.flag_awaiting_value,
2037                custom_env,
2038                &mut out.flag_origins,
2039            )? {}
2040            if let Some(flag) = out.flag_awaiting_value.first() {
2041                let spelling = flag
2042                    .long
2043                    .first()
2044                    .map(|long| format!("--{long}"))
2045                    .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2046                    .unwrap_or_else(|| flag.name.clone());
2047                return Err(UsageErr::InvalidFlag {
2048                    token: spelling.clone(),
2049                    reason: "requires an argument".to_string(),
2050                    span: (0, spelling.len()).into(),
2051                    input: spelling,
2052                }
2053                .into());
2054            }
2055            let name = clause.name.clone();
2056            if out.cmds.len() == 1 {
2057                root_clause_separator_seen = true;
2058            }
2059            finalize_current_clause(&mut out);
2060            out.arg_origins.clear();
2061            trace.record(argv, TokenRole::ClauseSeparator { name });
2062            next_arg_idx = 0;
2063            out.flag_awaiting_value.clear();
2064            reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2065            enable_flags = true;
2066            seen_double_dash = false;
2067            continue;
2068        }
2069
2070        // Check for restart_token - resets argument parsing for multiple command invocations
2071        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
2072        if let Some(ref restart_token) = out.cmd.restart_token
2073            && w == *restart_token
2074        {
2075            // Reset argument parsing state for a fresh command invocation, keeping the
2076            // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
2077            // is not cleared here either, so clearing it would let one arg report the same
2078            // violation once per invocation.
2079            out.args.clear();
2080            // With the values gone, so is where they came from — otherwise the second
2081            // invocation of `run lint ::: test` reports the first one's provenance. The
2082            // token trace is *not* cleared: those words were read, and a report that
2083            // dropped them would show a command line with a hole in it.
2084            out.arg_origins.clear();
2085            trace.record(argv, TokenRole::Restart);
2086            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2087            restart_seen = true;
2088            out.flag_awaiting_value.clear(); // Clear any pending flag values
2089            enable_flags = true; // Reset -- separator effect
2090            seen_double_dash = false; // The next invocation needs its own `--`
2091            continue;
2092        }
2093
2094        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
2095        // like, and that has to be asked before the separator arm below rather than
2096        // after it. Asked after, a `--` was consumed as a separator while the flag
2097        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
2098        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
2099        // itself, which is what clap does with the same declaration — and no flag can
2100        // still be waiting once the separator has done its job, so the starvation rule
2101        // below has no path around it.
2102        if enable_flags
2103            && !attached_continuation
2104            && w.starts_with('-')
2105            && out
2106                .flag_awaiting_value
2107                .last()
2108                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2109        {
2110            // A variadic argument collects here too: which token supplied its first
2111            // value says nothing about how many it takes.
2112            let should_return = bind_pending_flag_value(
2113                spec,
2114                &out.cmd,
2115                &mut out.errors,
2116                &mut out.flags,
2117                &mut out.flag_awaiting_value,
2118                &mut w,
2119                &mut input,
2120                custom_env,
2121                trace,
2122                argv,
2123                // The token a hyphen-valued flag takes is the following word, never attached.
2124                false,
2125            )?;
2126            if should_return {
2127                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2128                return Ok((out, overridden_flags));
2129            }
2130            continue;
2131        }
2132
2133        // A flag whose value may be omitted that cannot take this token as a detached
2134        // value finishes bare and leaves the token for whatever comes next:
2135        // `--color --verbose` colours with the missing value and still sets verbose,
2136        // and `--inspect 9229` with `require_equals` binds the missing value rather
2137        // than treating 9229 as the port.
2138        if enable_flags
2139            && !attached_continuation
2140            && !out.flag_awaiting_value.is_empty()
2141            && out.flag_awaiting_value.last().is_some_and(|flag| {
2142                (flag.default_missing.is_some() || flag.value_optional)
2143                    && !accepts_detached_flag_value(flag, &w)
2144            })
2145        {
2146            try_bind_default_missing(
2147                &mut out.flags,
2148                &mut out.flag_awaiting_value,
2149                custom_env,
2150                &mut out.flag_origins,
2151            )?;
2152        }
2153
2154        // The first explicit `--` is still a separator after an `automatic` argument has
2155        // stopped flag parsing. Once an explicit separator has done its job, a second one is
2156        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
2157        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
2158        // behavior as the bug it is.
2159        if w == "--" && !seen_double_dash {
2160            enable_flags = false;
2161
2162            // Only preserve the double dash token if we're collecting values for a variadic arg
2163            // in double_dash == `preserve` mode
2164            let should_preserve = active_args(&out.cmd)
2165                .get(next_arg_idx)
2166                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2167                .unwrap_or(false);
2168
2169            if should_preserve {
2170                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
2171                // neither counts as one nor unlocks a `double_dash="required"` arg.
2172            } else {
2173                seen_double_dash = true;
2174                trace.record(argv, TokenRole::Separator);
2175
2176                // Everything after an explicit `--` belongs to the arg that requires one, so
2177                // jump the cursor there — past any earlier arg, including a greedy variadic
2178                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
2179                // which is what `double_dash="required"` is generated from. Specs without such
2180                // an arg find nothing and keep the cursor where it was.
2181                let target = active_args(&out.cmd).iter().position(|arg| {
2182                    arg.double_dash == SpecDoubleDashChoices::Required
2183                        && !out.args.contains_key(arg)
2184                });
2185                if let Some(target) = target {
2186                    // Forward only. An unfilled required arg declared *before* the cursor is
2187                    // left where it is rather than rewound to — words already assigned to
2188                    // later args would have to be taken back for that to mean anything, and
2189                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
2190                    // `Arg::last(true)`, which is the final positional, so a spec that puts
2191                    // one ahead of others is already outside what this models.
2192                    if target > next_arg_idx {
2193                        next_arg_idx = target;
2194                    }
2195                }
2196                continue;
2197            }
2198        }
2199
2200        // long flags
2201        if enable_flags && w.starts_with("--") {
2202            grouped_flag = false;
2203            // `Some` only when an `=` was actually written, so `--jobs=` can supply
2204            // an empty value while `--jobs` supplies none. Collapsing the two lost
2205            // the flag entirely.
2206            let split = w.split_once('=');
2207            let word = split.map(|(word, _)| word).unwrap_or(&w);
2208            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2209            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2210                let command_level = binding
2211                    .as_ref()
2212                    .map(|(_, level)| *level)
2213                    .unwrap_or(out.cmds.len() - 1);
2214                parsed_flag_spellings
2215                    .entry(Arc::as_ptr(f) as usize)
2216                    .or_default()
2217                    .insert(word.to_string());
2218                // Recorded before the action check below: a token that named a flag named it
2219                // whether or not the parse can carry on afterwards.
2220                trace.record(
2221                    argv,
2222                    TokenRole::Flag {
2223                        flag: Arc::clone(f),
2224                        spelling: word.to_string(),
2225                        negated: f.negate.as_deref() == Some(word),
2226                    },
2227                );
2228                if f.action != crate::SpecFlagAction::Set {
2229                    out.errors
2230                        .push(render_action_err(spec, &out.cmd, f, word, custom_env));
2231                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2232                    return Ok((out, overridden_flags));
2233                }
2234                apply_flag_overrides(
2235                    f,
2236                    &out.available_flags,
2237                    &mut out.flags,
2238                    &mut out.flag_awaiting_value,
2239                    &mut overridden_flags,
2240                    &mut out.overridden_flags,
2241                );
2242                if let Some(pending) = out.flag_awaiting_value.first() {
2243                    out.errors.push(render_missing_flag_value(pending, &w));
2244                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2245                    return Ok((out, overridden_flags));
2246                }
2247                // An attached value only means something to a flag that takes one:
2248                // `--jobs=` is an empty string, while `--force=yes` has nothing to
2249                // give a flag that holds no value. Handing that leftover to the
2250                // positionals would re-split one token into two, so `ex --force=yes`
2251                // would fill an argument the caller never typed a word for.
2252                if f.arg.is_some() {
2253                    record_scalar_flag_occurrence(
2254                        &out.cmds,
2255                        f,
2256                        command_level,
2257                        None,
2258                        &mut scalar_occurrences,
2259                        &mut out.errors,
2260                    );
2261                    let f = Arc::clone(f);
2262                    out.flag_awaiting_value.push(Arc::clone(&f));
2263                    // The `=` has already settled that this text is the value, so it
2264                    // binds here rather than going back on the queue to be read as a
2265                    // token again — where `--jobs=--force` looked like a flag of its
2266                    // own and bound `force`, leaving `jobs` unset.
2267                    if let Some((_, val)) = split {
2268                        // The `=` settles where the *first* value came from and nothing
2269                        // more, so a variadic argument goes on collecting from the words
2270                        // after it exactly as the detached form does.
2271                        let mut val = val.to_string();
2272                        let should_return = bind_pending_flag_value(
2273                            spec,
2274                            &out.cmd,
2275                            &mut out.errors,
2276                            &mut out.flags,
2277                            &mut out.flag_awaiting_value,
2278                            &mut val,
2279                            &mut input,
2280                            custom_env,
2281                            trace,
2282                            argv,
2283                            // The `=` settled that this text is the value, so it rode in on
2284                            // the flag's own token.
2285                            true,
2286                        )?;
2287                        if should_return {
2288                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2289                            return Ok((out, overridden_flags));
2290                        }
2291                    }
2292                } else if f.count {
2293                    let arr = out
2294                        .flags
2295                        .entry(Arc::clone(f))
2296                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2297                        .try_as_multi_bool_mut()
2298                        .unwrap();
2299                    arr.push(true);
2300                } else {
2301                    let negate = f.negate.clone().unwrap_or_default();
2302                    let negated_form = word == negate;
2303                    let value = if f.bool_value {
2304                        match split.map(|(_, value)| value) {
2305                            Some("true") => !negated_form,
2306                            Some("false") => negated_form,
2307                            Some(value) => {
2308                                out.errors.push(UsageErr::InvalidValue {
2309                                    name: f.name.clone(),
2310                                    value: value.to_string(),
2311                                    reason: "expected `true` or `false`".to_string(),
2312                                });
2313                                continue;
2314                            }
2315                            None => !negated_form,
2316                        }
2317                    } else {
2318                        !negated_form
2319                    };
2320                    // Which form was typed is a question about the name, so it is
2321                    // asked of `word` rather than the whole token: the attached value
2322                    // is dropped just above, and comparing `--no-color=yes` against
2323                    // `--no-color` would take the negation down with it.
2324                    record_scalar_flag_occurrence(
2325                        &out.cmds,
2326                        f,
2327                        command_level,
2328                        Some(!negated_form),
2329                        &mut scalar_occurrences,
2330                        &mut out.errors,
2331                    );
2332                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2333                }
2334                continue;
2335            }
2336            if is_help_arg(spec, &out.cmd, &w) {
2337                out.errors
2338                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2339                trace.record(
2340                    argv,
2341                    TokenRole::Builtin {
2342                        spelling: w.clone(),
2343                    },
2344                );
2345                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2346                return Ok((out, overridden_flags));
2347            }
2348            if is_version_arg(spec, &out.cmds, &w) {
2349                out.errors.push(render_version_err(spec, w.len() > 2));
2350                trace.record(
2351                    argv,
2352                    TokenRole::Builtin {
2353                        spelling: w.clone(),
2354                    },
2355                );
2356                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2357                return Ok((out, overridden_flags));
2358            }
2359            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2360                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2361                trace.close(&input);
2362                return Err(refused.into());
2363            }
2364        }
2365
2366        // short flags
2367        //
2368        // A fresh token is checked whole before any of it is applied: `-az` with only
2369        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2370        // discovering that `z` names nothing. A grouped continuation is exempt — its
2371        // token was already checked when it arrived.
2372        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2373        let positional_negative_number = !declared_numeric_short
2374            && is_negative_number(&w)
2375            && active_args(&out.cmd)
2376                .get(next_arg_idx)
2377                .is_some_and(|arg| arg.allow_negative_numbers);
2378        if enable_flags
2379            && !grouped_flag
2380            // A word phase 1 already resolved to a flag needs no re-checking, and
2381            // the flags in scope have changed since, so re-checking would be wrong.
2382            && binding.is_none()
2383            && w.starts_with('-')
2384            && w.len() > 1
2385            && is_flag_like(&w)
2386            && !positional_negative_number
2387            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2388        {
2389            // Refused if this command asked for that; otherwise it carries on below
2390            // as one word, with none of its letters applied.
2391            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2392                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2393                trace.close(&input);
2394                return Err(refused.into());
2395            }
2396        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2397            let short = w.chars().nth(1).unwrap();
2398            if let Some(f) = binding
2399                .as_ref()
2400                .map(|(flag, _)| flag)
2401                .or_else(|| out.available_flags.get(&format!("-{short}")))
2402            {
2403                let command_level = binding
2404                    .as_ref()
2405                    .map(|(_, level)| *level)
2406                    .unwrap_or(out.cmds.len() - 1);
2407                if f.action != crate::SpecFlagAction::Set {
2408                    out.errors.push(render_action_err(
2409                        spec,
2410                        &out.cmd,
2411                        f,
2412                        &format!("-{short}"),
2413                        custom_env,
2414                    ));
2415                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2416                    return Ok((out, overridden_flags));
2417                }
2418                parsed_flag_spellings
2419                    .entry(Arc::as_ptr(f) as usize)
2420                    .or_default()
2421                    .insert(format!("-{short}"));
2422                trace.record(
2423                    argv,
2424                    TokenRole::Flag {
2425                        flag: Arc::clone(f),
2426                        spelling: format!("-{short}"),
2427                        // A short spelling is never the negated form: `negate` is a long.
2428                        negated: false,
2429                    },
2430                );
2431                apply_flag_overrides(
2432                    f,
2433                    &out.available_flags,
2434                    &mut out.flags,
2435                    &mut out.flag_awaiting_value,
2436                    &mut overridden_flags,
2437                    &mut out.overridden_flags,
2438                );
2439                if !attached_continuation && let Some(pending) = out.flag_awaiting_value.first() {
2440                    out.errors.push(render_missing_flag_value(pending, &w));
2441                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2442                    return Ok((out, overridden_flags));
2443                }
2444                let rest = &w[1 + short.len_utf8()..];
2445                if !rest.is_empty() {
2446                    // `-abc` is one token that names three flags, so the tail is read at the
2447                    // bundle's own position rather than at one of its own.
2448                    let mut tail = Token::new(format!("-{rest}"), argv);
2449                    if f.arg.is_none() {
2450                        tail.binding = default_bundle
2451                            .as_ref()
2452                            .filter(|bundle| bundle.argv == argv)
2453                            .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2454                    }
2455                    input.push_front(tail);
2456                }
2457                // A fully consumed short is no longer a grouped continuation.
2458                // Leaving this set after `-ai` made `-i` skip `require_equals`
2459                // and bind the following word.
2460                grouped_flag = !rest.is_empty();
2461                if f.arg.is_some() {
2462                    record_scalar_flag_occurrence(
2463                        &out.cmds,
2464                        f,
2465                        command_level,
2466                        None,
2467                        &mut scalar_occurrences,
2468                        &mut out.errors,
2469                    );
2470                    out.flag_awaiting_value.push(Arc::clone(f));
2471                } else if f.count {
2472                    let arr = out
2473                        .flags
2474                        .entry(Arc::clone(f))
2475                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2476                        .try_as_multi_bool_mut()
2477                        .unwrap();
2478                    arr.push(true);
2479                } else {
2480                    let negate = f.negate.clone().unwrap_or_default();
2481                    let value = w != negate;
2482                    record_scalar_flag_occurrence(
2483                        &out.cmds,
2484                        f,
2485                        command_level,
2486                        Some(value),
2487                        &mut scalar_occurrences,
2488                        &mut out.errors,
2489                    );
2490                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2491                }
2492                continue;
2493            }
2494            // The letter nothing declared may still be one the parser supplies, and it may
2495            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2496            // neither reaches the whole-token spellings below.
2497            if let Some(err) = supplied_short(spec, &out.cmds, short, custom_env) {
2498                out.errors.push(err);
2499                trace.record(
2500                    argv,
2501                    TokenRole::Builtin {
2502                        spelling: format!("-{short}"),
2503                    },
2504                );
2505                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2506                return Ok((out, overridden_flags));
2507            }
2508            if is_help_arg(spec, &out.cmd, &w) {
2509                out.errors
2510                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2511                trace.record(
2512                    argv,
2513                    TokenRole::Builtin {
2514                        spelling: w.clone(),
2515                    },
2516                );
2517                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2518                return Ok((out, overridden_flags));
2519            }
2520            if is_version_arg(spec, &out.cmds, &w) {
2521                out.errors.push(render_version_err(spec, w.len() > 2));
2522                trace.record(
2523                    argv,
2524                    TokenRole::Builtin {
2525                        spelling: w.clone(),
2526                    },
2527                );
2528                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2529                return Ok((out, overridden_flags));
2530            }
2531            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2532                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2533                trace.close(&input);
2534                return Err(refused.into());
2535            }
2536            if grouped_flag {
2537                grouped_flag = false;
2538                w.remove(0);
2539                // What is left is a short flag's attached value, and one `=` between
2540                // the letter and the value is a separator: `-j=8` means 8. Only one,
2541                // so `-j==8` still means `=8`.
2542                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2543                    w.remove(0);
2544                }
2545            }
2546        }
2547
2548        // Only while flags are still being read. A flag still waiting when the separator
2549        // was consumed is starved: its value would have to come from after the `--`,
2550        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2551        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2552        // was gone. Left waiting, it is reported as the missing value it is.
2553        // `require_equals` refuses a detached value: `--flag value` is a missing
2554        // value, not a flag of `"value"`. The attached form is still bound above.
2555        // Reported here rather than left waiting until the end of the line: falling
2556        // through would offer `value` to the positionals and call it an unexpected
2557        // word, which is the wrong error and a different one from usage-argv.
2558        if enable_flags
2559            && !attached_continuation
2560            && !out.flag_awaiting_value.is_empty()
2561            && out
2562                .flag_awaiting_value
2563                .last()
2564                .is_some_and(|flag| flag.require_equals)
2565        {
2566            let flag = out.flag_awaiting_value.last().unwrap();
2567            let token = flag
2568                .long
2569                .first()
2570                .map(|l| format!("--{l}"))
2571                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2572                .unwrap_or_else(|| flag.name.clone());
2573            out.errors.push(UsageErr::InvalidFlag {
2574                token: token.clone(),
2575                reason: "requires an argument".to_string(),
2576                span: (0, 0).into(),
2577                input: format!("{token} {w}"),
2578            });
2579            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2580            return Ok((out, overridden_flags));
2581        }
2582        if enable_flags
2583            && !out.flag_awaiting_value.is_empty()
2584            && (attached_continuation
2585                || out
2586                    .flag_awaiting_value
2587                    .last()
2588                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2589        {
2590            // Held before the drain pops it: a flag whose argument is variadic keeps
2591            // taking values after this first one.
2592            let should_return = bind_pending_flag_value(
2593                spec,
2594                &out.cmd,
2595                &mut out.errors,
2596                &mut out.flags,
2597                &mut out.flag_awaiting_value,
2598                &mut w,
2599                &mut input,
2600                custom_env,
2601                trace,
2602                argv,
2603                attached_continuation,
2604            )?;
2605            if should_return {
2606                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2607                return Ok((out, overridden_flags));
2608            }
2609            continue;
2610        }
2611
2612        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2613            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2614            .flatten()
2615        {
2616            if value.is_empty() {
2617                out.errors.push(UsageErr::InvalidValue {
2618                    name: arg.name.clone(),
2619                    value: w.clone(),
2620                    reason: format!("expected a value after sigil {sigil:?}"),
2621                });
2622                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2623                return Ok((out, overridden_flags));
2624            }
2625            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2626            let suppress_trailing_delimiter =
2627                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2628            let delimiter = if suppress_trailing_delimiter && trailing_value {
2629                None
2630            } else {
2631                arg.delimiter
2632            };
2633            let parts = match delimiter {
2634                Some(delimiter) => value
2635                    .split(delimiter)
2636                    .map(str::to_string)
2637                    .collect::<Vec<_>>(),
2638                None => vec![value.to_string()],
2639            };
2640            let mut refused = false;
2641            for part in &parts {
2642                if validate_choices(
2643                    spec,
2644                    &out.cmd,
2645                    &mut out.errors,
2646                    ChoiceTarget::arg(arg),
2647                    part,
2648                    arg.choices.as_ref(),
2649                    custom_env,
2650                )? {
2651                    refused = true;
2652                    break;
2653                }
2654            }
2655            if refused {
2656                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2657                return Ok((out, overridden_flags));
2658            }
2659            trace.record(
2660                argv,
2661                TokenRole::Sigil {
2662                    arg: Arc::new(arg.clone()),
2663                    sigil: sigil.to_string(),
2664                    values: parts.clone(),
2665                },
2666            );
2667            let key = Arc::new(arg.clone());
2668            if arg.var {
2669                let arr = out
2670                    .args
2671                    .entry(key)
2672                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2673                    .try_as_multi_string_mut()
2674                    .unwrap();
2675                arr.extend(parts);
2676            } else {
2677                out.args.insert(key, ParseValue::String(value.to_string()));
2678            }
2679            if out.cmds.len() == 1 {
2680                root_positional_arg_found = true;
2681            }
2682            continue;
2683        }
2684
2685        if out.cmd.allow_missing_positional {
2686            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2687            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2688                if current.required || out.args.contains_key(current) {
2689                    break;
2690                }
2691                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2692                    .iter()
2693                    .filter(|arg| arg.required && arg.sigil.is_none())
2694                    .count();
2695                if required_after == 0 {
2696                    break;
2697                }
2698                let remaining_values = 1 + input
2699                    .iter()
2700                    .filter(|token| {
2701                        (!enable_flags || !is_flag_like(&token.word))
2702                            && (!enable_flags
2703                                || restart_seen
2704                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2705                    })
2706                    .count();
2707                if remaining_values > required_after {
2708                    break;
2709                }
2710                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2711            }
2712        }
2713
2714        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2715            if arg.var
2716                && out.args.contains_key(arg)
2717                && arg.value_terminator.as_deref() == Some(w.as_str())
2718            {
2719                trace.record(
2720                    argv,
2721                    TokenRole::ValueTerminator {
2722                        ends: arg.name.clone(),
2723                    },
2724                );
2725                next_arg_idx += 1;
2726                continue;
2727            }
2728            // Before anything else: an arg that requires `--` accepts nothing until one has been
2729            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2730            // reported as an invalid choice, and from reaching that function's help escape.
2731            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2732                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2733                trace.record(
2734                    argv,
2735                    TokenRole::Refused {
2736                        reason: format!("{} only accepts words after `--`", arg.name),
2737                    },
2738                );
2739                // Drop the word without filling the arg or advancing the cursor: every later
2740                // word hits the same arg and is rejected the same way, so the parse still ends
2741                // in an error rather than in `unexpected word`.
2742                continue;
2743            }
2744            // Split before judging, as the flag path does: after the split the word is
2745            // no longer one value, and `choices` has to be asked about each. Judging
2746            // first rejects `src:docs` against a list that both halves are on, and
2747            // names the whole word rather than the half that was wrong.
2748            let trailing_value =
2749                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2750            let suppress_trailing_delimiter =
2751                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2752            let delimiter = if suppress_trailing_delimiter && trailing_value {
2753                None
2754            } else {
2755                arg.delimiter
2756            };
2757            let parts: Vec<String> = match delimiter {
2758                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2759                None => vec![w.clone()],
2760            };
2761            let mut refused = false;
2762            for part in &parts {
2763                if validate_choices(
2764                    spec,
2765                    &out.cmd,
2766                    &mut out.errors,
2767                    ChoiceTarget::arg(arg),
2768                    part,
2769                    arg.choices.as_ref(),
2770                    custom_env,
2771                )? {
2772                    refused = true;
2773                    break;
2774                }
2775            }
2776            if refused {
2777                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2778                return Ok((out, overridden_flags));
2779            }
2780            // `double_dash="automatic"` means the first value this arg takes is the last
2781            // token read as anything but data: a wrapper declaring it can forward flags
2782            // without its caller typing a `--`. Set before the value is stored, so the
2783            // rest of the command line is already past flag parsing.
2784            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2785                enable_flags = false;
2786            }
2787            // A flag-like word reaching a positional while flags are still being read was
2788            // offered to every declaration and matched none: under the default
2789            // `unknown_flags="value"` it becomes data, and saying so is the difference
2790            // between "you have a typo" and "this argument took your typo".
2791            let unknown_flag = enable_flags
2792                && !positional_negative_number
2793                && is_flag_like(&w)
2794                && binding.is_none();
2795            trace.record(
2796                argv,
2797                if unknown_flag {
2798                    TokenRole::UnknownFlag {
2799                        bound_as: Some(Arc::new(arg.clone())),
2800                    }
2801                } else {
2802                    TokenRole::Arg {
2803                        arg: Arc::new(arg.clone()),
2804                        values: parts.clone(),
2805                    }
2806                },
2807            );
2808            if out.cmds.len() == 1 {
2809                root_positional_arg_found = true;
2810            }
2811            if arg.var {
2812                let arr = out
2813                    .args
2814                    .entry(Arc::new(arg.clone()))
2815                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2816                    .try_as_multi_string_mut()
2817                    .unwrap();
2818                // The values this word carried, split above so that everything
2819                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2820                // counts the values the user meant rather than the words they typed.
2821                arr.extend(parts.iter().cloned());
2822                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2823                    next_arg_idx += 1;
2824                }
2825            } else {
2826                out.args
2827                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2828                next_arg_idx += 1;
2829            }
2830            if out
2831                .cmd
2832                .clause
2833                .as_ref()
2834                .is_some_and(|clause| clause.separator.is_none())
2835                && next_arg_idx >= active_args(&out.cmd).len()
2836            {
2837                while try_bind_default_missing(
2838                    &mut out.flags,
2839                    &mut out.flag_awaiting_value,
2840                    custom_env,
2841                    &mut out.flag_origins,
2842                )? {}
2843                if let Some(flag) = out.flag_awaiting_value.first() {
2844                    let spelling = flag
2845                        .long
2846                        .first()
2847                        .map(|long| format!("--{long}"))
2848                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2849                        .unwrap_or_else(|| flag.name.clone());
2850                    return Err(UsageErr::InvalidFlag {
2851                        token: spelling.clone(),
2852                        reason: "requires an argument".to_string(),
2853                        span: (0, spelling.len()).into(),
2854                        input: spelling,
2855                    }
2856                    .into());
2857                }
2858                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2859                finalize_current_clause(&mut out);
2860                out.arg_origins.clear();
2861                trace.record(argv, TokenRole::ClauseSeparator { name });
2862                next_arg_idx = 0;
2863                out.flag_awaiting_value.clear();
2864                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2865                enable_flags = true;
2866                seen_double_dash = false;
2867            }
2868            continue;
2869        }
2870        if is_help_arg(spec, &out.cmd, &w) {
2871            out.errors
2872                .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2873            trace.record(
2874                argv,
2875                TokenRole::Builtin {
2876                    spelling: w.clone(),
2877                },
2878            );
2879            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2880            return Ok((out, overridden_flags));
2881        }
2882        if is_version_arg(spec, &out.cmds, &w) {
2883            out.errors.push(render_version_err(spec, w.len() > 2));
2884            trace.record(
2885                argv,
2886                TokenRole::Builtin {
2887                    spelling: w.clone(),
2888                },
2889            );
2890            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2891            return Ok((out, overridden_flags));
2892        }
2893        trace.record(
2894            argv,
2895            TokenRole::Refused {
2896                reason: "no declaration takes this word".to_string(),
2897            },
2898        );
2899        trace.close(&input);
2900        bail!("unexpected word: {w}");
2901    }
2902
2903    // Resolve optional defaults before deciding whether argv is empty. This is deliberately
2904    // inside the opt-in gate: completion's partial parser must retain its pending value when
2905    // the feature is disabled.
2906    if spec.default_subcommand_on_empty
2907        && spec.default_subcommand.is_some()
2908        && out.cmds.len() == 1
2909        && !root_positional_arg_found
2910        && !seen_double_dash
2911        && !root_clause_separator_seen
2912        && out.errors.is_empty()
2913    {
2914        while try_bind_default_missing(
2915            &mut out.flags,
2916            &mut out.flag_awaiting_value,
2917            custom_env,
2918            &mut out.flag_origins,
2919        )? {}
2920        if !out.flag_awaiting_value.is_empty() {
2921            // A required flag value is still pending; leave it for full-parse diagnostics or
2922            // completion rather than routing to a synthetic child.
2923        } else if let Some(subcommand) = out
2924            .cmd
2925            .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2926        {
2927            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2928                bail!(
2929                    "subcommand '{}' cannot be used with arguments on its parent command",
2930                    subcommand.name
2931                );
2932            }
2933            let mut subcommand = subcommand.clone();
2934            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2935            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2936            out.merge_command_flags(&subcommand, crossing_mount);
2937            out.cmds.push(subcommand.clone());
2938            out.cmd = subcommand;
2939            command_has_argv = false;
2940            prefix_flags.clear();
2941            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2942        }
2943    }
2944
2945    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2946    if validate_clauses {
2947        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2948    }
2949    let clause_flag_names = out
2950        .cmd
2951        .clause
2952        .iter()
2953        .flat_map(|clause| &clause.flags)
2954        .map(|flag| flag.name.clone())
2955        .collect::<HashSet<_>>();
2956
2957    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2958    // declarations with the same canonical name therefore share one public value entry even
2959    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2960    // both, which is what exclusivity needs.
2961    let flag_was_parsed =
2962        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2963
2964    // The spellings the selected command's own declaration speaks for, on this object.
2965    //
2966    // Empty unless that declaration really is this object's: a parent and child may each
2967    // declare `--clean` without merging, leaving two flags that share a name, and the
2968    // ancestor's must not be read as the child's. The test is whether every spelling the child
2969    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2970    // an ancestor whose long form the child took over.
2971    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2972        let declared: HashSet<String> = out
2973            .cmd
2974            .flags
2975            .iter()
2976            .filter(|declared| declared.name == flag.name)
2977            .flat_map(flag_keys)
2978            .collect();
2979        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2980        // some other inherited global already owns — `-c --clean` beside an inherited
2981        // `-c --config` — and that collision is resolved in the other global's favor, so the
2982        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2983        // unrelated collision disown the child from the `--clean` it plainly does own.
2984        //
2985        // Still enough to tell the two-object case apart, which is what this guards: when a
2986        // child re-declares a global as global, the child's own spellings resolve to the
2987        // child's separate flag, so none of them lands on the ancestor's.
2988        let speaks_for_this_flag = declared.iter().any(|spelling| {
2989            out.available_flags
2990                .get(spelling)
2991                .is_some_and(|available| Arc::ptr_eq(available, flag))
2992        });
2993        if speaks_for_this_flag {
2994            declared
2995        } else {
2996            HashSet::new()
2997        }
2998    };
2999
3000    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
3001    //
3002    // A child that re-declares an inherited global merges into one object answering to two
3003    // alias sets whose declarations may disagree, so there is no single owner to name: the
3004    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
3005    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
3006    // child's in one invocation — and each carries its own declaration's answer.
3007    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
3008        let child = child_spellings(flag);
3009        let child_exclusive = !child.is_empty()
3010            && out
3011                .cmd
3012                .flags
3013                .iter()
3014                .any(|declared| declared.name == flag.name && declared.exclusive);
3015        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
3016            Some(spellings) => (
3017                child_exclusive && spellings.iter().any(|s| child.contains(s)),
3018                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
3019            ),
3020            // An environment value has no spelling to attribute it by. The declaration the
3021            // selected command has in scope is the one that answers — which is the child's
3022            // when it re-declared the flag, and the ancestor's when it did not.
3023            None => (child_exclusive, flag.exclusive && child.is_empty()),
3024        }
3025    };
3026
3027    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
3028        let (child, ancestor) = exclusivity_in_play(flag);
3029        child || ancestor
3030    };
3031
3032    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
3033    // command that otherwise needs an input. Companions are still diagnosed below, but an
3034    // exclusive occurrence suppresses the missing-value checks that would make it unusable
3035    // whether it was alone or not.
3036    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3037        .filter(|flag| !clause_flag_names.contains(&flag.name))
3038        .any(|flag| {
3039            exclusive_occurrence(flag)
3040                && !overridden_flags.contains(&flag.name)
3041                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
3042        });
3043    let requirements_apply = |command_index: usize| {
3044        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
3045    };
3046
3047    if out.cmd.arg_required_else_help && !command_has_argv {
3048        out.errors
3049            .push(render_help_err(spec, &out.cmd, false, custom_env));
3050    }
3051
3052    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
3053    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
3054    // the child would be here instead. The spec has carried `subcommand_required` since it was
3055    // added for the derive, and this parser never read it — so `mise generate` parsed as a
3056    // complete invocation while usage-argv and clap both refused it.
3057    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
3058        let mut names: Vec<&str> = out
3059            .cmd
3060            .subcommands
3061            .iter()
3062            // Aliases share a map entry with the name they point at; listing both would offer
3063            // the same command twice under two spellings.
3064            .filter(|(name, sub)| sub.name == **name && !sub.hide)
3065            .map(|(name, _)| name.as_str())
3066            .collect();
3067        names.sort_unstable();
3068        out.errors.push(UsageErr::MissingSubcommand(
3069            out.cmd.name.clone(),
3070            names.join(", "),
3071        ));
3072    }
3073
3074    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
3075    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
3076    if !exclusive_present {
3077        for arg in out
3078            .cmds
3079            .iter()
3080            .enumerate()
3081            .filter(|(index, _)| requirements_apply(*index))
3082            .flat_map(|(_, cmd)| &cmd.args)
3083        {
3084            if out.args.contains_key(arg) {
3085                continue;
3086            }
3087            // Already reported as needing a `--`; one mistake should not yield two messages.
3088            if double_dash_violations.contains(&arg.name) {
3089                continue;
3090            }
3091            let required_if = arg.required_if.iter().any(|selector| {
3092                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3093            });
3094            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
3095                selector_explicit_has_value(
3096                    &condition.selector,
3097                    &condition.value,
3098                    &out,
3099                    &overridden_flags,
3100                    custom_env,
3101                )
3102            });
3103            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3104                && arg.required_if_eq_all.iter().all(|condition| {
3105                    selector_explicit_has_value(
3106                        &condition.selector,
3107                        &condition.value,
3108                        &out,
3109                        &overridden_flags,
3110                        custom_env,
3111                    )
3112                });
3113            let unless_any = arg.required_unless.iter().any(|selector| {
3114                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3115            });
3116            let unless_all = !arg.required_unless_all.is_empty()
3117                && arg.required_unless_all.iter().all(|selector| {
3118                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3119                });
3120            let required_unless = !(unless_any
3121                || unless_all
3122                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3123            if (arg.required
3124                || required_if
3125                || required_if_eq
3126                || required_if_eq_all
3127                || required_unless)
3128                && arg.default.is_empty()
3129            {
3130                // Check if there's an env var available (custom env map takes precedence)
3131                let has_env = arg
3132                    .env
3133                    .as_ref()
3134                    .is_some_and(|env_var| env_contains(custom_env, env_var));
3135                if !has_env {
3136                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3137                }
3138            }
3139        }
3140    }
3141
3142    // Conflicts are a question about the invocation as a whole rather than about any one
3143    // token, so they are checked here beside the requirement checks rather than at the
3144    // point a flag is matched — the flag it conflicts with may still be ahead of it.
3145    // Its own loop: the requirement loop below skips the flags that *were* given, which
3146    // is exactly the set this needs.
3147    //
3148    // A value from the environment counts on both sides, matching what
3149    // `selector_is_explicit` says about the other flag: the question is whether a flag
3150    // has a value, not how it got one. That is what clap does, and an asymmetric rule
3151    // would make the same pair of flags a conflict or not depending on which one
3152    // happened to be typed.
3153    for flag in unique_flags(out.available_flags.values())
3154        .filter(|flag| !clause_flag_names.contains(&flag.name))
3155    {
3156        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3157        if !given || overridden_flags.contains(&flag.name) {
3158            continue;
3159        }
3160        for other in &flag.conflicts {
3161            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3162                out.errors.push(UsageErr::InvalidFlag {
3163                    token: format!("--{}", flag.name),
3164                    reason: format!("conflicts with {other}"),
3165                    span: (0, 0).into(),
3166                    input: format!("--{} {other}", flag.name),
3167                });
3168            }
3169        }
3170        // The positive form, checked in the same pass and under the same rule: a value
3171        // from the environment satisfies a requirement, because the question is whether
3172        // the other flag has a value rather than how it got one. A flag that was
3173        // overridden away has not been given, so it cannot satisfy anything either —
3174        // which is what `selector_is_explicit` already accounts for.
3175        //
3176        // Reported as the missing flag rather than as something wrong with the flag that
3177        // named it, which is what clap says too: an unmet `requires` is a required
3178        // argument that was not provided. Named by its own name, resolved through the
3179        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
3180        let owner = out
3181            .cmds
3182            .iter()
3183            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3184            .unwrap_or(out.cmds.len() - 1);
3185        if !exclusive_present && requirements_apply(owner) {
3186            for other in &flag.requires {
3187                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3188                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3189                    if other.starts_with('-') {
3190                        out.errors.push(UsageErr::MissingFlag(name));
3191                    } else {
3192                        out.errors.push(UsageErr::MissingArg(name));
3193                    }
3194                }
3195            }
3196            for condition in &flag.requires_if {
3197                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3198                    && !selector_is_satisfied(
3199                        &condition.requires,
3200                        &out,
3201                        &overridden_flags,
3202                        custom_env,
3203                    )
3204                {
3205                    let name = selector_flag_name(&condition.requires, &out)
3206                        .unwrap_or_else(|| condition.requires.clone());
3207                    out.errors.push(UsageErr::MissingFlag(name));
3208                }
3209            }
3210        }
3211    }
3212
3213    // Positionals can declare the same pairwise conflict as flags. Their selector is
3214    // the bare argument name, while a flag keeps its dashed spelling.
3215    for (command_index, arg) in out
3216        .cmds
3217        .iter()
3218        .enumerate()
3219        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3220    {
3221        let given = arg_is_explicit(arg, &out, custom_env);
3222        if !given {
3223            continue;
3224        }
3225        for other in &arg.conflicts {
3226            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3227                out.errors.push(UsageErr::InvalidFlag {
3228                    token: arg.name.clone(),
3229                    reason: format!("conflicts with {other}"),
3230                    span: (0, 0).into(),
3231                    input: format!("{} {other}", arg.name),
3232                });
3233            }
3234        }
3235        if !exclusive_present && requirements_apply(command_index) {
3236            for other in &arg.requires {
3237                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3238                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3239                    if other.starts_with('-') {
3240                        out.errors.push(UsageErr::MissingFlag(name));
3241                    } else {
3242                        out.errors.push(UsageErr::MissingArg(name));
3243                    }
3244                }
3245            }
3246        }
3247    }
3248
3249    // An exclusive flag is the whole-command form of a conflict: `--version` means the
3250    // rest of the line has nothing to act on. Everything the invocation supplied counts,
3251    // positionals included, which is what distinguishes it from being in a group with
3252    // every other flag.
3253    //
3254    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
3255    // exclusive one is nobody saying anything, and counting it would make the exclusive
3256    // flag unusable on any command that has a default. Environment values do count, also as
3257    // `conflicts` reads them, so the spec parser and the derive agree.
3258    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3259        .filter(|flag| !clause_flag_names.contains(&flag.name))
3260    {
3261        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
3262        // but re-declared aliases can leave distinct declarations with that same name in
3263        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
3264        // compare the parser's `Arc`s by identity here.
3265        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3266        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3267            continue;
3268        }
3269        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3270            .filter(|other| !clause_flag_names.contains(&other.name))
3271            .find(|other| {
3272                !Arc::ptr_eq(other, flag)
3273                    && !overridden_flags.contains(&other.name)
3274                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3275            })
3276            .map(|other| format!("--{}", other.name));
3277        let other_arg = active_args(&out.cmd).iter().find(|arg| {
3278            out.args.keys().any(|given| given.name == arg.name)
3279                || out
3280                    .clauses
3281                    .values()
3282                    .flatten()
3283                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
3284                || arg
3285                    .env
3286                    .as_ref()
3287                    .is_some_and(|env| env_contains(custom_env, env))
3288        });
3289        // Selecting a child is company for an exclusive flag declared by an ancestor. An
3290        // exclusive flag belonging to the child itself does not conflict with the command word
3291        // needed to reach that child — so the question is not who owns the flag but whose
3292        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
3293        // separated.
3294        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3295        let selected_subcommand =
3296            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3297        let other = other_flag
3298            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3299            .or(selected_subcommand);
3300        if let Some(other) = other {
3301            out.errors.push(UsageErr::InvalidFlag {
3302                token: format!("--{}", flag.name),
3303                reason: format!("must be given on its own, and {other} was given too"),
3304                span: (0, 0).into(),
3305                input: format!("--{} {other}", flag.name),
3306            });
3307        }
3308    }
3309
3310    // Groups, checked once per group rather than per flag: both questions a group asks —
3311    // how many members were given, and whether that is enough — are about the set, which
3312    // is the whole reason a group exists rather than a pile of pairwise conflicts.
3313    //
3314    // The same "given" rule as everything else here, so a member filled from the
3315    // environment or a default counts.
3316    // Every command in the chain, not only the selected one: a group may name global
3317    // flags, which belong to an ancestor and are declared there.
3318    let mut group_errors: Vec<UsageErr> = Vec::new();
3319    for (command_index, group) in out
3320        .cmds
3321        .iter()
3322        .enumerate()
3323        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3324    {
3325        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
3326        // `--file` are two spellings of one flag, and a group naming both — or naming one
3327        // flag twice — would otherwise report that flag as conflicting with itself the
3328        // moment it was given. Deduplicated rather than refused where the group is
3329        // written, because listing both spellings is redundant, not wrong.
3330        let mut given: Vec<&str> = Vec::new();
3331        let mut seen: Vec<String> = Vec::new();
3332        for selector in &group.members {
3333            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3334                continue;
3335            }
3336            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3337            if seen.contains(&name) {
3338                continue;
3339            }
3340            seen.push(name);
3341            given.push(selector.as_str());
3342        }
3343        if !group.multiple && given.len() > 1 {
3344            group_errors.push(UsageErr::InvalidFlag {
3345                token: given[1].to_string(),
3346                reason: format!("cannot be used with {} in group {}", given[0], group.name),
3347                span: (0, 0).into(),
3348                input: format!("{} {}", given[0], given[1]),
3349            });
3350        }
3351        // Requiredness is a *positive* rule, so it reads a default as filling a member —
3352        // the rule `requires` follows. That is also why it cannot reuse `given` above:
3353        // exclusivity must count only what was supplied, or a defaulted member would
3354        // collide with the sibling the user actually typed.
3355        let satisfied = group
3356            .members
3357            .iter()
3358            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3359        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3360            // The members are what a user has to type, so they are in the message; the
3361            // group's name is there too, since a command with several groups would
3362            // otherwise report the same sentence twice with nothing to tell them apart.
3363            group_errors.push(UsageErr::MissingGroup {
3364                group: group.name.clone(),
3365                members: group.members.join(", "),
3366            });
3367        }
3368    }
3369    out.errors.extend(group_errors);
3370
3371    if !exclusive_present {
3372        for flag in unique_flags(out.available_flags.values())
3373            .filter(|flag| !clause_flag_names.contains(&flag.name))
3374        {
3375            let owner = out
3376                .cmds
3377                .iter()
3378                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3379                .unwrap_or(out.cmds.len() - 1);
3380            if !requirements_apply(owner) {
3381                continue;
3382            }
3383            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3384                continue;
3385            }
3386            let has_default =
3387                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3388            let has_env = flag_has_env(flag, custom_env);
3389            let required_if = flag.required_if.iter().any(|selector| {
3390                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3391            });
3392            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3393                selector_explicit_has_value(
3394                    &condition.selector,
3395                    &condition.value,
3396                    &out,
3397                    &overridden_flags,
3398                    custom_env,
3399                )
3400            });
3401            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3402                && flag.required_if_eq_all.iter().all(|condition| {
3403                    selector_explicit_has_value(
3404                        &condition.selector,
3405                        &condition.value,
3406                        &out,
3407                        &overridden_flags,
3408                        custom_env,
3409                    )
3410                });
3411            let unless_any = flag.required_unless.iter().any(|selector| {
3412                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3413            });
3414            let unless_all = !flag.required_unless_all.is_empty()
3415                && flag.required_unless_all.iter().all(|selector| {
3416                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3417                });
3418            let required_unless = !(unless_any
3419                || unless_all
3420                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3421            if (flag.required
3422                || required_if
3423                || required_if_eq
3424                || required_if_eq_all
3425                || required_unless)
3426                && !has_default
3427                && !has_env
3428            {
3429                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3430            }
3431        }
3432    }
3433
3434    // Validate var_min/var_max constraints for variadic args
3435    for (arg, value) in &out.args {
3436        if arg.var
3437            && let ParseValue::MultiString(values) = value
3438        {
3439            if let Some(min) = arg.var_min
3440                && values.len() < min
3441            {
3442                out.errors.push(UsageErr::VarArgTooFew {
3443                    name: arg.name.clone(),
3444                    min,
3445                    got: values.len(),
3446                });
3447            }
3448            if let Some(max) = arg.var_max
3449                && values.len() > max
3450            {
3451                out.errors.push(UsageErr::VarArgTooMany {
3452                    name: arg.name.clone(),
3453                    max,
3454                    got: values.len(),
3455                });
3456            }
3457        }
3458    }
3459
3460    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3461    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3462    // by binding once per occurrence, where the per-occurrence count is still available.
3463    for flag in unique_flags(out.available_flags.values())
3464        .filter(|flag| !clause_flag_names.contains(&flag.name))
3465    {
3466        if flag.var {
3467            let bound = match out.flags.get(flag) {
3468                Some(ParseValue::MultiString(values)) => values.len(),
3469                Some(ParseValue::MultiBool(values)) => values.len(),
3470                Some(_) => 1,
3471                None => 0,
3472            };
3473            // A partial parse deliberately leaves the final value-optional flag pending so
3474            // completion can still answer for it. It is nevertheless a real occurrence for
3475            // the repeated flag's bounds; the full parser closes it just after this phase.
3476            let pending = out
3477                .flag_awaiting_value
3478                .iter()
3479                .filter(|pending| {
3480                    Arc::ptr_eq(pending, flag)
3481                        && (pending.value_optional || pending.default_missing.is_some())
3482                })
3483                .count();
3484            let count = bound + pending;
3485            if count == 0 {
3486                continue;
3487            }
3488            if let Some(min) = flag.var_min
3489                && count < min
3490            {
3491                out.errors.push(UsageErr::VarFlagTooFew {
3492                    name: flag.name.clone(),
3493                    min,
3494                    got: count,
3495                });
3496            }
3497            if let Some(max) = flag.var_max
3498                && count > max
3499            {
3500                out.errors.push(UsageErr::VarFlagTooMany {
3501                    name: flag.name.clone(),
3502                    max,
3503                    got: count,
3504                });
3505            }
3506        }
3507    }
3508
3509    Ok((out, overridden_flags))
3510}
3511
3512fn validate_expression(
3513    name: &str,
3514    expression: Option<&str>,
3515    message: Option<&str>,
3516    parsed: &ParseValue,
3517    errors: &mut Vec<UsageErr>,
3518) {
3519    let Some(expression) = expression else {
3520        return;
3521    };
3522    #[cfg(not(feature = "validation"))]
3523    let _ = expression;
3524    let values: &[String] = match parsed {
3525        ParseValue::String(value) => std::slice::from_ref(value),
3526        ParseValue::MultiString(values) => values,
3527        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3528    };
3529    #[cfg(feature = "validation")]
3530    for value in values {
3531        let reason = match usage_validation::validate(expression, value) {
3532            Ok(true) => continue,
3533            Ok(false) => message
3534                .unwrap_or("does not satisfy the validation expression")
3535                .to_string(),
3536            Err(error) => format!("validation expression failed: {error}"),
3537        };
3538        errors.push(UsageErr::InvalidValue {
3539            name: name.to_string(),
3540            value: value.clone(),
3541            reason,
3542        });
3543        break;
3544    }
3545    #[cfg(not(feature = "validation"))]
3546    if let Some(value) = values.first() {
3547        let _ = message;
3548        errors.push(UsageErr::InvalidValue {
3549            name: name.to_string(),
3550            value: value.clone(),
3551            reason: "expression validation requires the `validation` feature".to_string(),
3552        });
3553    }
3554}
3555
3556#[cfg(all(test, not(feature = "validation")))]
3557mod optional_validation_tests {
3558    use crate::{Spec, parse};
3559
3560    #[test]
3561    fn validation_declarations_require_the_opt_in_runtime_feature() {
3562        let spec: Spec = r#"
3563name "ex"
3564bin "ex"
3565arg "<port>" validate="int(value) > 0"
3566        "#
3567        .parse()
3568        .unwrap();
3569        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3570        assert!(
3571            error
3572                .to_string()
3573                .contains("requires the `validation` feature"),
3574            "{error:?}"
3575        );
3576    }
3577}
3578
3579fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3580    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3581}
3582
3583fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3584    overrider
3585        .overrides
3586        .iter()
3587        .any(|selector| flag_matches_selector(overridden, selector))
3588}
3589
3590fn apply_prefix_flag_overrides(
3591    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3592    flag: Arc<SpecFlag>,
3593) {
3594    prefix_flags
3595        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3596}
3597
3598fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3599    prefix_flags
3600        .iter()
3601        .flat_map(|(_, words)| words.iter().cloned())
3602        .collect()
3603}
3604
3605fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3606    match custom_env {
3607        Some(env) => env.contains_key(env_var),
3608        None => std::env::var(env_var).is_ok(),
3609    }
3610}
3611
3612fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3613    flag.env_names()
3614        .any(|env_var| env_contains(custom_env, env_var))
3615}
3616
3617fn fallback_is_true(value: &str) -> bool {
3618    matches!(value, "1" | "true" | "True" | "TRUE")
3619}
3620
3621fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3622    match delimiter {
3623        Some(delimiter) => values
3624            .iter()
3625            .flat_map(|value| value.split(delimiter).map(str::to_string))
3626            .collect(),
3627        None => values.to_vec(),
3628    }
3629}
3630
3631fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3632    if let Some(min) = arg.var_min
3633        && count < min
3634    {
3635        errors.push(UsageErr::VarArgTooFew {
3636            name: arg.name.clone(),
3637            min,
3638            got: count,
3639        });
3640    }
3641    if let Some(max) = arg.var_max
3642        && count > max
3643    {
3644        errors.push(UsageErr::VarArgTooMany {
3645            name: arg.name.clone(),
3646            max,
3647            got: count,
3648        });
3649    }
3650}
3651
3652fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3653    if let Some(min) = flag.var_min
3654        && count < min
3655    {
3656        errors.push(UsageErr::VarFlagTooFew {
3657            name: flag.name.clone(),
3658            min,
3659            got: count,
3660        });
3661    }
3662    if let Some(max) = flag.var_max
3663        && count > max
3664    {
3665        errors.push(UsageErr::VarFlagTooMany {
3666            name: flag.name.clone(),
3667            max,
3668            got: count,
3669        });
3670    }
3671}
3672
3673fn validate_flag_arg_fallback_count(
3674    flag: &SpecFlag,
3675    arg: &SpecArg,
3676    count: usize,
3677    errors: &mut Vec<UsageErr>,
3678) {
3679    if let Some(min) = arg.var_min
3680        && count < min
3681    {
3682        errors.push(UsageErr::VarFlagTooFew {
3683            name: flag.name.clone(),
3684            min,
3685            got: count,
3686        });
3687    }
3688    if let Some(max) = arg.var_max
3689        && count > max
3690    {
3691        errors.push(UsageErr::VarFlagTooMany {
3692            name: flag.name.clone(),
3693            max,
3694            got: count,
3695        });
3696    }
3697}
3698
3699/// Bind a fallback the way an unconditional `default` does: one value, or several
3700/// for `var`, and choices checked the same way.
3701fn bind_flag_fallback(
3702    flag: &Arc<SpecFlag>,
3703    values: &[String],
3704    out: &mut ParseOutput,
3705    custom_env: Option<&HashMap<String, String>>,
3706    origin: ValueOrigin,
3707) -> Result<(), miette::Error> {
3708    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3709        return Ok(());
3710    };
3711    out.flags.insert(Arc::clone(flag), value);
3712    out.flag_origins
3713        .entry(Arc::clone(flag))
3714        .or_default()
3715        .push(origin);
3716    Ok(())
3717}
3718
3719fn flag_fallback_value(
3720    flag: &SpecFlag,
3721    values: &[String],
3722    errors: &mut Vec<UsageErr>,
3723    custom_env: Option<&HashMap<String, String>>,
3724) -> Result<Option<ParseValue>, miette::Error> {
3725    if values.is_empty() {
3726        return Ok(None);
3727    }
3728    if let Some(arg) = flag.arg.as_ref() {
3729        let values = split_fallback_values(values, arg.delimiter);
3730        if flag.var || arg.var {
3731            if flag.var {
3732                validate_flag_fallback_count(flag, values.len(), errors);
3733            }
3734            if arg.var {
3735                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3736            }
3737            validate_choice_values(
3738                ChoiceTarget::option(flag),
3739                &values,
3740                arg.choices.as_ref(),
3741                custom_env,
3742            )?;
3743            Ok(Some(ParseValue::MultiString(values)))
3744        } else {
3745            let value = values.into_iter().next().unwrap_or_default();
3746            validate_choice_value(
3747                ChoiceTarget::option(flag),
3748                &value,
3749                arg.choices.as_ref(),
3750                custom_env,
3751            )?;
3752            Ok(Some(ParseValue::String(value)))
3753        }
3754    } else if flag.var {
3755        validate_flag_fallback_count(flag, values.len(), errors);
3756        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3757        Ok(Some(ParseValue::MultiBool(bools)))
3758    } else {
3759        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3760    }
3761}
3762
3763/// Fill scoped flags inside the clause instances argv created.
3764///
3765/// The returned sets contain argv and environment sources only. Relationship
3766/// validation uses them to keep defaults from becoming explicit conflicts while
3767/// still allowing a fallback value to satisfy a required scoped flag.
3768fn apply_clause_flag_fallbacks(
3769    out: &mut ParseOutput,
3770    overridden_flags: &HashSet<String>,
3771    custom_env: Option<&HashMap<String, String>>,
3772) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3773    let Some(clause) = out.cmd.clause.clone() else {
3774        return Ok(None);
3775    };
3776    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3777        return Ok(None);
3778    };
3779    let positional_instances = positional_instances.clone();
3780    let mut flag_instances = out
3781        .clause_flags
3782        .shift_remove(&clause.name)
3783        .unwrap_or_default();
3784    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3785    let get_env = |key: &str| -> Option<String> {
3786        custom_env
3787            .and_then(|values| values.get(key).cloned())
3788            .or_else(|| {
3789                custom_env
3790                    .is_none()
3791                    .then(|| std::env::var(key).ok())
3792                    .flatten()
3793            })
3794    };
3795    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3796
3797    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3798        let positional = &positional_instances[instance_index];
3799        let mut explicit = scoped
3800            .keys()
3801            .map(|flag| flag.name.clone())
3802            .collect::<HashSet<_>>();
3803
3804        // Environment first so every default_if sees all explicit sources,
3805        // independent of declaration order.
3806        for declared in &clause.flags {
3807            let flag = Arc::new(declared.clone());
3808            if scoped.contains_key(&flag) {
3809                continue;
3810            }
3811            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3812                continue;
3813            };
3814            if let Some(warning) = flag_deprecation(declared) {
3815                out.warnings.push(warning);
3816            }
3817            if flag_env_is_deprecated(declared, env_name) {
3818                out.warnings
3819                    .push(Warning::env(env_name, flag_current_env(declared)));
3820            }
3821            if let Some(value) = flag_fallback_value(
3822                declared,
3823                std::slice::from_ref(&env_value),
3824                &mut out.errors,
3825                custom_env,
3826            )? {
3827                scoped.insert(flag, value);
3828                explicit.insert(declared.name.clone());
3829            }
3830        }
3831
3832        let condition_matches =
3833            |condition: &crate::SpecDefaultIf| {
3834                if let Some(flag) = clause
3835                    .flags
3836                    .iter()
3837                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3838                {
3839                    if !explicit.contains(&flag.name) {
3840                        return false;
3841                    }
3842                    return condition.when.as_ref().is_none_or(|expected| {
3843                        scoped
3844                            .iter()
3845                            .find(|(present, _)| present.name == flag.name)
3846                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3847                    });
3848                }
3849                if let Some(arg) = clause.args.iter().find(|arg| {
3850                    !condition.selector.starts_with('-') && arg.name == condition.selector
3851                }) {
3852                    return positional.get(arg).is_some_and(|value| {
3853                        condition
3854                            .when
3855                            .as_ref()
3856                            .is_none_or(|expected| parse_value_has(value, expected))
3857                    });
3858                }
3859                let command_flag = out
3860                    .available_flags
3861                    .values()
3862                    .chain(out.flags.keys())
3863                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3864                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3865                command_flag.is_some_and(|flag| {
3866                    !overridden_flags.contains(&flag.name)
3867                        && command_flag_has_explicit_source(flag, out)
3868                        && condition.when.as_ref().is_none_or(|expected| {
3869                            out.flags
3870                                .get(flag)
3871                                .is_some_and(|value| parse_value_has(value, expected))
3872                        })
3873                })
3874            };
3875
3876        let conditional = clause
3877            .flags
3878            .iter()
3879            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3880            .filter_map(|flag| {
3881                flag.default_if
3882                    .iter()
3883                    .find(|condition| condition_matches(condition))
3884                    .map(|condition| (flag.clone(), condition.value.clone()))
3885            })
3886            .collect::<Vec<_>>();
3887        for (declared, value) in conditional {
3888            if let Some(value) = flag_fallback_value(
3889                &declared,
3890                std::slice::from_ref(&value),
3891                &mut out.errors,
3892                custom_env,
3893            )? {
3894                scoped.insert(Arc::new(declared), value);
3895            }
3896        }
3897
3898        for declared in &clause.flags {
3899            let flag = Arc::new(declared.clone());
3900            if scoped.contains_key(&flag) {
3901                continue;
3902            }
3903            let values = if !declared.default.is_empty() {
3904                &declared.default
3905            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3906                &arg.default
3907            } else {
3908                continue;
3909            };
3910            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3911            {
3912                scoped.insert(flag, value);
3913            }
3914        }
3915        explicit_instances.push(explicit);
3916    }
3917
3918    out.clause_flags.insert(clause.name, flag_instances);
3919    Ok(Some(explicit_instances))
3920}
3921
3922fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3923    out.flags.contains_key(flag)
3924        && out.flag_origins.get(flag).is_none_or(|origins| {
3925            origins
3926                .iter()
3927                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3928        })
3929}
3930
3931fn default_if_condition_matches(
3932    condition: &crate::SpecDefaultIf,
3933    out: &ParseOutput,
3934    overridden_flags: &HashSet<String>,
3935    custom_env: Option<&HashMap<String, String>>,
3936) -> bool {
3937    match &condition.when {
3938        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3939        Some(when) => {
3940            let Some(flag) = out
3941                .available_flags
3942                .values()
3943                .chain(out.flags.keys())
3944                .find(|flag| flag_matches_selector(flag, &condition.selector))
3945            else {
3946                return false;
3947            };
3948            if overridden_flags.contains(&flag.name) {
3949                return false;
3950            }
3951            explicit_flag_has_value(flag, when, out, custom_env)
3952        }
3953    }
3954}
3955
3956/// Whether an explicitly supplied value of `flag` equals `expected`.
3957///
3958/// clap treats command-line and environment values as explicit for `requires_if`, but
3959/// not defaults. Keep that source distinction here instead of consulting the flag's
3960/// defaults through `selector_is_satisfied`.
3961fn explicit_flag_has_value(
3962    flag: &SpecFlag,
3963    expected: &str,
3964    out: &ParseOutput,
3965    custom_env: Option<&HashMap<String, String>>,
3966) -> bool {
3967    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3968        ParseValue::Bool(value) => value.to_string() == expected,
3969        ParseValue::String(value) => value == expected,
3970        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3971        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3972    });
3973    if out.flags.contains_key(flag) {
3974        return parsed_matches;
3975    }
3976
3977    let value = flag.env_names().find_map(|env| match custom_env {
3978        Some(values) => values.get(env).cloned(),
3979        None => std::env::var(env).ok(),
3980    });
3981    value.is_some_and(
3982        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3983            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3984            None if flag.arg.is_none() => {
3985                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3986            }
3987            None => value == expected,
3988        },
3989    )
3990}
3991
3992fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3993    match value {
3994        ParseValue::Bool(value) => value.to_string() == expected,
3995        ParseValue::String(value) => value == expected,
3996        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3997        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3998    }
3999}
4000
4001/// Clause flags share the command's spelling table so they can be recognized while parsing,
4002/// but their values and validation belong to one clause instance rather than the command.
4003fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
4004    out.cmd
4005        .clause
4006        .as_ref()
4007        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
4008}
4009
4010fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
4011    out.cmd
4012        .clause
4013        .as_ref()?
4014        .flags
4015        .iter()
4016        .find(|flag| flag_matches_selector(flag, selector))
4017}
4018
4019fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
4020    out.clause_flags
4021        .values()
4022        .flatten()
4023        .any(|instance| instance.keys().any(|present| present.name == flag.name))
4024        || out.flags.keys().any(|present| present.name == flag.name)
4025}
4026
4027fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
4028    out.clause_flags.values().flatten().any(|instance| {
4029        instance
4030            .iter()
4031            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
4032    }) || out
4033        .flags
4034        .iter()
4035        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
4036}
4037
4038fn validate_clause_relationships(
4039    out: &mut ParseOutput,
4040    overridden_flags: &HashSet<String>,
4041    custom_env: Option<&HashMap<String, String>>,
4042    explicit_instances: Option<&[HashSet<String>]>,
4043) {
4044    let Some(clause) = out.cmd.clause.as_ref() else {
4045        return;
4046    };
4047    let Some(instances) = out.clauses.get(&clause.name) else {
4048        return;
4049    };
4050    let command_flag_is_explicit = |selector: &str| {
4051        out.available_flags
4052            .values()
4053            .chain(out.flags.keys())
4054            .filter(|flag| !is_clause_scoped_flag(out, flag))
4055            .any(|flag| {
4056                flag_matches_selector(flag, selector)
4057                    && !overridden_flags.contains(&flag.name)
4058                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4059            })
4060    };
4061    let command_flag_matches_value = |selector: &str, expected: &str| {
4062        out.available_flags
4063            .values()
4064            .chain(out.flags.keys())
4065            .filter(|flag| !is_clause_scoped_flag(out, flag))
4066            .find(|flag| flag_matches_selector(flag, selector))
4067            .is_some_and(|flag| {
4068                !overridden_flags.contains(&flag.name)
4069                    && explicit_flag_has_value(flag, expected, out, custom_env)
4070            })
4071    };
4072    let command_flag_is_satisfied = |selector: &str| {
4073        out.available_flags
4074            .values()
4075            .chain(out.flags.keys())
4076            .filter(|flag| !is_clause_scoped_flag(out, flag))
4077            .any(|flag| flag_matches_selector(flag, selector))
4078            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
4079    };
4080    let mut errors = Vec::new();
4081    for (instance_index, instance) in instances.iter().enumerate() {
4082        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
4083        let scoped = out
4084            .clause_flags
4085            .get(&clause.name)
4086            .and_then(|instances| instances.get(instance_index));
4087        let scoped_flag = |selector: &str| {
4088            clause
4089                .flags
4090                .iter()
4091                .find(|flag| flag_matches_selector(flag, selector))
4092        };
4093        let scoped_value = |flag: &SpecFlag| {
4094            scoped.and_then(|values| {
4095                values
4096                    .iter()
4097                    .find(|(present, _)| present.name == flag.name)
4098                    .map(|(_, value)| value)
4099            })
4100        };
4101        let arg_is_explicit = |selector: &str| {
4102            instance
4103                .keys()
4104                .any(|arg| !selector.starts_with('-') && arg.name == selector)
4105        };
4106        let selector_is_explicit = |selector: &str| {
4107            scoped_flag(selector).is_some_and(|flag| {
4108                explicit
4109                    .map(|given| given.contains(&flag.name))
4110                    .unwrap_or_else(|| scoped_value(flag).is_some())
4111            }) || command_flag_is_explicit(selector)
4112                || arg_is_explicit(selector)
4113        };
4114        let selector_has_value = |selector: &str, expected: &str| {
4115            scoped_flag(selector)
4116                .filter(|flag| {
4117                    explicit
4118                        .map(|given| given.contains(&flag.name))
4119                        .unwrap_or_else(|| scoped_value(flag).is_some())
4120                })
4121                .and_then(scoped_value)
4122                .is_some_and(|value| parse_value_has(value, expected))
4123                || command_flag_matches_value(selector, expected)
4124                || instance.iter().any(|(arg, value)| {
4125                    !selector.starts_with('-')
4126                        && arg.name == selector
4127                        && parse_value_has(value, expected)
4128                })
4129        };
4130        let selector_is_satisfied = |selector: &str| {
4131            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4132                || command_flag_is_satisfied(selector)
4133                || arg_is_explicit(selector)
4134        };
4135        let selector_name = |selector: &str| {
4136            scoped_flag(selector)
4137                .map(|flag| flag.name.clone())
4138                .or_else(|| selector_flag_name(selector, out))
4139                .unwrap_or_else(|| selector.to_string())
4140        };
4141
4142        for flag in &clause.flags {
4143            let value = scoped_value(flag);
4144            if value.is_none() {
4145                let required_if = flag
4146                    .required_if
4147                    .iter()
4148                    .any(|selector| selector_is_explicit(selector));
4149                let required_if_eq = flag
4150                    .required_if_eq
4151                    .iter()
4152                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4153                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4154                    && flag
4155                        .required_if_eq_all
4156                        .iter()
4157                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4158                let unless_any = flag
4159                    .required_unless
4160                    .iter()
4161                    .any(|selector| selector_is_explicit(selector));
4162                let unless_all = !flag.required_unless_all.is_empty()
4163                    && flag
4164                        .required_unless_all
4165                        .iter()
4166                        .all(|selector| selector_is_explicit(selector));
4167                let required_unless = !(unless_any
4168                    || unless_all
4169                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4170                if flag.required
4171                    || required_if
4172                    || required_if_eq
4173                    || required_if_eq_all
4174                    || required_unless
4175                {
4176                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
4177                }
4178                continue;
4179            }
4180
4181            let explicitly_provided = explicit
4182                .map(|given| given.contains(&flag.name))
4183                .unwrap_or(true);
4184
4185            if explicitly_provided {
4186                for other in &flag.conflicts {
4187                    if selector_is_explicit(other) {
4188                        errors.push(UsageErr::InvalidFlag {
4189                            token: format!("--{}", flag.name),
4190                            reason: format!("conflicts with {other}"),
4191                            span: (0, 0).into(),
4192                            input: format!("--{} {other}", flag.name),
4193                        });
4194                    }
4195                }
4196                for other in &flag.requires {
4197                    if !selector_is_satisfied(other) {
4198                        errors.push(UsageErr::MissingFlag(selector_name(other)));
4199                    }
4200                }
4201                for condition in &flag.requires_if {
4202                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
4203                        && !selector_is_satisfied(&condition.requires)
4204                    {
4205                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4206                    }
4207                }
4208            }
4209            if let (true, Some(value)) = (flag.var, value) {
4210                let count = match value {
4211                    ParseValue::MultiString(values) => values.len(),
4212                    ParseValue::MultiBool(values) => values.len(),
4213                    _ => 1,
4214                };
4215                if let Some(min) = flag.var_min.filter(|min| count < *min) {
4216                    errors.push(UsageErr::VarFlagTooFew {
4217                        name: flag.name.clone(),
4218                        min,
4219                        got: count,
4220                    });
4221                }
4222                if let Some(max) = flag.var_max.filter(|max| count > *max) {
4223                    errors.push(UsageErr::VarFlagTooMany {
4224                        name: flag.name.clone(),
4225                        max,
4226                        got: count,
4227                    });
4228                }
4229            }
4230            if let (Some(arg), Some(value)) = (&flag.arg, value) {
4231                validate_expression(
4232                    &flag.name,
4233                    arg.validate.as_deref(),
4234                    arg.validate_error.as_deref(),
4235                    value,
4236                    &mut errors,
4237                );
4238            }
4239        }
4240        for arg in &clause.args {
4241            let given = instance.keys().any(|present| present.name == arg.name);
4242            if !given {
4243                let required_if = arg
4244                    .required_if
4245                    .iter()
4246                    .any(|selector| selector_is_explicit(selector));
4247                let required_if_eq = arg
4248                    .required_if_eq
4249                    .iter()
4250                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4251                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4252                    && arg
4253                        .required_if_eq_all
4254                        .iter()
4255                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4256                let unless_any = arg
4257                    .required_unless
4258                    .iter()
4259                    .any(|selector| selector_is_explicit(selector));
4260                let unless_all = !arg.required_unless_all.is_empty()
4261                    && arg
4262                        .required_unless_all
4263                        .iter()
4264                        .all(|selector| selector_is_explicit(selector));
4265                let required_unless = !(unless_any
4266                    || unless_all
4267                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4268                if required_if || required_if_eq || required_if_eq_all || required_unless {
4269                    errors.push(UsageErr::MissingClauseArg {
4270                        clause: clause.name.clone(),
4271                        instance: instance_index + 1,
4272                        arg: arg.name.clone(),
4273                    });
4274                }
4275                continue;
4276            }
4277            for other in &arg.conflicts {
4278                if selector_is_explicit(other) {
4279                    errors.push(UsageErr::InvalidFlag {
4280                        token: arg.name.clone(),
4281                        reason: format!("conflicts with {other}"),
4282                        span: (0, 0).into(),
4283                        input: format!("{} {other}", arg.name),
4284                    });
4285                }
4286            }
4287            for other in &arg.requires {
4288                if selector_is_satisfied(other) {
4289                    continue;
4290                }
4291                if other.starts_with('-') {
4292                    errors.push(UsageErr::MissingFlag(selector_name(other)));
4293                } else {
4294                    errors.push(UsageErr::MissingClauseArg {
4295                        clause: clause.name.clone(),
4296                        instance: instance_index + 1,
4297                        arg: other.clone(),
4298                    });
4299                }
4300            }
4301        }
4302    }
4303    out.errors.extend(errors);
4304}
4305
4306fn selector_explicit_has_value(
4307    selector: &str,
4308    expected: &str,
4309    out: &ParseOutput,
4310    overridden_flags: &HashSet<String>,
4311    custom_env: Option<&HashMap<String, String>>,
4312) -> bool {
4313    if let Some(flag) = selected_clause_flag(out, selector) {
4314        return clause_flag_has_value(out, flag, expected);
4315    }
4316    if let Some(flag) = out
4317        .available_flags
4318        .values()
4319        .chain(out.flags.keys())
4320        .filter(|flag| !is_clause_scoped_flag(out, flag))
4321        .find(|flag| flag_matches_selector(flag, selector))
4322    {
4323        return !overridden_flags.contains(&flag.name)
4324            && explicit_flag_has_value(flag, expected, out, custom_env);
4325    }
4326    let Some(arg) = selector_arg(selector, out) else {
4327        return false;
4328    };
4329    let parsed = out
4330        .args
4331        .iter()
4332        .find(|(given, _)| given.name == arg.name)
4333        .map(|(_, value)| value)
4334        .or_else(|| {
4335            out.clauses.values().flatten().find_map(|instance| {
4336                instance
4337                    .iter()
4338                    .find(|(given, _)| given.name == arg.name)
4339                    .map(|(_, value)| value)
4340            })
4341        });
4342    if let Some(value) = parsed {
4343        return match value {
4344            ParseValue::String(value) => value == expected,
4345            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4346            ParseValue::Bool(value) => value.to_string() == expected,
4347            ParseValue::MultiBool(values) => {
4348                values.iter().any(|value| value.to_string() == expected)
4349            }
4350        };
4351    }
4352    let value = arg.env_names().find_map(|env| match custom_env {
4353        Some(values) => values.get(env).cloned(),
4354        None => std::env::var(env).ok(),
4355    });
4356    value.is_some_and(|value| match arg.delimiter {
4357        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4358        None => value == expected,
4359    })
4360}
4361
4362fn selector_is_explicit(
4363    selector: &str,
4364    out: &ParseOutput,
4365    overridden_flags: &HashSet<String>,
4366    custom_env: Option<&HashMap<String, String>>,
4367) -> bool {
4368    let scoped_flag_is_explicit =
4369        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4370    let flag_is_explicit = out
4371        .available_flags
4372        .values()
4373        .chain(out.flags.keys())
4374        .filter(|flag| !is_clause_scoped_flag(out, flag))
4375        .any(|flag| {
4376            flag_matches_selector(flag, selector)
4377                && !overridden_flags.contains(&flag.name)
4378                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4379        });
4380    scoped_flag_is_explicit
4381        || flag_is_explicit
4382        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4383}
4384
4385/// The name of the flag a selector points at, for an error that has to name it.
4386///
4387/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
4388/// about a flag that is *missing* has to say which one, and the selector may be a short
4389/// form or an alias rather than the name.
4390fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4391    if let Some(flag) = selected_clause_flag(out, selector) {
4392        return Some(flag.name.clone());
4393    }
4394    out.available_flags
4395        .values()
4396        .chain(out.flags.keys())
4397        .filter(|flag| !is_clause_scoped_flag(out, flag))
4398        .find(|flag| flag_matches_selector(flag, selector))
4399        .map(|flag| flag.name.clone())
4400        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4401}
4402
4403/// Whether a selector's flag ended up with a value, however it got one.
4404///
4405/// The rule for a *positive* relationship, and the difference from
4406/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4407/// exclusivity — has to count only what was given, or a flag with a default would
4408/// conflict with everything and no command line would parse. A positive one asks whether
4409/// the flag it names has a value, and a default is a value: that is already how plain
4410/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4411/// otherwise would have made the same flag missing here and present ten lines below.
4412fn selector_is_satisfied(
4413    selector: &str,
4414    out: &ParseOutput,
4415    overridden_flags: &HashSet<String>,
4416    custom_env: Option<&HashMap<String, String>>,
4417) -> bool {
4418    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4419        return true;
4420    }
4421    let flag_is_satisfied = out
4422        .available_flags
4423        .values()
4424        .chain(out.flags.keys())
4425        .filter(|flag| !is_clause_scoped_flag(out, flag))
4426        .filter(|flag| flag_matches_selector(flag, selector))
4427        .any(|flag| {
4428            !overridden_flags.contains(&flag.name)
4429                && (!flag.default.is_empty()
4430                    || flag.arg.iter().any(|a| !a.default.is_empty())
4431                    || flag.default_if.iter().any(|condition| {
4432                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4433                    }))
4434        });
4435    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4436}
4437
4438fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4439    // Bare words are positional selectors. Keep accepting a flag's internal name above
4440    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4441    if selector.starts_with('-') {
4442        return None;
4443    }
4444    out.cmds
4445        .iter()
4446        .flat_map(active_args)
4447        .find(|arg| arg.name == selector)
4448}
4449
4450fn arg_is_explicit(
4451    arg: &SpecArg,
4452    out: &ParseOutput,
4453    custom_env: Option<&HashMap<String, String>>,
4454) -> bool {
4455    out.args.keys().any(|given| given.name == arg.name)
4456        || out
4457            .clauses
4458            .values()
4459            .flatten()
4460            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4461        || arg
4462            .env
4463            .as_ref()
4464            .is_some_and(|env| env_contains(custom_env, env))
4465}
4466
4467fn apply_flag_overrides(
4468    flag: &Arc<SpecFlag>,
4469    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4470    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4471    pending_flags: &mut Vec<Arc<SpecFlag>>,
4472    overridden_flags: &mut HashSet<String>,
4473    // The reportable half of the same fact: which flag did the overriding. The set above
4474    // only stops a default or an environment value restoring what was overridden, and
4475    // "`--quiet` is unset despite its default" has no answer without the name.
4476    attributed: &mut BTreeMap<String, String>,
4477) {
4478    let overridden_names: HashSet<String> = available_flags
4479        .values()
4480        .chain(parsed_flags.keys())
4481        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4482        .map(|other| other.name.clone())
4483        .collect();
4484
4485    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4486    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4487    for name in &overridden_names {
4488        attributed.insert(name.clone(), flag.name.clone());
4489    }
4490    overridden_flags.extend(overridden_names);
4491    // An explicit occurrence always restores this flag, including self-overrides.
4492    overridden_flags.remove(&flag.name);
4493    attributed.remove(&flag.name);
4494}
4495
4496/// The help a parse answers `--help` with. A parse is a program running, so its page lists
4497/// what `choices run=` prints, run against the environment the parse was given.
4498#[cfg(feature = "cli-help")]
4499fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool, env: Env<'_>) -> UsageErr {
4500    UsageErr::Help(docs::cli::render_runtime_help(
4501        spec,
4502        cmd,
4503        long,
4504        docs::cli::Style::PLAIN,
4505        env,
4506    ))
4507}
4508
4509#[cfg(feature = "cli-help")]
4510fn render_help_all_err(spec: &Spec, cmd: &SpecCommand, env: Env<'_>) -> UsageErr {
4511    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand, env: Env<'_>) {
4512        if !out.is_empty() {
4513            out.push('\n');
4514        }
4515        out.push_str(&docs::cli::render_runtime_help(
4516            spec,
4517            cmd,
4518            true,
4519            docs::cli::Style::PLAIN,
4520            env,
4521        ));
4522        let mut children: Vec<_> = cmd
4523            .subcommands
4524            .values()
4525            .filter(|child| !child.hide)
4526            .collect();
4527        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4528        for child in children {
4529            append(out, spec, child, env);
4530        }
4531    }
4532
4533    let mut out = String::new();
4534    append(&mut out, spec, cmd, env);
4535    UsageErr::Help(out)
4536}
4537
4538#[cfg(not(feature = "cli-help"))]
4539fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool, _env: Env<'_>) -> UsageErr {
4540    UsageErr::Help("help".to_string())
4541}
4542
4543#[cfg(not(feature = "cli-help"))]
4544fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand, _env: Env<'_>) -> UsageErr {
4545    UsageErr::Help("help".to_string())
4546}
4547
4548/// The version to answer with. `--version` prefers the long text and `-V` the concise
4549/// one, each falling back to the other when only one is declared.
4550fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4551    let value = if long {
4552        spec.long_version.as_ref().or(spec.version.as_ref())
4553    } else {
4554        spec.version.as_ref().or(spec.long_version.as_ref())
4555    };
4556    UsageErr::Version(value.cloned().unwrap_or_default())
4557}
4558
4559/// The environment a parse was given, which is also what a `choices run=` command runs with.
4560type Env<'a> = Option<&'a HashMap<String, String>>;
4561
4562fn render_action_err(
4563    spec: &Spec,
4564    cmd: &SpecCommand,
4565    flag: &SpecFlag,
4566    spelling: &str,
4567    env: Env<'_>,
4568) -> UsageErr {
4569    use crate::SpecFlagAction;
4570    match flag.action {
4571        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--"), env),
4572        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false, env),
4573        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true, env),
4574        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd, env),
4575        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4576        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4577    }
4578}
4579
4580/// Report a required flag value that was displaced by a later option.
4581fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4582    let token = flag
4583        .long
4584        .first()
4585        .map(|long| format!("--{long}"))
4586        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4587        .unwrap_or_else(|| flag.name.clone());
4588    UsageErr::InvalidFlag {
4589        token: token.clone(),
4590        reason: "requires an argument".to_string(),
4591        span: (0, 0).into(),
4592        input: format!("{token} {following}"),
4593    }
4594}
4595
4596#[derive(Copy, Clone)]
4597struct ChoiceTarget<'a> {
4598    kind: &'a str,
4599    name: &'a str,
4600}
4601
4602impl<'a> ChoiceTarget<'a> {
4603    fn arg(arg: &'a SpecArg) -> Self {
4604        Self {
4605            kind: "arg",
4606            name: &arg.name,
4607        }
4608    }
4609
4610    fn option(flag: &'a SpecFlag) -> Self {
4611        Self {
4612            kind: "option",
4613            name: &flag.name,
4614        }
4615    }
4616}
4617
4618/// Whether every letter of a short token names a flag in scope.
4619///
4620/// Scanning stops at the first letter whose flag takes a value, because everything
4621/// after it is that value rather than more letters.
4622fn short_bundle_is_known(
4623    spec: &Spec,
4624    cmds: &[SpecCommand],
4625    available: &BTreeMap<String, Arc<SpecFlag>>,
4626    token: &str,
4627) -> bool {
4628    for c in token.chars().skip(1) {
4629        match available.get(&format!("-{c}")) {
4630            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4631            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4632            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4633            // clap all answer it with help.
4634            None if supplies_short(spec, cmds, c) => {}
4635            None => return false,
4636            Some(f) if f.arg.is_some() => return true,
4637            Some(_) => {}
4638        }
4639    }
4640    true
4641}
4642
4643/// The response `-h` or `-V` produces where nothing declares that letter.
4644///
4645/// The letter form of the flags the parser supplies rather than a spec declaring them,
4646/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4647/// about here one letter at a time, because a bundle is read one letter at a time.
4648///
4649/// Always the short response: `-h` is short help however many letters share its token,
4650/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4651/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4652fn supplied_short(
4653    spec: &Spec,
4654    cmds: &[SpecCommand],
4655    letter: char,
4656    env: Env<'_>,
4657) -> Option<UsageErr> {
4658    let cmd = cmds.last()?;
4659    match letter {
4660        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false, env)),
4661        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4662        _ => None,
4663    }
4664}
4665
4666/// Whether [`supplied_short`] would answer for `letter`, without drawing the page it would
4667/// answer with — which can mean running `choices run=` commands.
4668fn supplies_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> bool {
4669    cmds.last().is_some_and(|cmd| match letter {
4670        'h' => is_help_arg(spec, cmd, "-h"),
4671        'V' => is_version_arg(spec, cmds, "-V"),
4672        _ => false,
4673    })
4674}
4675
4676/// Refuse a flag-like token that named nothing, if this command asked for that.
4677///
4678/// Called from the flag branches, where the lookup has just failed and nothing from
4679/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4680/// than after setting `-a`.
4681fn reject_unknown_flag_if_asked(
4682    spec: &Spec,
4683    path: &[SpecCommand],
4684    token: &str,
4685) -> Result<(), UsageErr> {
4686    // A lone `-` is a value by convention. A negative number reaches this only
4687    // when no pending value opted into the narrower exception.
4688    if !is_flag_like(token) {
4689        return Ok(());
4690    }
4691    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4692        return Ok(());
4693    }
4694    Err(UsageErr::InvalidFlag {
4695        token: token.to_string(),
4696        reason: "no such flag".to_string(),
4697        span: (0, 0).into(),
4698        input: token.to_string(),
4699    })
4700}
4701
4702/// Whether a flag-like token that matches nothing is a value or an error, here.
4703///
4704/// The nearest enclosing command that stated a preference wins, then the spec,
4705/// then the default. Inherited, unlike `effect`: it describes how a command line
4706/// is read, and a CLI that forwards options tends to forward them at every level.
4707fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4708    path.iter()
4709        .rev()
4710        .find_map(|cmd| cmd.unknown_flags)
4711        .or(spec.unknown_flags)
4712        .unwrap_or_default()
4713}
4714
4715/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4716/// ones.
4717///
4718/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4719/// a field may make the narrower negative-number exception.
4720fn is_flag_like(token: &str) -> bool {
4721    match token.strip_prefix('-') {
4722        None | Some("") => false,
4723        Some(_) => true,
4724    }
4725}
4726
4727fn is_negative_number(token: &str) -> bool {
4728    token.strip_prefix('-').is_some_and(is_number)
4729}
4730
4731/// Whether a flag may claim its following token as a detached value.
4732///
4733/// A required value keeps the historical negative-number exception. A value
4734/// that may be omitted needs an explicit opt-in to distinguish a negative value
4735/// from the flag's bare form.
4736fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4737    !flag.require_equals
4738        && (!is_flag_like(token)
4739            || flag.allow_hyphen_values()
4740            || (is_negative_number(token)
4741                && (flag
4742                    .arg
4743                    .as_ref()
4744                    .is_some_and(|arg| arg.allow_negative_numbers)
4745                    || (flag.default_missing.is_none() && !flag.value_optional))))
4746}
4747
4748fn record_scalar_flag_occurrence(
4749    cmds: &[SpecCommand],
4750    flag: &Arc<SpecFlag>,
4751    command_level: usize,
4752    bool_value: Option<bool>,
4753    occurrences: &mut HashMap<(usize, usize), u8>,
4754    errors: &mut Vec<UsageErr>,
4755) {
4756    let strict = cmds.get(command_level).is_some_and(|cmd| {
4757        !cmd.args_override_self
4758            || cmd
4759                .clause
4760                .as_ref()
4761                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4762    });
4763    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4764    if !strict || flag.count || collects_values {
4765        return;
4766    }
4767
4768    let bit = match bool_value {
4769        Some(false) if flag.negate.is_some() => 0b10,
4770        _ => 0b01,
4771    };
4772    let key = (Arc::as_ptr(flag) as usize, command_level);
4773    let seen = occurrences.entry(key).or_default();
4774    if *seen & bit != 0 {
4775        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4776    }
4777    *seen |= bit;
4778}
4779
4780/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4781/// external command.
4782///
4783/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4784/// so it was never a candidate to *select* anything either. usage-argv uses the
4785/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4786/// treated every token that `starts_with('-')` as a flag.
4787fn is_command_word(token: &str) -> bool {
4788    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4789}
4790
4791/// Whether an otherwise numeric-looking token is an exact declared short flag.
4792///
4793/// This check belongs in both parse phases: phase 1 must skip the flag while it
4794/// searches for a later subcommand, and phase 2 must bind it instead of offering
4795/// it to an `allow_negative_numbers` positional.
4796fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4797    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4798}
4799
4800/// Whether an unmatched word belongs to the root's default command.
4801///
4802/// Ordinary words always do. A negative number only does when the default command's
4803/// first positional explicitly accepts one; otherwise it stays at the root, matching
4804/// usage-argv and generated Go.
4805fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4806    !is_negative_number(token)
4807        || cmd
4808            .find_subcommand(default_name)
4809            .and_then(|default| default.args.first())
4810            .is_some_and(|arg| arg.allow_negative_numbers)
4811}
4812
4813/// Digits, at most one `.`, and an optional exponent.
4814///
4815/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4816/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4817/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4818/// two disagreed about `-1e5` when one used a float parse and the other did not.
4819fn is_number(rest: &str) -> bool {
4820    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4821        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4822        None => (rest, None),
4823    };
4824
4825    let mut seen_digit = false;
4826    let mut seen_dot = false;
4827    for c in mantissa.chars() {
4828        match c {
4829            '0'..='9' => seen_digit = true,
4830            '.' if !seen_dot => seen_dot = true,
4831            _ => return false,
4832        }
4833    }
4834    if !seen_digit {
4835        return false;
4836    }
4837
4838    match exponent {
4839        None => true,
4840        Some(exp) => {
4841            let digits = exp
4842                .strip_prefix('+')
4843                .or_else(|| exp.strip_prefix('-'))
4844                .unwrap_or(exp);
4845            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4846        }
4847    }
4848}
4849
4850/// Bind one value to the flag waiting for it, and let a variadic argument go on
4851/// collecting from the words that follow.
4852///
4853/// Every route to a flag's value comes through here — the following word, the text
4854/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4855/// like — so that all three agree on how many values the flag ends up with.
4856#[allow(clippy::too_many_arguments)]
4857fn bind_pending_flag_value(
4858    spec: &Spec,
4859    cmd: &SpecCommand,
4860    errors: &mut Vec<UsageErr>,
4861    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4862    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4863    word: &mut String,
4864    input: &mut VecDeque<Token>,
4865    custom_env: Option<&HashMap<String, String>>,
4866    trace: &mut Trace,
4867    // Which token supplied `word`, and whether it rode along on the flag's own token
4868    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4869    // their own positions and are recorded where they are read.
4870    argv: usize,
4871    attached: bool,
4872) -> miette::Result<bool> {
4873    // Held before the drain pops it, along with what the flag is already carrying: a
4874    // `var_max` bounds the values this occurrence takes, not the list they are appended
4875    // to, so a second `--include` starts counting again.
4876    let collecting = flag_awaiting_value
4877        .last()
4878        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4879        .cloned()
4880        .map(|flag| {
4881            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4882            (flag, carried)
4883        });
4884    let mut bound = vec![];
4885    let refused = drain_pending_flag_values(
4886        spec,
4887        cmd,
4888        errors,
4889        flags,
4890        flag_awaiting_value,
4891        word,
4892        custom_env,
4893        &mut bound,
4894    )?;
4895    for (flag, values) in bound {
4896        trace.record(
4897            argv,
4898            TokenRole::Value {
4899                flag,
4900                values,
4901                attached,
4902            },
4903        );
4904    }
4905    if refused {
4906        return Ok(true);
4907    }
4908    let Some((flag, carried)) = collecting else {
4909        return Ok(false);
4910    };
4911    collect_variadic_flag_values(
4912        spec,
4913        cmd,
4914        errors,
4915        flags,
4916        flag_awaiting_value,
4917        &flag,
4918        carried,
4919        input,
4920        custom_env,
4921        trace,
4922    )
4923}
4924
4925/// Keep feeding a flag whose argument is variadic from the words that follow it.
4926///
4927/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4928/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4929/// This is greedy by design — a command declaring both such a flag and positionals will
4930/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4931///
4932/// `carried` is what the flag already held when this occurrence began, so the bound
4933/// counts this run rather than everything the flag has collected across the command
4934/// line. Each value goes through the same drain as the first, so choices are checked
4935/// and the value lands in the same list rather than by a second route that could
4936/// disagree.
4937#[allow(clippy::too_many_arguments)]
4938fn collect_variadic_flag_values(
4939    spec: &Spec,
4940    cmd: &SpecCommand,
4941    errors: &mut Vec<UsageErr>,
4942    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4943    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4944    flag: &Arc<SpecFlag>,
4945    carried: usize,
4946    input: &mut VecDeque<Token>,
4947    custom_env: Option<&HashMap<String, String>>,
4948    trace: &mut Trace,
4949) -> miette::Result<bool> {
4950    let max = flag
4951        .arg
4952        .as_ref()
4953        .and_then(|arg| arg.var_max)
4954        .unwrap_or(usize::MAX);
4955    while flags
4956        .get(flag)
4957        .map(value_count)
4958        .unwrap_or(0)
4959        .saturating_sub(carried)
4960        < max
4961    {
4962        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4963            break;
4964        };
4965        if flag
4966            .arg
4967            .as_ref()
4968            .and_then(|arg| arg.value_terminator.as_deref())
4969            == Some(next)
4970        {
4971            let terminator = input.pop_front().unwrap();
4972            trace.record(
4973                terminator.argv,
4974                TokenRole::ValueTerminator {
4975                    ends: flag.name.clone(),
4976                },
4977            );
4978            break;
4979        }
4980        // The separator is left where it is: stopping here hands it to the arm that
4981        // knows what it means, rather than reading it as one more value.
4982        if next == "--"
4983            || (is_flag_like(next)
4984                && !(flag
4985                    .arg
4986                    .as_ref()
4987                    .is_some_and(|arg| arg.allow_negative_numbers)
4988                    && is_negative_number(next)))
4989        {
4990            break;
4991        }
4992        let taken = input.pop_front().unwrap();
4993        let argv = taken.argv;
4994        let mut word = taken.word;
4995        flag_awaiting_value.push(Arc::clone(flag));
4996        let mut bound = vec![];
4997        let refused = drain_pending_flag_values(
4998            spec,
4999            cmd,
5000            errors,
5001            flags,
5002            flag_awaiting_value,
5003            &mut word,
5004            custom_env,
5005            &mut bound,
5006        )?;
5007        for (flag, values) in bound {
5008            // A later word of the same occurrence is its own token, and never attached:
5009            // only the first value can ride along on the flag.
5010            trace.record(
5011                argv,
5012                TokenRole::Value {
5013                    flag,
5014                    values,
5015                    attached: false,
5016                },
5017            );
5018        }
5019        if refused {
5020            return Ok(true);
5021        }
5022    }
5023    // The loop stops once the occurrence has reached its bound, which without a delimiter is
5024    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
5025    // several values, so the run can end up *past* the bound rather than on it, and stopping
5026    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
5027    // case — three values out of the one word the loop was entitled to take.
5028    //
5029    // Counted against `carried` like the loop itself, so this stays a statement about the
5030    // occurrence rather than about the list the occurrences build up.
5031    let taken = flags
5032        .get(flag)
5033        .map(value_count)
5034        .unwrap_or(0)
5035        .saturating_sub(carried);
5036    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min)
5037        && taken < min
5038    {
5039        errors.push(UsageErr::VarFlagTooFew {
5040            name: flag.name.clone(),
5041            min,
5042            got: taken,
5043        });
5044    }
5045    if taken > max {
5046        errors.push(UsageErr::VarFlagTooMany {
5047            name: flag.name.clone(),
5048            max,
5049            got: taken,
5050        });
5051    }
5052    Ok(false)
5053}
5054
5055/// How many values a flag is holding, for a bound that counts them.
5056fn value_count(value: &ParseValue) -> usize {
5057    match value {
5058        ParseValue::MultiString(values) => values.len(),
5059        ParseValue::MultiBool(values) => values.len(),
5060        _ => 1,
5061    }
5062}
5063
5064/// Finish a value-optional flag that was given with no value.
5065///
5066/// Returns whether anything was bound. Completions keep the flag waiting — a
5067/// half-typed `--color ` is a question about the value — so this is asked only
5068/// once a full parse has decided the value is not coming, or once the next token
5069/// has made that decision.
5070///
5071/// The missing string is a real value: if the flag names `choices`, it has to
5072/// be one of them, the same way an env var or a `default` is checked. Binding
5073/// first and failing later would leave the flag set to a value the spec forbids.
5074fn try_bind_default_missing(
5075    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5076    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5077    custom_env: Option<&HashMap<String, String>>,
5078    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
5079) -> miette::Result<bool> {
5080    let Some(flag) = flag_awaiting_value.last() else {
5081        return Ok(false);
5082    };
5083    let value = match flag.default_missing.clone() {
5084        Some(value) => value,
5085        None if flag.value_optional => {
5086            let flag = flag_awaiting_value.pop().unwrap();
5087            // Presence in the map distinguishes this from an absent flag; an
5088            // empty collection distinguishes it from an explicitly empty
5089            // `--flag=` string without inventing a sentinel value.
5090            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
5091            origins
5092                .entry(Arc::clone(&flag))
5093                .or_default()
5094                .push(ValueOrigin::DefaultMissing);
5095            if flag.var {
5096                // A repeated bare occurrence is still an occurrence. The string collection
5097                // uses an empty value for it, just as the concrete `default_missing` path
5098                // pushes one value per occurrence; otherwise bounds and consumers silently
5099                // lose every bare repeat after the first.
5100                flags
5101                    .entry(flag)
5102                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
5103                    .try_as_multi_string_mut()
5104                    .unwrap()
5105                    .push(String::new());
5106            } else if variadic_value {
5107                // A variadic occurrence stays pending after each value. Reaching the next
5108                // flag (or EOF) closes that same occurrence; it must not erase what it took.
5109                flags
5110                    .entry(flag)
5111                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
5112            } else {
5113                // A scalar pending here is a new bare occurrence. The normal permissive
5114                // repeat policy makes the later occurrence a correction, including a
5115                // correction from an explicit value back to the bare tri-state.
5116                flags.insert(flag, ParseValue::MultiString(Vec::new()));
5117            }
5118            return Ok(true);
5119        }
5120        None => return Ok(false),
5121    };
5122    if let Some(arg) = flag.arg.as_ref() {
5123        validate_choice_value(
5124            ChoiceTarget::option(flag),
5125            &value,
5126            arg.choices.as_ref(),
5127            custom_env,
5128        )?;
5129    }
5130    let flag = flag_awaiting_value.pop().unwrap();
5131    origins
5132        .entry(Arc::clone(&flag))
5133        .or_default()
5134        .push(ValueOrigin::DefaultMissing);
5135    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5136    if collecting {
5137        let arr = flags
5138            .entry(flag)
5139            .or_insert_with(|| ParseValue::MultiString(vec![]))
5140            .try_as_multi_string_mut()
5141            .unwrap();
5142        arr.push(value);
5143    } else {
5144        flags.insert(flag, ParseValue::String(value));
5145    }
5146    Ok(true)
5147}
5148
5149/// `bound` collects what each drained flag took, in the order it took it. The values are
5150/// the word after any `delimiter` split, which is the only place that split is known: by the
5151/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
5152/// second occurrence has appended to the same list.
5153#[allow(clippy::too_many_arguments)]
5154fn drain_pending_flag_values(
5155    spec: &Spec,
5156    cmd: &SpecCommand,
5157    errors: &mut Vec<UsageErr>,
5158    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5159    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5160    word: &mut String,
5161    custom_env: Option<&HashMap<String, String>>,
5162    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5163) -> miette::Result<bool> {
5164    while let Some(flag) = flag_awaiting_value.pop() {
5165        let arg = flag.arg.as_ref().unwrap();
5166        // Split before anything judges the word, because after the split it is no longer
5167        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
5168        // Judging first would reject the whole word against a list neither half is on.
5169        let parts: Vec<String> = match arg.delimiter {
5170            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5171            None => vec![std::mem::take(word)],
5172        };
5173        for part in &parts {
5174            if validate_choices(
5175                spec,
5176                cmd,
5177                errors,
5178                ChoiceTarget::option(&flag),
5179                part,
5180                arg.choices.as_ref(),
5181                custom_env,
5182            )? {
5183                return Ok(true);
5184            }
5185        }
5186        word.clear();
5187        bound.push((Arc::clone(&flag), parts.clone()));
5188        // Two ways to hold several values, and both record a list: a `var` flag
5189        // collects one per occurrence, a variadic argument collects several from one.
5190        if flag.var || arg.var {
5191            let arr = flags
5192                .entry(flag)
5193                .or_insert_with(|| ParseValue::MultiString(vec![]))
5194                .try_as_multi_string_mut()
5195                .unwrap();
5196            arr.extend(parts);
5197        } else {
5198            // Nowhere for a second value to go, so the word stands as it was typed. A
5199            // delimiter on a flag that takes one value is refused where it is written.
5200            flags.insert(
5201                flag,
5202                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5203            );
5204        }
5205    }
5206    Ok(false)
5207}
5208
5209fn choice_error(
5210    target: ChoiceTarget<'_>,
5211    value: &str,
5212    choices: Option<&SpecChoices>,
5213    custom_env: Option<&HashMap<String, String>>,
5214) -> Option<String> {
5215    let choices = choices?;
5216    if !choices.strict {
5217        return None;
5218    }
5219    // The declared values and `env=` first, so a value they accept never costs a command.
5220    match choices.matches_resolved(value, custom_env) {
5221        Ok(true) => return None,
5222        Ok(false) => {}
5223        // Only a command could say, and this parse may not run one: let the value through
5224        // rather than report an error the real run might not have.
5225        Err(_) if choices.run().is_some() && crate::spec::choices::runs_disabled() => return None,
5226        Err(err) => {
5227            return Some(format!(
5228                "Could not check {} {}: {value} against its choices: {err}",
5229                target.kind, target.name,
5230            ));
5231        }
5232    }
5233    // Already run by the check above, so this reads what it printed rather than running it
5234    // again.
5235    let values = choices
5236        .resolved_values(custom_env)
5237        .unwrap_or_else(|_| choices.values_with_env(custom_env));
5238    if let Some(run) = choices.run()
5239        && values.is_empty()
5240    {
5241        return Some(format!(
5242            "Invalid choice for {} {}: {value}, `{run}` printed no choices",
5243            target.kind, target.name,
5244        ));
5245    }
5246    if let Some(env) = choices.env()
5247        && values.is_empty()
5248    {
5249        return Some(format!(
5250            "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5251            target.kind, target.name,
5252        ));
5253    }
5254    Some(format!(
5255        "Invalid choice for {} {}: {value}, expected one of {}",
5256        target.kind,
5257        target.name,
5258        values.join(", ")
5259    ))
5260}
5261
5262fn validate_choices(
5263    spec: &Spec,
5264    cmd: &SpecCommand,
5265    errors: &mut Vec<UsageErr>,
5266    target: ChoiceTarget<'_>,
5267    value: &str,
5268    choices: Option<&SpecChoices>,
5269    custom_env: Option<&HashMap<String, String>>,
5270) -> miette::Result<bool> {
5271    // A choice named like a help flag is a value, whether it was declared or printed by
5272    // `run=`. A command that cannot be run leaves the word to mean help, which is what a
5273    // user typing it most likely wanted.
5274    if is_help_arg(spec, cmd, value)
5275        && choices.is_some_and(|choices| {
5276            choices.strict && !choices.matches_resolved(value, custom_env).unwrap_or(false)
5277        })
5278    {
5279        errors.push(render_help_err(spec, cmd, value.len() > 2, custom_env));
5280        return Ok(true);
5281    }
5282
5283    if let Some(err) = choice_error(target, value, choices, custom_env) {
5284        bail!("{err}");
5285    }
5286    Ok(false)
5287}
5288
5289fn validate_choice_value(
5290    target: ChoiceTarget<'_>,
5291    value: &str,
5292    choices: Option<&SpecChoices>,
5293    custom_env: Option<&HashMap<String, String>>,
5294) -> miette::Result<()> {
5295    if let Some(err) = choice_error(target, value, choices, custom_env) {
5296        bail!("{err}");
5297    }
5298    Ok(())
5299}
5300
5301fn validate_choice_values(
5302    target: ChoiceTarget<'_>,
5303    values: &[String],
5304    choices: Option<&SpecChoices>,
5305    custom_env: Option<&HashMap<String, String>>,
5306) -> miette::Result<()> {
5307    for value in values {
5308        validate_choice_value(target, value, choices, custom_env)?;
5309    }
5310    Ok(())
5311}
5312
5313/// Everything a parse records about where it stopped: the positional cursor, so callers that
5314/// do not re-run the parse — completions, above all — agree with it, and the token trace.
5315///
5316/// Every exit from the binding phase comes through here, which is what makes it the right
5317/// place to close the trace: whatever is still queued was never read, and saying so is more
5318/// useful than leaving those words out of the report entirely.
5319fn record_stop(
5320    out: &mut ParseOutput,
5321    next_arg_idx: usize,
5322    seen_double_dash: bool,
5323    trace: &mut Trace,
5324    unread: &VecDeque<Token>,
5325) {
5326    out.next_arg = active_args(&out.cmd)
5327        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5328        .cloned()
5329        .map(Arc::new);
5330    out.double_dash_seen = seen_double_dash;
5331    finalize_current_clause(out);
5332    trace.close(unread);
5333    out.tokens = std::mem::take(&mut trace.tokens);
5334}
5335
5336fn finalize_current_clause(out: &mut ParseOutput) {
5337    let Some(clause) = out.cmd.clause.as_ref() else {
5338        return;
5339    };
5340    let has_scoped_flags = clause
5341        .flags
5342        .iter()
5343        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5344    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5345        return;
5346    }
5347    out.clauses
5348        .entry(clause.name.clone())
5349        .or_default()
5350        .push(std::mem::take(&mut out.args));
5351    let mut flags = IndexMap::new();
5352    for clause_flag in &clause.flags {
5353        let key = Arc::new(clause_flag.clone());
5354        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5355            flags.insert(flag, value);
5356        }
5357    }
5358    out.clause_flags
5359        .entry(clause.name.clone())
5360        .or_default()
5361        .push(flags);
5362}
5363
5364fn restore_current_clause(out: &mut ParseOutput) {
5365    let Some(clause) = out.cmd.clause.as_ref() else {
5366        return;
5367    };
5368    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5369        out.args = current;
5370    }
5371    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5372        out.flags.extend(current);
5373    }
5374}
5375
5376fn reset_clause_scalar_occurrences(
5377    out: &ParseOutput,
5378    occurrences: &mut HashMap<(usize, usize), u8>,
5379) {
5380    let Some(clause) = out.cmd.clause.as_ref() else {
5381        return;
5382    };
5383    let names = clause
5384        .flags
5385        .iter()
5386        .map(|flag| flag.name.as_str())
5387        .collect::<HashSet<_>>();
5388    let pointers = unique_flags(out.available_flags.values())
5389        .filter(|flag| names.contains(flag.name.as_str()))
5390        .map(|flag| Arc::as_ptr(flag) as usize)
5391        .collect::<HashSet<_>>();
5392    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5393}
5394
5395fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5396    while active_args(cmd)
5397        .get(idx)
5398        .is_some_and(|arg| arg.sigil.is_some())
5399    {
5400        idx += 1;
5401    }
5402    idx
5403}
5404
5405fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5406    cmd.clause
5407        .as_ref()
5408        .map(|clause| clause.args.as_slice())
5409        .unwrap_or(cmd.args.as_slice())
5410}
5411
5412fn match_sigil_arg<'a>(
5413    cmd: &'a SpecCommand,
5414    word: &'a str,
5415) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5416    cmd.args
5417        .iter()
5418        .filter_map(|arg| {
5419            let sigil = arg.sigil.as_deref()?;
5420            let value = word.strip_prefix(sigil)?;
5421            Some((arg, sigil, value))
5422        })
5423        .max_by_key(|(_, sigil, _)| sigil.len())
5424}
5425
5426fn match_sigil_arg_chain<'a>(
5427    cmds: &'a [SpecCommand],
5428    word: &'a str,
5429) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5430    cmds.iter()
5431        .filter_map(|cmd| match_sigil_arg(cmd, word))
5432        .max_by_key(|(_, sigil, _)| sigil.len())
5433}
5434
5435/// Record that `arg` was handed a word before the `--` it requires.
5436///
5437/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
5438/// message is emitted only the first time each arg is seen. The set is also what suppresses the
5439/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
5440/// end of the parse.
5441fn report_double_dash_violation(
5442    arg: &SpecArg,
5443    errors: &mut Vec<UsageErr>,
5444    violations: &mut HashSet<String>,
5445) {
5446    if violations.insert(arg.name.clone()) {
5447        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5448    }
5449}
5450
5451/// `--version` and `-V`, which the parser supplies where the spec declares a version.
5452///
5453/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
5454/// command tables — both of which accepted these spellings while this parser called them
5455/// unknown words. The help page has always listed `-V, --version` under the same
5456/// condition, and said in as many words that it did so "only where a version is
5457/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
5458/// page advertising a flag the parse refused.
5459///
5460/// The root only, because that is where the page lists it: `version` is a property of
5461/// the program, and a subcommand answering with the program's version is a claim no spec
5462/// made. A declared flag wins by arriving first — every scan consults this only after
5463/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
5464/// keeps `--version` supplied beside it.
5465fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5466    (spec.version.is_some() || spec.long_version.is_some())
5467        && cmds.len() == 1
5468        && !spec.cmd.disable_version_flag
5469        && (w == "--version" || w == "-V")
5470}
5471
5472fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5473    spec.disable_help != Some(true)
5474        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5475            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5476}
5477
5478impl ParseOutput {
5479    pub fn as_env(&self) -> BTreeMap<String, String> {
5480        let mut env = BTreeMap::new();
5481        for (flag, val) in &self.flags {
5482            let key = format!("usage_{}", crate::case::snake(&flag.name));
5483            let val = match val {
5484                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5485                ParseValue::String(s) => s.clone(),
5486                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5487                ParseValue::MultiString(s) => crate::shell_words::join(s),
5488            };
5489            env.insert(key, val);
5490        }
5491        for (arg, val) in &self.args {
5492            let key = format!("usage_{}", crate::case::snake(&arg.name));
5493            env.insert(key, val.to_string());
5494        }
5495        // The subcommand path, canonical names joined by spaces (`db migrate`), so a script can
5496        // dispatch on it without re-reading argv. Unset at the root, where there is nothing to
5497        // dispatch on. `cmd` is also an ordinary argument name, so a flag or arg the spec
5498        // declares under it anywhere on the path, clauses included, keeps the variable: a
5499        // script reading `$usage_cmd` for its own `<cmd>` must never get the path instead,
5500        // even when the arg was left out.
5501        let path = self.cmds.iter().skip(1).map(|c| c.name.as_str()).join(" ");
5502        let declared = |c: &SpecCommand| {
5503            let clause = c.clause.as_ref();
5504            c.args
5505                .iter()
5506                .chain(clause.into_iter().flat_map(|cl| &cl.args))
5507                .any(|a| crate::case::snake(&a.name) == "cmd")
5508                || c.flags
5509                    .iter()
5510                    .chain(clause.into_iter().flat_map(|cl| &cl.flags))
5511                    .any(|f| crate::case::snake(&f.name) == "cmd")
5512        };
5513        if !path.is_empty() && !self.cmds.iter().any(declared) {
5514            env.entry("usage_cmd".to_string()).or_insert(path);
5515        }
5516        env
5517    }
5518}
5519
5520impl Display for ParseValue {
5521    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5522        match self {
5523            ParseValue::Bool(b) => write!(f, "{b}"),
5524            ParseValue::String(s) => write!(f, "{s}"),
5525            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5526            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5527        }
5528    }
5529}
5530
5531/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5532fn render_token(token: &TokenBinding) -> String {
5533    let roles = token.roles.iter().map(render_role).join(", ");
5534    let synthesized = if token.synthesized { " (read as)" } else { "" };
5535    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5536}
5537
5538fn render_role(role: &TokenRole) -> String {
5539    match role {
5540        TokenRole::Program => "program".to_string(),
5541        TokenRole::Command { name } => format!("subcommand {name}"),
5542        TokenRole::Flag {
5543            flag,
5544            spelling,
5545            negated,
5546        } => {
5547            let negated = if *negated { ", negated" } else { "" };
5548            format!("flag {} as {spelling}{negated}", flag.name)
5549        }
5550        TokenRole::Value {
5551            flag,
5552            values,
5553            attached,
5554        } => {
5555            let attached = if *attached { ", attached" } else { "" };
5556            format!("value of {} = {values:?}{attached}", flag.name)
5557        }
5558        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5559        TokenRole::Sigil { arg, sigil, values } => {
5560            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5561        }
5562        TokenRole::Separator => "separator".to_string(),
5563        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5564        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5565        TokenRole::Restart => "restart".to_string(),
5566        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5567        TokenRole::UnknownFlag { bound_as } => match bound_as {
5568            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5569            None => "unknown flag".to_string(),
5570        },
5571        TokenRole::Refused { reason } => format!("refused: {reason}"),
5572        TokenRole::External => "external".to_string(),
5573        TokenRole::Unread => "unread".to_string(),
5574    }
5575}
5576
5577impl Debug for ParseOutput {
5578    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5579        f.debug_struct("ParseOutput")
5580            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5581            .field(
5582                "args",
5583                &self
5584                    .args
5585                    .iter()
5586                    .map(|(a, w)| format!("{}: {w}", a.name))
5587                    .collect_vec(),
5588            )
5589            .field("clauses", &self.clauses)
5590            .field(
5591                "available_flags",
5592                &self
5593                    .available_flags
5594                    .iter()
5595                    .map(|(f, w)| format!("{f}: {w}"))
5596                    .collect_vec(),
5597            )
5598            .field(
5599                "flags",
5600                &self
5601                    .flags
5602                    .iter()
5603                    .map(|(f, w)| format!("{}: {w}", f.name))
5604                    .collect_vec(),
5605            )
5606            .field("flag_awaiting_value", &self.flag_awaiting_value)
5607            .field("errors", &self.errors)
5608            .field("external", &self.external)
5609            // Provenance, one line per token and one per fallback. This is the parser's
5610            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5611            // first — `usage explain` renders the same facts for a reader.
5612            .field(
5613                "tokens",
5614                &self.tokens.iter().map(render_token).collect_vec(),
5615            )
5616            .field(
5617                "origins",
5618                &self
5619                    .flag_origins
5620                    .iter()
5621                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5622                    .chain(
5623                        self.arg_origins
5624                            .iter()
5625                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5626                    )
5627                    .collect_vec(),
5628            )
5629            .field("overridden_flags", &self.overridden_flags)
5630            .finish()
5631    }
5632}
5633
5634#[cfg(test)]
5635mod tests {
5636    use super::*;
5637    use crate::SpecFlagAction;
5638
5639    fn input(words: &[&str]) -> Vec<String> {
5640        words.iter().map(|word| (*word).to_string()).collect()
5641    }
5642
5643    #[test]
5644    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5645        // The page has always listed `-V, --version` wherever a `version` is declared,
5646        // and usage-argv and usage-go have always accepted both. This parser called them
5647        // unknown words, so the one implementation the corpus measures the others against
5648        // was the one that disagreed.
5649        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5650            .parse()
5651            .unwrap();
5652
5653        for spelling in ["--version", "-V"] {
5654            let err = parse(&spec, &input(&["ex", spelling]))
5655                .expect_err("answering with a version ends the parse");
5656            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5657        }
5658    }
5659
5660    #[test]
5661    fn the_supplied_version_flag_is_the_roots_alone() {
5662        // `version` describes the program, and the page lists the entry on the program's
5663        // own page only. A subcommand answering with it would be a claim no spec made.
5664        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5665            .parse()
5666            .unwrap();
5667
5668        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5669        assert_eq!(err.to_string(), "unexpected word: --version");
5670    }
5671
5672    #[test]
5673    fn no_declared_version_supplies_nothing() {
5674        // A `--version` answering with nothing is worse than one that is not there, which
5675        // is why the entry is conditional on the page and the spelling on the parse.
5676        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5677
5678        for spelling in ["--version", "-V"] {
5679            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5680            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5681        }
5682    }
5683
5684    #[test]
5685    fn disable_version_flag_removes_the_supplied_spellings() {
5686        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5687            .parse()
5688            .unwrap();
5689
5690        for spelling in ["--version", "-V"] {
5691            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5692            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5693        }
5694    }
5695
5696    #[test]
5697    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5698        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5699        // parse agrees without being told, because a declared flag is matched first.
5700        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5701            .parse()
5702            .unwrap();
5703
5704        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5705        assert_eq!(out.flags.len(), 1);
5706
5707        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5708        assert_eq!(err.to_string(), "1.2.3");
5709    }
5710
5711    #[test]
5712    fn the_supplied_spellings_split_the_two_version_texts() {
5713        // The same split `render_action_err` gives a declared version flag: the long
5714        // spelling prefers `long_version`, the short prefers the concise one.
5715        let spec: Spec =
5716            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5717                .parse()
5718                .unwrap();
5719
5720        assert_eq!(
5721            parse(&spec, &input(&["ex", "--version"]))
5722                .unwrap_err()
5723                .to_string(),
5724            "1.2.3 (abcdef)"
5725        );
5726        assert_eq!(
5727            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5728            "1.2.3"
5729        );
5730    }
5731
5732    #[test]
5733    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5734        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5735        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5736        // naming nothing: usage-argv and usage-go both resolve the letter through the
5737        // same lookup that finds a declared short, and clap prints help for it too.
5738        let spec: Spec =
5739            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5740                .parse()
5741                .unwrap();
5742
5743        for token in ["-vh", "-hv"] {
5744            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5745            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5746        }
5747        for token in ["-vV", "-Vv"] {
5748            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5749            assert_eq!(err.to_string(), "1.2.3", "{token}");
5750        }
5751    }
5752
5753    #[test]
5754    fn a_bundled_help_letter_asks_for_the_short_page() {
5755        // Whatever else shares the token: `-h` is the short spelling, and the letters
5756        // beside it say nothing about which page was asked for.
5757        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"
5758            .parse()
5759            .unwrap();
5760
5761        let short = parse(&spec, &input(&["ex", "-vh"]))
5762            .unwrap_err()
5763            .to_string();
5764        let long = parse(&spec, &input(&["ex", "--help"]))
5765            .unwrap_err()
5766            .to_string();
5767        assert!(short.contains("Be loud"), "{short}");
5768        assert!(!short.contains("at length"), "{short}");
5769        assert!(long.contains("at length"), "{long}");
5770    }
5771
5772    #[test]
5773    fn the_bundled_version_letter_is_the_roots_alone() {
5774        // The same rule the whole-token spelling follows, asked one letter at a time.
5775        let spec: Spec =
5776            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5777                .parse()
5778                .unwrap();
5779
5780        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5781        assert_eq!(err.to_string(), "unexpected word: -vV");
5782    }
5783
5784    #[test]
5785    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5786        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5787        // this spec's own `-h`, taking its value from the rest of the token.
5788        let spec: Spec =
5789            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5790                .parse()
5791                .unwrap();
5792
5793        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5794        assert_eq!(out.flags.len(), 2);
5795        assert!(
5796            out.flags
5797                .iter()
5798                .any(|(flag, value)| flag.name == "host" && value.to_string() == "local")
5799        );
5800    }
5801
5802    #[test]
5803    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5804        let spec: Spec =
5805            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5806                .parse()
5807                .unwrap();
5808
5809        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5810        assert_eq!(err.to_string(), "unexpected word: -vh");
5811    }
5812
5813    #[test]
5814    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5815        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5816        // on the way to discovering that `z` names nothing.
5817        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5818            .parse()
5819            .unwrap();
5820
5821        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5822        assert!(out.flags.is_empty(), "{:?}", out.flags);
5823        assert_eq!(out.args.len(), 1);
5824    }
5825
5826    fn spec_with_arg(arg: SpecArg) -> Spec {
5827        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5828        Spec {
5829            name: "test".to_string(),
5830            bin: "test".to_string(),
5831            cmd,
5832            ..Default::default()
5833        }
5834    }
5835
5836    fn spec_with_flag(flag: SpecFlag) -> Spec {
5837        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5838        Spec {
5839            name: "test".to_string(),
5840            bin: "test".to_string(),
5841            cmd,
5842            ..Default::default()
5843        }
5844    }
5845
5846    fn parse_with_env(
5847        spec: &Spec,
5848        words: &[&str],
5849        env: &[(&str, &str)],
5850    ) -> Result<ParseOutput, miette::Error> {
5851        let env = env
5852            .iter()
5853            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5854            .collect();
5855        Parser::new(spec).with_env(env).parse(&input(words))
5856    }
5857
5858    fn first_string_value(parsed: &ParseOutput) -> &str {
5859        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5860            return value;
5861        }
5862        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5863            return value;
5864        }
5865        panic!("expected first parsed value to be ParseValue::String");
5866    }
5867
5868    #[test]
5869    fn custom_environment_parser_dispatches_executable_views() {
5870        let spec: Spec = r#"
5871bin "ex"
5872view "runner" root="run"
5873cmd "run" {
5874    flag "--token <token>" env="TOKEN"
5875}
5876        "#
5877        .parse()
5878        .unwrap();
5879        let parsed = Parser::new(&spec)
5880            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5881            .parse(&input(&["runner"]))
5882            .unwrap();
5883
5884        assert_eq!(parsed.cmd.name, "runner");
5885        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5886            && matches!(value, ParseValue::String(value) if value == "secret")));
5887    }
5888
5889    #[test]
5890    fn an_executable_view_keeps_the_hosts_version_action() {
5891        let spec: Spec = r#"
5892bin "ex"
5893version "1.2.3"
5894flag "-V --version" action="version"
5895flag "--verbose" global=#true
5896view "runner" root="run" globals=#true
5897cmd "run"
5898        "#
5899        .parse()
5900        .unwrap();
5901
5902        let error = Parser::new(&spec)
5903            .parse(&input(&["runner", "--version"]))
5904            .expect_err("the host version action should answer before view projection");
5905        assert_eq!(error.to_string(), "1.2.3");
5906
5907        let error = Parser::new(&spec)
5908            .parse(&input(&["runner", "--verbose", "--version"]))
5909            .expect_err("the host version action should remain after a carried global");
5910        assert_eq!(error.to_string(), "1.2.3");
5911    }
5912
5913    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5914        let flag = parsed
5915            .flags
5916            .keys()
5917            .find(|flag| flag.name == name)
5918            .unwrap_or_else(|| panic!("expected flag {name}"));
5919        let value = parsed
5920            .flags
5921            .get(flag)
5922            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5923        match value {
5924            ParseValue::String(value) => value,
5925            _ => panic!("expected flag {name} to be ParseValue::String"),
5926        }
5927    }
5928
5929    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5930        let err = result.expect_err("expected parser error");
5931        assert_eq!(format!("{err}"), expected);
5932    }
5933
5934    #[test]
5935    fn a_short_version_action_falls_back_to_the_long_version() {
5936        let flag = SpecFlag::builder()
5937            .short('R')
5938            .action(SpecFlagAction::Version)
5939            .build();
5940        let spec = Spec {
5941            name: "test".to_string(),
5942            bin: "test".to_string(),
5943            long_version: Some("1.2.3\ncommit abc123".to_string()),
5944            ..Default::default()
5945        };
5946        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R", None)
5947        else {
5948            panic!("expected version action")
5949        };
5950        assert_eq!(version, "1.2.3\ncommit abc123");
5951    }
5952
5953    #[cfg(feature = "unstable_choices_env")]
5954    fn spec_arg_choices_env(key: &str) -> Spec {
5955        spec_with_arg(
5956            SpecArg::builder()
5957                .name("env")
5958                .choices_env(key)
5959                .required(false)
5960                .build(),
5961        )
5962    }
5963
5964    #[cfg(feature = "unstable_choices_env")]
5965    fn spec_flag_choices_env(key: &str) -> Spec {
5966        spec_with_flag(
5967            SpecFlag::builder()
5968                .long("env")
5969                .arg(SpecArg::builder().name("env").choices_env(key).build())
5970                .build(),
5971        )
5972    }
5973
5974    #[test]
5975    fn test_parse() {
5976        let cmd = SpecCommand::builder()
5977            .name("test")
5978            .arg(SpecArg::builder().name("arg").build())
5979            .flag(SpecFlag::builder().long("flag").build())
5980            .build();
5981        let spec = Spec {
5982            name: "test".to_string(),
5983            bin: "test".to_string(),
5984            cmd,
5985            ..Default::default()
5986        };
5987        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5988        let parsed = parse(&spec, &input).unwrap();
5989        assert_eq!(parsed.cmds.len(), 1);
5990        assert_eq!(parsed.cmds[0].name, "test");
5991        assert_eq!(parsed.args.len(), 1);
5992        assert_eq!(parsed.flags.len(), 1);
5993        assert_eq!(parsed.available_flags.len(), 1);
5994    }
5995
5996    #[test]
5997    fn test_flag_overrides_last_occurrence_wins() {
5998        let spec: Spec = r#"
5999flag "--stdin" default=#true
6000flag "--file <file>" overrides="--stdin"
6001        "#
6002        .parse()
6003        .unwrap();
6004
6005        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
6006        assert_eq!(file_wins.flags.len(), 1);
6007        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
6008        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
6009
6010        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
6011        assert_eq!(stdin_wins.flags.len(), 1);
6012        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
6013        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
6014    }
6015
6016    #[test]
6017    fn test_flag_override_clears_pending_value() {
6018        let spec: Spec = r#"
6019flag "--file <file>" overrides="--stdin"
6020flag "--stdin"
6021arg "[input]"
6022        "#
6023        .parse()
6024        .unwrap();
6025
6026        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
6027        assert_eq!(parsed.flags.len(), 1);
6028        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
6029        assert_eq!(first_string_value(&parsed), "input.txt");
6030    }
6031
6032    #[cfg(unix)]
6033    #[test]
6034    fn a_mount_on_the_root_discovers_subcommands() {
6035        // The root is a command like any other, so it can find its own subcommands
6036        // by running something. Uses `echo` rather than a fixture because resolving
6037        // a mount is what is being tested.
6038        let spec: Spec = r#"
6039name "ex"
6040bin "ex"
6041cmd "declared"
6042mount run="echo 'cmd \"discovered\"'"
6043"#
6044        .parse()
6045        .unwrap();
6046
6047        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6048        assert_eq!(out.cmd.name, "discovered");
6049    }
6050
6051    #[test]
6052    fn unknown_env_knows_the_variables_a_mounted_command_declares() {
6053        let spec: Spec = r#"
6054name "ex"
6055bin "ex"
6056unknown_env prefix="EX_"
6057mount run="discover"
6058cmd "publish"
6059"#
6060        .parse()
6061        .unwrap();
6062        let mounts: HashMap<String, String> = [(
6063            "discover".to_string(),
6064            "cmd \"serve\" {\n  flag \"--port <port>\" env=\"EX_PORT\"\n}\n".to_string(),
6065        )]
6066        .into();
6067        let env = |name: &str| -> HashMap<String, String> { [(name.into(), "1".into())].into() };
6068
6069        let out = Parser::new(&spec)
6070            .with_mount_outputs(mounts.clone())
6071            .with_env(env("EX_PORT"))
6072            .parse(&input(&["ex", "serve"]))
6073            .expect("the mounted command declares EX_PORT");
6074        assert_eq!(out.cmd.name, "serve");
6075
6076        // `publish` is declared, so the parse never runs the mount, and the check does not run
6077        // it either: what only a mount declares is for `allow`.
6078        let err = Parser::new(&spec)
6079            .with_mount_outputs(mounts.clone())
6080            .with_env(env("EX_PORT"))
6081            .parse(&input(&["ex", "publish"]))
6082            .unwrap_err();
6083        assert!(err.to_string().contains("EX_PORT"), "{err}");
6084
6085        let err = Parser::new(&spec)
6086            .with_mount_outputs(mounts)
6087            .with_env(env("EX_PROT"))
6088            .parse(&input(&["ex", "serve"]))
6089            .unwrap_err();
6090        assert!(err.to_string().contains("EX_PROT"), "{err}");
6091    }
6092
6093    #[test]
6094    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
6095        let spec: Spec = r#"
6096name "ex"
6097bin "ex"
6098mount run="this command must never run"
6099cmd "declared"
6100"#
6101        .parse()
6102        .unwrap();
6103
6104        Parser::new(&spec)
6105            .with_mount_outputs(HashMap::new())
6106            .parse(&input(&["ex", "declared"]))
6107            .expect("a declared command does not resolve the mount");
6108
6109        let error = Parser::new(&spec)
6110            .with_mount_outputs(HashMap::new())
6111            .parse(&input(&["ex", "discovered"]))
6112            .unwrap_err();
6113        assert!(
6114            error
6115                .to_string()
6116                .contains("No injected output was provided for mount command"),
6117            "{error}"
6118        );
6119    }
6120
6121    #[cfg(unix)]
6122    #[test]
6123    fn completion_sees_root_mounted_commands_with_nothing_typed() {
6124        // The case a root mount exists for. `mycli <tab>` has no word to trigger
6125        // discovery with, so a completion has to resolve up front or the mounted
6126        // commands are never offered.
6127        let spec: Spec = r#"
6128name "ex"
6129bin "ex"
6130cmd "declared"
6131mount run="echo 'cmd \"discovered\"'"
6132"#
6133        .parse()
6134        .unwrap();
6135
6136        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6137        assert!(
6138            out.cmd.subcommands.contains_key("discovered"),
6139            "a completion should see mounted commands; got {:?}",
6140            out.cmd.subcommands.keys().collect::<Vec<_>>()
6141        );
6142    }
6143
6144    #[cfg(unix)]
6145    #[test]
6146    fn completion_and_execution_agree_about_discovery() {
6147        // Offering a command that a real parse would hand to the default instead is
6148        // worse than not offering it, so the gate applies to both paths. The mount
6149        // fails if it runs, which is how both halves are checked at once.
6150        let spec: Spec = r#"
6151name "ex"
6152bin "ex"
6153default_subcommand "run"
6154cmd "run" {
6155  arg "<task>"
6156}
6157mount run="exit 1"
6158"#
6159        .parse()
6160        .unwrap();
6161
6162        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6163        assert!(
6164            !out.cmd.subcommands.contains_key("discovered"),
6165            "a completion must not offer what execution will not route"
6166        );
6167
6168        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6169        assert_eq!(out.cmd.name, "run");
6170    }
6171
6172    #[cfg(unix)]
6173    #[test]
6174    fn a_default_subcommand_outranks_discovery() {
6175        // The default already says what an unmatched word means, and says it for
6176        // free. The mount fails if it runs, so parsing proves discovery was skipped.
6177        let spec: Spec = r#"
6178name "ex"
6179bin "ex"
6180default_subcommand "run"
6181cmd "run" {
6182  arg "<task>"
6183}
6184mount run="exit 1"
6185"#
6186        .parse()
6187        .unwrap();
6188
6189        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6190        assert_eq!(out.cmd.name, "run");
6191    }
6192
6193    #[cfg(unix)]
6194    #[test]
6195    fn a_mount_may_ask_to_outrank_the_default() {
6196        // Opting in, and paying for it: discovery runs first, so a discovered
6197        // command wins over the fallback.
6198        let spec: Spec = r#"
6199name "ex"
6200bin "ex"
6201default_subcommand "run"
6202cmd "run" {
6203  arg "<task>"
6204}
6205mount run="echo 'cmd \"discovered\"'" overrides_default=#true
6206"#
6207        .parse()
6208        .unwrap();
6209
6210        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6211        assert_eq!(out.cmd.name, "discovered");
6212
6213        // A word it does not know still reaches the default.
6214        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6215        assert_eq!(out.cmd.name, "run");
6216    }
6217
6218    #[cfg(unix)]
6219    #[test]
6220    fn a_flag_does_not_run_the_mount() {
6221        // A flag matches no subcommand, which would have been enough to trigger
6222        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
6223        // so parsing at all is the proof that it did not.
6224        let spec: Spec = r#"
6225name "ex"
6226bin "ex"
6227flag "--verbose"
6228cmd "declared"
6229mount run="exit 1"
6230"#
6231        .parse()
6232        .unwrap();
6233
6234        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6235        assert_eq!(out.cmd.name, "ex");
6236    }
6237
6238    #[cfg(unix)]
6239    #[test]
6240    fn a_declared_subcommand_does_not_run_the_mount() {
6241        // The mount would fail if it ran, so this parsing at all is the proof that
6242        // discovery is skipped when the word is already known. Worth pinning: a root
6243        // mount that resolved eagerly would spawn a process on every invocation.
6244        let spec: Spec = r#"
6245name "ex"
6246bin "ex"
6247cmd "declared"
6248mount run="exit 1"
6249"#
6250        .parse()
6251        .unwrap();
6252
6253        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6254        assert_eq!(out.cmd.name, "declared");
6255    }
6256
6257    #[test]
6258    fn a_root_mount_survives_being_written_out() {
6259        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6260            .parse()
6261            .unwrap();
6262        assert_eq!(spec.cmd.mounts.len(), 1);
6263
6264        let reparsed: Spec = spec.to_string().parse().unwrap();
6265        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6266        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6267    }
6268
6269    #[test]
6270    fn test_mount_prefix_applies_flag_overrides() {
6271        let stdin = Arc::new(
6272            SpecFlag::builder()
6273                .name("stdin")
6274                .long("stdin")
6275                .global(true)
6276                .build(),
6277        );
6278        let file = Arc::new(
6279            SpecFlag::builder()
6280                .name("file")
6281                .long("file")
6282                .arg(SpecArg::builder().name("file").build())
6283                .global(true)
6284                .overrides_with(vec!["--stdin".to_string()])
6285                .build(),
6286        );
6287        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6288
6289        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6290        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6291
6292        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6293    }
6294
6295    #[test]
6296    fn test_flag_override_suppresses_env_value() {
6297        let spec: Spec = r#"
6298flag "--stdin" env="USE_STDIN"
6299flag "--file <file>" overrides="--stdin"
6300        "#
6301        .parse()
6302        .unwrap();
6303
6304        let parsed = parse_with_env(
6305            &spec,
6306            &["test", "--file", "input.txt"],
6307            &[("USE_STDIN", "true")],
6308        )
6309        .unwrap();
6310        assert_eq!(parsed.flags.len(), 1);
6311        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6312    }
6313
6314    #[test]
6315    fn test_flag_override_suppresses_required_check() {
6316        let spec: Spec = r#"
6317flag "--stdin" required=#true
6318flag "--file <file>" overrides="--stdin"
6319        "#
6320        .parse()
6321        .unwrap();
6322
6323        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6324        assert_eq!(parsed.flags.len(), 1);
6325        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6326    }
6327
6328    #[test]
6329    fn test_flag_required_if() {
6330        let spec: Spec = r#"
6331flag "--dir <dir>"
6332flag "--file <file>" required_if="--dir"
6333        "#
6334        .parse()
6335        .unwrap();
6336
6337        parse(&spec, &input(&["test"])).unwrap();
6338        assert_parse_err(
6339            parse(&spec, &input(&["test", "--dir", "src"])),
6340            "Missing required flag: --file <file>",
6341        );
6342        parse(
6343            &spec,
6344            &input(&["test", "--dir", "src", "--file", "input.txt"]),
6345        )
6346        .unwrap();
6347    }
6348
6349    #[test]
6350    fn test_flag_required_unless() {
6351        let spec: Spec = r#"
6352flag "--stdin"
6353flag "--file <file>" required_unless="--stdin"
6354        "#
6355        .parse()
6356        .unwrap();
6357
6358        assert_parse_err(
6359            parse(&spec, &input(&["test"])),
6360            "Missing required flag: --file <file>",
6361        );
6362        parse(&spec, &input(&["test", "--stdin"])).unwrap();
6363        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6364    }
6365
6366    #[test]
6367    fn complete_required_relationship_truth_tables() {
6368        let spec: Spec = r#"
6369name "ex"
6370bin "ex"
6371flag "--mode <mode>"
6372flag "--scope <scope>"
6373flag "--token <token>" {
6374    required_if_eq "--mode" "remote"
6375}
6376flag "--approval <approval>" {
6377    required_if_eq_all "--mode" "remote" "--scope" "global"
6378}
6379flag "--input <input>" {
6380    required_unless "--stdin" "--file"
6381}
6382flag "--checksum <checksum>" {
6383    required_unless_all "--stdin" "--file"
6384}
6385flag "--stdin"
6386flag "--file <file>"
6387arg "[request]" {
6388    requires "--mode" "--scope"
6389}
6390"#
6391        .parse()
6392        .unwrap();
6393        let parse_args = |args: &[&str]| {
6394            parse(
6395                &spec,
6396                &args
6397                    .iter()
6398                    .map(|arg| (*arg).to_string())
6399                    .collect::<Vec<_>>(),
6400            )
6401        };
6402
6403        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6404        assert!(
6405            parse_args(&[
6406                "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6407            ])
6408            .is_err()
6409        );
6410        parse_args(&[
6411            "ex",
6412            "--mode",
6413            "remote",
6414            "--token",
6415            "secret",
6416            "--scope",
6417            "global",
6418            "--approval",
6419            "yes",
6420            "--stdin",
6421            "--file",
6422            "in",
6423        ])
6424        .unwrap();
6425        parse_args(&[
6426            "ex",
6427            "--mode",
6428            "local",
6429            "--scope",
6430            "project",
6431            "--stdin",
6432            "--checksum",
6433            "sum",
6434            "request.json",
6435        ])
6436        .unwrap();
6437
6438        let reparsed: Spec = spec.to_string().parse().unwrap();
6439        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6440        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6441        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6442        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6443    }
6444
6445    #[test]
6446    fn test_conditional_requirements_treat_env_as_explicit() {
6447        let spec: Spec = r#"
6448flag "--dir <dir>" env="INPUT_DIR"
6449flag "--stdin" env="USE_STDIN"
6450flag "--file <file>" required_if="--dir" required_unless="--stdin"
6451        "#
6452        .parse()
6453        .unwrap();
6454
6455        assert_parse_err(
6456            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6457            "Missing required flag: --file <file>",
6458        );
6459        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6460    }
6461
6462    #[test]
6463    fn test_custom_env_does_not_fall_back_to_process_env() {
6464        assert!(std::env::var("PATH").is_ok());
6465        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6466
6467        assert_parse_err(
6468            parse_with_env(&spec, &["test"], &[]),
6469            "Missing required flag: --file <file>",
6470        );
6471    }
6472
6473    #[test]
6474    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6475        let spec: Spec = r#"
6476flag "--dir <dir>" default="src"
6477flag "--file <file>" required_if="--dir"
6478        "#
6479        .parse()
6480        .unwrap();
6481
6482        parse(&spec, &input(&["test"])).unwrap();
6483    }
6484
6485    #[test]
6486    fn test_conditional_requirements_see_overridden_flags_as_absent() {
6487        let spec: Spec = r#"
6488flag "--stdin"
6489flag "--dir <dir>" overrides="--stdin"
6490flag "--file <file>" required_unless="--stdin"
6491        "#
6492        .parse()
6493        .unwrap();
6494
6495        assert_parse_err(
6496            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6497            "Missing required flag: --file <file>",
6498        );
6499    }
6500
6501    #[test]
6502    fn short_flag_is_one_character_not_one_byte() {
6503        // A short is declared and read by character. Counting bytes instead either
6504        // refuses the declaration or slices the token inside the character, and clap
6505        // — which many specs are generated from — accepts shorts like this one.
6506        let spec = spec_with_flag(
6507            SpecFlag::builder()
6508                .short('磨')
6509                .long("polish")
6510                .arg(SpecArg::builder().name("opt").build())
6511                .build(),
6512        );
6513        let attached = Parser::new(&spec)
6514            .parse(&input(&["test", "-磨VALUE"]))
6515            .unwrap();
6516        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6517        let detached = Parser::new(&spec)
6518            .parse(&input(&["test", "-磨", "V"]))
6519            .unwrap();
6520        assert_eq!(flag_string_value(&detached, "polish"), "V");
6521    }
6522
6523    #[test]
6524    fn test_as_env() {
6525        let cmd = SpecCommand::builder()
6526            .name("test")
6527            .arg(SpecArg::builder().name("arg").build())
6528            .flag(SpecFlag::builder().long("flag").build())
6529            .flag(
6530                SpecFlag::builder()
6531                    .long("force")
6532                    .negate("--no-force")
6533                    .build(),
6534            )
6535            .build();
6536        let spec = Spec {
6537            name: "test".to_string(),
6538            bin: "test".to_string(),
6539            cmd,
6540            ..Default::default()
6541        };
6542        let input = vec![
6543            "test".to_string(),
6544            "--flag".to_string(),
6545            "--no-force".to_string(),
6546        ];
6547        let parsed = parse(&spec, &input).unwrap();
6548        let env = parsed.as_env();
6549        assert_eq!(env.len(), 2);
6550        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6551        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6552    }
6553
6554    #[test]
6555    fn as_env_names_the_subcommand_path() {
6556        let spec: Spec = r#"
6557            flag "-v --verbose"
6558            cmd "db" {
6559                cmd "migrate" {
6560                    alias "m"
6561                    arg "[target]"
6562                }
6563            }
6564            "#
6565        .parse()
6566        .unwrap();
6567        let env = |args: &[&str]| {
6568            let input: Vec<String> = std::iter::once("test")
6569                .chain(args.iter().copied())
6570                .map(String::from)
6571                .collect();
6572            parse(&spec, &input).unwrap().as_env()
6573        };
6574
6575        // The canonical name, not the alias that was typed.
6576        let nested = env(&["db", "m", "v2"]);
6577        assert_eq!(
6578            nested.get("usage_cmd").map(String::as_str),
6579            Some("db migrate")
6580        );
6581        assert_eq!(nested.get("usage_target").map(String::as_str), Some("v2"));
6582        assert_eq!(
6583            env(&["db"]).get("usage_cmd").map(String::as_str),
6584            Some("db")
6585        );
6586        assert!(!env(&["-v"]).contains_key("usage_cmd"));
6587    }
6588
6589    #[test]
6590    fn as_env_leaves_usage_cmd_to_a_declared_cmd() {
6591        // Declared on the chosen command, and supplied: the value is the arg's.
6592        let spec: Spec = r#"
6593            cmd "run" {
6594                arg "[cmd]"
6595            }
6596            "#
6597        .parse()
6598        .unwrap();
6599        let input = ["test", "run", "build"].map(String::from);
6600        let env = parse(&spec, &input).unwrap().as_env();
6601        assert_eq!(env.get("usage_cmd").map(String::as_str), Some("build"));
6602        // Declared but left out: still the arg's, so unset rather than the path.
6603        let input = ["test", "run"].map(String::from);
6604        assert!(
6605            !parse(&spec, &input)
6606                .unwrap()
6607                .as_env()
6608                .contains_key("usage_cmd")
6609        );
6610
6611        // Declared as a global flag on an ancestor.
6612        let spec: Spec = r#"
6613            flag "--cmd <cmd>" global=#true
6614            cmd "run"
6615            "#
6616        .parse()
6617        .unwrap();
6618        let input = ["test", "run"].map(String::from);
6619        assert!(
6620            !parse(&spec, &input)
6621                .unwrap()
6622                .as_env()
6623                .contains_key("usage_cmd")
6624        );
6625
6626        // Declared inside a clause, and the clause left out.
6627        let spec: Spec = r#"
6628            cmd "run" {
6629                clause "steps" separator=":::" {
6630                    arg "[cmd]"
6631                }
6632            }
6633            "#
6634        .parse()
6635        .unwrap();
6636        let input = ["test", "run"].map(String::from);
6637        assert!(
6638            !parse(&spec, &input)
6639                .unwrap()
6640                .as_env()
6641                .contains_key("usage_cmd")
6642        );
6643    }
6644
6645    #[test]
6646    fn test_arg_env_var() {
6647        let cmd = SpecCommand::builder()
6648            .name("test")
6649            .arg(
6650                SpecArg::builder()
6651                    .name("input")
6652                    .env("TEST_ARG_INPUT")
6653                    .required(true)
6654                    .build(),
6655            )
6656            .build();
6657        let spec = Spec {
6658            name: "test".to_string(),
6659            bin: "test".to_string(),
6660            cmd,
6661            ..Default::default()
6662        };
6663
6664        // Set env var
6665        // FIXME: Audit that the environment access only happens in single-threaded code.
6666        unsafe { std::env::set_var("TEST_ARG_INPUT", "test_file.txt") };
6667
6668        let input = vec!["test".to_string()];
6669        let parsed = parse(&spec, &input).unwrap();
6670
6671        assert_eq!(parsed.args.len(), 1);
6672        let arg = parsed.args.keys().next().unwrap();
6673        assert_eq!(arg.name, "input");
6674        let value = parsed.args.values().next().unwrap();
6675        assert_eq!(value.to_string(), "test_file.txt");
6676
6677        // Clean up
6678        // FIXME: Audit that the environment access only happens in single-threaded code.
6679        unsafe { std::env::remove_var("TEST_ARG_INPUT") };
6680    }
6681
6682    #[test]
6683    fn test_flag_env_var_with_arg() {
6684        let cmd = SpecCommand::builder()
6685            .name("test")
6686            .flag(
6687                SpecFlag::builder()
6688                    .long("output")
6689                    .env("TEST_FLAG_OUTPUT")
6690                    .arg(SpecArg::builder().name("file").build())
6691                    .build(),
6692            )
6693            .build();
6694        let spec = Spec {
6695            name: "test".to_string(),
6696            bin: "test".to_string(),
6697            cmd,
6698            ..Default::default()
6699        };
6700
6701        // Set env var
6702        // FIXME: Audit that the environment access only happens in single-threaded code.
6703        unsafe { std::env::set_var("TEST_FLAG_OUTPUT", "output.txt") };
6704
6705        let input = vec!["test".to_string()];
6706        let parsed = parse(&spec, &input).unwrap();
6707
6708        assert_eq!(parsed.flags.len(), 1);
6709        let flag = parsed.flags.keys().next().unwrap();
6710        assert_eq!(flag.name, "output");
6711        let value = parsed.flags.values().next().unwrap();
6712        assert_eq!(value.to_string(), "output.txt");
6713
6714        // Clean up
6715        // FIXME: Audit that the environment access only happens in single-threaded code.
6716        unsafe { std::env::remove_var("TEST_FLAG_OUTPUT") };
6717    }
6718
6719    #[test]
6720    fn test_flag_env_var_boolean() {
6721        let cmd = SpecCommand::builder()
6722            .name("test")
6723            .flag(
6724                SpecFlag::builder()
6725                    .long("verbose")
6726                    .env("TEST_FLAG_VERBOSE")
6727                    .build(),
6728            )
6729            .build();
6730        let spec = Spec {
6731            name: "test".to_string(),
6732            bin: "test".to_string(),
6733            cmd,
6734            ..Default::default()
6735        };
6736
6737        // Set env var to true
6738        // FIXME: Audit that the environment access only happens in single-threaded code.
6739        unsafe { std::env::set_var("TEST_FLAG_VERBOSE", "true") };
6740
6741        let input = vec!["test".to_string()];
6742        let parsed = parse(&spec, &input).unwrap();
6743
6744        assert_eq!(parsed.flags.len(), 1);
6745        let flag = parsed.flags.keys().next().unwrap();
6746        assert_eq!(flag.name, "verbose");
6747        let value = parsed.flags.values().next().unwrap();
6748        assert_eq!(value.to_string(), "true");
6749
6750        // Clean up
6751        // FIXME: Audit that the environment access only happens in single-threaded code.
6752        unsafe { std::env::remove_var("TEST_FLAG_VERBOSE") };
6753    }
6754
6755    #[test]
6756    fn test_env_var_precedence() {
6757        // CLI args should take precedence over env vars
6758        let cmd = SpecCommand::builder()
6759            .name("test")
6760            .arg(
6761                SpecArg::builder()
6762                    .name("input")
6763                    .env("TEST_PRECEDENCE_INPUT")
6764                    .required(true)
6765                    .build(),
6766            )
6767            .build();
6768        let spec = Spec {
6769            name: "test".to_string(),
6770            bin: "test".to_string(),
6771            cmd,
6772            ..Default::default()
6773        };
6774
6775        // Set env var
6776        // FIXME: Audit that the environment access only happens in single-threaded code.
6777        unsafe { std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt") };
6778
6779        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6780        let parsed = parse(&spec, &input).unwrap();
6781
6782        assert_eq!(parsed.args.len(), 1);
6783        let value = parsed.args.values().next().unwrap();
6784        // CLI arg should take precedence
6785        assert_eq!(value.to_string(), "cli_file.txt");
6786
6787        // Clean up
6788        // FIXME: Audit that the environment access only happens in single-threaded code.
6789        unsafe { std::env::remove_var("TEST_PRECEDENCE_INPUT") };
6790    }
6791
6792    #[test]
6793    fn test_flag_var_true_with_single_default() {
6794        // When var=true and default="bar", the default should be MultiString(["bar"])
6795        let cmd = SpecCommand::builder()
6796            .name("test")
6797            .flag(
6798                SpecFlag::builder()
6799                    .long("foo")
6800                    .var(true)
6801                    .arg(SpecArg::builder().name("foo").build())
6802                    .default_value("bar")
6803                    .build(),
6804            )
6805            .build();
6806        let spec = Spec {
6807            name: "test".to_string(),
6808            bin: "test".to_string(),
6809            cmd,
6810            ..Default::default()
6811        };
6812
6813        // User doesn't provide the flag
6814        let input = vec!["test".to_string()];
6815        let parsed = parse(&spec, &input).unwrap();
6816
6817        assert_eq!(parsed.flags.len(), 1);
6818        let flag = parsed.flags.keys().next().unwrap();
6819        assert_eq!(flag.name, "foo");
6820        let value = parsed.flags.values().next().unwrap();
6821        // Should be MultiString, not String
6822        match value {
6823            ParseValue::MultiString(v) => {
6824                assert_eq!(v.len(), 1);
6825                assert_eq!(v[0], "bar");
6826            }
6827            _ => panic!("Expected MultiString, got {:?}", value),
6828        }
6829    }
6830
6831    #[test]
6832    fn test_flag_var_true_with_multiple_defaults() {
6833        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6834        let cmd = SpecCommand::builder()
6835            .name("test")
6836            .flag(
6837                SpecFlag::builder()
6838                    .long("foo")
6839                    .var(true)
6840                    .arg(SpecArg::builder().name("foo").build())
6841                    .default_values(["xyz", "bar"])
6842                    .build(),
6843            )
6844            .build();
6845        let spec = Spec {
6846            name: "test".to_string(),
6847            bin: "test".to_string(),
6848            cmd,
6849            ..Default::default()
6850        };
6851
6852        // User doesn't provide the flag
6853        let input = vec!["test".to_string()];
6854        let parsed = parse(&spec, &input).unwrap();
6855
6856        assert_eq!(parsed.flags.len(), 1);
6857        let value = parsed.flags.values().next().unwrap();
6858        // Should be MultiString with both values
6859        match value {
6860            ParseValue::MultiString(v) => {
6861                assert_eq!(v.len(), 2);
6862                assert_eq!(v[0], "xyz");
6863                assert_eq!(v[1], "bar");
6864            }
6865            _ => panic!("Expected MultiString, got {:?}", value),
6866        }
6867    }
6868
6869    #[test]
6870    fn test_flag_var_false_with_default_remains_string() {
6871        // When var=false (default), the default should still be String("bar")
6872        let cmd = SpecCommand::builder()
6873            .name("test")
6874            .flag(
6875                SpecFlag::builder()
6876                    .long("foo")
6877                    .var(false) // Default behavior
6878                    .arg(SpecArg::builder().name("foo").build())
6879                    .default_value("bar")
6880                    .build(),
6881            )
6882            .build();
6883        let spec = Spec {
6884            name: "test".to_string(),
6885            bin: "test".to_string(),
6886            cmd,
6887            ..Default::default()
6888        };
6889
6890        // User doesn't provide the flag
6891        let input = vec!["test".to_string()];
6892        let parsed = parse(&spec, &input).unwrap();
6893
6894        assert_eq!(parsed.flags.len(), 1);
6895        let value = parsed.flags.values().next().unwrap();
6896        // Should be String, not MultiString
6897        match value {
6898            ParseValue::String(s) => {
6899                assert_eq!(s, "bar");
6900            }
6901            _ => panic!("Expected String, got {:?}", value),
6902        }
6903    }
6904
6905    #[test]
6906    fn test_arg_var_true_with_single_default() {
6907        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6908        let cmd = SpecCommand::builder()
6909            .name("test")
6910            .arg(
6911                SpecArg::builder()
6912                    .name("files")
6913                    .var(true)
6914                    .default_value("default.txt")
6915                    .required(false)
6916                    .build(),
6917            )
6918            .build();
6919        let spec = Spec {
6920            name: "test".to_string(),
6921            bin: "test".to_string(),
6922            cmd,
6923            ..Default::default()
6924        };
6925
6926        // User doesn't provide the arg
6927        let input = vec!["test".to_string()];
6928        let parsed = parse(&spec, &input).unwrap();
6929
6930        assert_eq!(parsed.args.len(), 1);
6931        let value = parsed.args.values().next().unwrap();
6932        // Should be MultiString, not String
6933        match value {
6934            ParseValue::MultiString(v) => {
6935                assert_eq!(v.len(), 1);
6936                assert_eq!(v[0], "default.txt");
6937            }
6938            _ => panic!("Expected MultiString, got {:?}", value),
6939        }
6940    }
6941
6942    #[test]
6943    fn test_arg_var_true_with_multiple_defaults() {
6944        // When arg has var=true and multiple defaults
6945        let cmd = SpecCommand::builder()
6946            .name("test")
6947            .arg(
6948                SpecArg::builder()
6949                    .name("files")
6950                    .var(true)
6951                    .default_values(["file1.txt", "file2.txt"])
6952                    .required(false)
6953                    .build(),
6954            )
6955            .build();
6956        let spec = Spec {
6957            name: "test".to_string(),
6958            bin: "test".to_string(),
6959            cmd,
6960            ..Default::default()
6961        };
6962
6963        // User doesn't provide the arg
6964        let input = vec!["test".to_string()];
6965        let parsed = parse(&spec, &input).unwrap();
6966
6967        assert_eq!(parsed.args.len(), 1);
6968        let value = parsed.args.values().next().unwrap();
6969        // Should be MultiString with both values
6970        match value {
6971            ParseValue::MultiString(v) => {
6972                assert_eq!(v.len(), 2);
6973                assert_eq!(v[0], "file1.txt");
6974                assert_eq!(v[1], "file2.txt");
6975            }
6976            _ => panic!("Expected MultiString, got {:?}", value),
6977        }
6978    }
6979
6980    #[test]
6981    fn test_arg_var_false_with_default_remains_string() {
6982        // When arg has var=false (default), the default should still be String
6983        let cmd = SpecCommand::builder()
6984            .name("test")
6985            .arg(
6986                SpecArg::builder()
6987                    .name("file")
6988                    .var(false)
6989                    .default_value("default.txt")
6990                    .required(false)
6991                    .build(),
6992            )
6993            .build();
6994        let spec = Spec {
6995            name: "test".to_string(),
6996            bin: "test".to_string(),
6997            cmd,
6998            ..Default::default()
6999        };
7000
7001        // User doesn't provide the arg
7002        let input = vec!["test".to_string()];
7003        let parsed = parse(&spec, &input).unwrap();
7004
7005        assert_eq!(parsed.args.len(), 1);
7006        let value = parsed.args.values().next().unwrap();
7007        // Should be String, not MultiString
7008        match value {
7009            ParseValue::String(s) => {
7010                assert_eq!(s, "default.txt");
7011            }
7012            _ => panic!("Expected String, got {:?}", value),
7013        }
7014    }
7015
7016    #[test]
7017    fn test_scalar_defaults_validate_only_first_default_choice() {
7018        let specs = [
7019            spec_with_arg(
7020                SpecArg::builder()
7021                    .name("env")
7022                    .var(false)
7023                    .default_values(["dev", "prod"])
7024                    .choices(["dev"])
7025                    .required(false)
7026                    .build(),
7027            ),
7028            spec_with_flag(
7029                SpecFlag::builder()
7030                    .long("env")
7031                    .arg(
7032                        SpecArg::builder()
7033                            .name("env")
7034                            .default_values(["dev", "prod"])
7035                            .choices(["dev"])
7036                            .build(),
7037                    )
7038                    .build(),
7039            ),
7040        ];
7041
7042        for spec in specs {
7043            let parsed = parse(&spec, &input(&["test"])).unwrap();
7044            assert_eq!(first_string_value(&parsed), "dev");
7045        }
7046    }
7047
7048    #[test]
7049    fn a_delimiter_turns_one_word_into_several_values() {
7050        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
7051            .parse()
7052            .unwrap();
7053
7054        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
7055        let multi = |value: &ParseValue| match value {
7056            ParseValue::MultiString(values) => values.clone(),
7057            other => panic!("expected several values, got {other:?}"),
7058        };
7059        let tags = parsed
7060            .flags
7061            .iter()
7062            .find(|(f, _)| f.name == "tags")
7063            .map(|(_, v)| v)
7064            .unwrap();
7065        assert_eq!(multi(tags), vec!["a", "b", "c"]);
7066        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
7067    }
7068
7069    #[test]
7070    fn a_positional_splits_before_its_choices_are_asked() {
7071        // The flag path did this and the positional path did not, so a word whose parts
7072        // were all choices was rejected as one value, and a bad half was reported as the
7073        // whole word.
7074        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
7075            .parse()
7076            .unwrap();
7077
7078        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
7079
7080        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
7081        let message = err.to_string();
7082        assert!(message.contains("nowhere"), "{message}");
7083        assert!(
7084            !message.contains("src:nowhere"),
7085            "the bad half should be named, not the whole word: {message}"
7086        );
7087    }
7088
7089    #[test]
7090    fn a_split_value_is_counted_and_judged_as_values() {
7091        // Split during the parse rather than after it, so everything downstream sees the
7092        // values the user meant rather than the words they typed: `choices` judges each
7093        // one, and the bounds count them.
7094        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
7095            .parse()
7096            .unwrap();
7097
7098        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
7099        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
7100        assert!(err.to_string().contains("staging"), "{err}");
7101        assert!(
7102            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
7103            "three values should breach var_max=2"
7104        );
7105    }
7106
7107    #[test]
7108    fn a_split_bound_counts_one_occurrence_at_a_time() {
7109        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
7110        // delimiter the collection simply stops at it, so it could never be exceeded; a word
7111        // carrying several values can carry an occurrence past it in one step, and that is
7112        // the only way this bound is ever breached.
7113        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
7114            .parse()
7115            .unwrap();
7116
7117        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
7118        assert!(
7119            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
7120            "three values out of one word is still three values"
7121        );
7122        // The rule the corpus documents for plain words, on split ones: a second occurrence
7123        // starts counting again rather than adding to the first.
7124        parse(
7125            &spec,
7126            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
7127        )
7128        .expect("two per occurrence, twice, is within the bound");
7129    }
7130
7131    #[test]
7132    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
7133        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
7134            .parse()
7135            .unwrap();
7136
7137        parse(
7138            &spec,
7139            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
7140        )
7141        .expect("each occurrence satisfies the bound independently");
7142
7143        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
7144        assert!(
7145            error
7146                .to_string()
7147                .contains("requires at least 2 value(s), got 1"),
7148            "{error:?}"
7149        );
7150    }
7151
7152    #[test]
7153    fn an_exclusive_flag_has_to_be_alone() {
7154        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
7155            .parse()
7156            .unwrap();
7157
7158        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
7159
7160        // Any other flag.
7161        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
7162        assert!(err.to_string().contains("on its own"), "{err}");
7163
7164        // And a positional, which is what makes this more than a conflict with every
7165        // other flag.
7166        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
7167        assert!(err.to_string().contains("on its own"), "{err}");
7168
7169        // Not given, so it imposes nothing.
7170        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
7171    }
7172
7173    #[test]
7174    fn an_exclusive_flag_conflicts_with_clause_arguments() {
7175        let spec: Spec = r#"name "ex"
7176bin "ex"
7177flag "--dump" exclusive=#true
7178clause "tasks" separator=":::" {
7179  arg "<task>"
7180}
7181"#
7182        .parse()
7183        .unwrap();
7184
7185        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
7186        assert!(err.to_string().contains("on its own"), "{err}");
7187    }
7188
7189    #[test]
7190    fn a_partial_parse_reports_the_next_implicit_clause_argument() {
7191        let spec: Spec = r#"name "ex"
7192bin "ex"
7193clause "tools" {
7194  arg "<tool@version>"
7195}
7196"#
7197        .parse()
7198        .unwrap();
7199
7200        let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
7201
7202        assert_eq!(
7203            parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
7204            Some("tool@version")
7205        );
7206    }
7207
7208    #[test]
7209    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
7210        // Only what was supplied counts, as `conflicts` reads it. A default counting as
7211        // company would make an exclusive flag unusable on any command that has one.
7212        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
7213            .parse()
7214            .unwrap();
7215
7216        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
7217        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
7218    }
7219
7220    #[test]
7221    fn an_exclusive_flag_bypasses_required_siblings() {
7222        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
7223            .parse()
7224            .unwrap();
7225
7226        parse(&spec, &input(&["ex", "--dump"]))
7227            .expect("exclusive is the command's requiredness escape");
7228        assert!(
7229            parse(
7230                &spec,
7231                &input(&["ex", "--dump", "--out", "somewhere", "target"])
7232            )
7233            .is_err()
7234        );
7235    }
7236
7237    #[test]
7238    fn an_environment_value_counts_for_an_exclusive_flag() {
7239        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
7240            .parse()
7241            .unwrap();
7242
7243        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
7244        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
7245    }
7246
7247    #[test]
7248    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
7249        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
7250            .parse()
7251            .unwrap();
7252
7253        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
7254        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
7255    }
7256
7257    #[test]
7258    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
7259        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7260            .parse()
7261            .unwrap();
7262
7263        parse(&spec, &input(&["ex", "run", "--clean"]))
7264            .expect("the child flag is alone within the child command");
7265        assert!(
7266            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7267            "the parent flag still conflicts with selecting the child"
7268        );
7269    }
7270
7271    #[test]
7272    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
7273        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7274            .parse()
7275            .unwrap();
7276
7277        parse(&spec, &input(&["ex", "run", "--clean"]))
7278            .expect("the child-local exclusive flag is alone inside the child command");
7279        assert!(
7280            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7281            "the ancestor spelling still conflicts with selecting the child"
7282        );
7283    }
7284
7285    #[test]
7286    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
7287        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
7288            .parse()
7289            .unwrap();
7290
7291        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
7292        assert!(
7293            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
7294            "the distinct parent declaration is still company despite sharing a name"
7295        );
7296    }
7297
7298    #[test]
7299    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
7300        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7301            .parse()
7302            .unwrap();
7303
7304        parse(&spec, &input(&["ex", "run", "--clean"]))
7305            .expect("the child exclusive flag is valid alone");
7306        assert!(
7307            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7308            "merging with the inherited global must not discard child exclusivity"
7309        );
7310    }
7311
7312    #[test]
7313    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
7314        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7315            .parse()
7316            .unwrap();
7317
7318        parse(&spec, &input(&["ex", "run", "--clean"]))
7319            .expect("the typed long form belongs to the child declaration");
7320        assert!(
7321            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7322            "the inherited short form still belongs to the ancestor"
7323        );
7324        assert!(
7325            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
7326            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
7327        );
7328    }
7329
7330    #[test]
7331    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7332        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
7333            .parse()
7334            .unwrap();
7335
7336        parse(&spec, &input(&["ex", "run", "--clean"]))
7337            .expect("the child's spelling does not activate the orphan ancestor alias");
7338        assert!(
7339            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7340            "the inherited short alias still belongs to the ancestor exclusive flag"
7341        );
7342    }
7343
7344    #[test]
7345    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7346        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"
7347            .parse()
7348            .unwrap();
7349
7350        assert!(
7351            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7352            "the inherited negated alias still belongs to the ancestor exclusive flag"
7353        );
7354    }
7355
7356    #[test]
7357    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7358        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
7359        // an unrelated inherited global already owns. That collision is resolved in the other
7360        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
7361        // the `--clean` it declared, and its exclusivity holds.
7362        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"
7363            .parse()
7364            .unwrap();
7365
7366        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7367        assert!(
7368            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7369            "one unrelated alias collision cannot disown the child from its own flag"
7370        );
7371    }
7372
7373    #[test]
7374    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7375        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
7376        // subcommand word the flag can only be the ancestor's, because that is the only one in
7377        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
7378        // is set. The pair below differ in nothing else, which is what makes this one rule
7379        // rather than two behaviors.
7380        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7381            .parse()
7382            .unwrap();
7383        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7384            .expect("the ancestor owns this occurrence, and it is not exclusive");
7385        assert!(
7386            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7387            "after the subcommand word the child's declaration is in scope, and it is exclusive"
7388        );
7389
7390        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7391            .parse()
7392            .unwrap();
7393        assert!(
7394            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7395            "the same rule, with an exclusive ancestor: selecting the child is company for it"
7396        );
7397    }
7398
7399    #[test]
7400    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7401        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
7402        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
7403        // the ancestor ever declared, so the ancestor's answer still governs it.
7404        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
7405            .parse()
7406            .unwrap();
7407
7408        assert!(
7409            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7410            "the orphan ancestor alias is still the ancestor's exclusive flag"
7411        );
7412        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7413            .expect("the child's own spelling drops the exclusivity the child did not restate");
7414    }
7415
7416    #[test]
7417    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7418        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
7419        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
7420        // occurrence to the ancestor because `-c` appeared in it lost that.
7421        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7422            .parse()
7423            .unwrap();
7424
7425        assert!(
7426            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7427            "the child spelling is exclusive whatever it was typed beside"
7428        );
7429        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7430            .expect("the ancestor's own spelling was never exclusive");
7431    }
7432
7433    #[test]
7434    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7435        // An environment value has no spelling to attribute, so the declaration the selected
7436        // command has in scope answers — in both directions. Comparing whole alias sets asked
7437        // the ancestor instead, because the merged flag also carries its orphan `-c`.
7438        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"
7439            .parse()
7440            .unwrap();
7441
7442        assert!(
7443            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7444            "the child added exclusivity the environment value has to honor"
7445        );
7446
7447        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"
7448            .parse()
7449            .unwrap();
7450
7451        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7452            .expect("the child dropped the exclusivity, and the environment value follows it");
7453    }
7454
7455    #[test]
7456    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7457        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
7458        // the same reason the companion check does.
7459        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"
7460            .parse()
7461            .unwrap();
7462
7463        parse(&spec, &input(&["ex", "run", "--clean"]))
7464            .expect("a merged child exclusive flag is still the command's requiredness escape");
7465    }
7466
7467    #[test]
7468    fn a_group_allows_one_member_and_refuses_two() {
7469        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7470            .parse()
7471            .unwrap();
7472
7473        // One is fine, and so is none: a plain group says "at most one".
7474        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7475        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7476
7477        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7478        assert!(err.to_string().contains("group input"), "{err}");
7479    }
7480
7481    #[test]
7482    fn positional_selectors_work_in_conflicts_and_groups() {
7483        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7484            .parse()
7485            .unwrap();
7486        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7487            .expect("the flag alone is valid");
7488        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7489        assert!(
7490            parse(
7491                &conflicts,
7492                &input(&["ex", "--from-file", "vars.env", "literal"])
7493            )
7494            .is_err()
7495        );
7496
7497        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7498            .parse()
7499            .unwrap();
7500        assert!(
7501            parse(
7502                &positional_source,
7503                &input(&["ex", "--from-file", "vars.env", "literal"])
7504            )
7505            .is_err()
7506        );
7507
7508        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7509            .parse()
7510            .unwrap();
7511        assert!(parse(&group, &input(&["ex"])).is_err());
7512        parse(&group, &input(&["ex", "target-name"]))
7513            .expect("a positional satisfies a required group");
7514        assert!(
7515            parse(
7516                &group,
7517                &input(&["ex", "--file", "input.txt", "target-name"])
7518            )
7519            .is_err()
7520        );
7521    }
7522
7523    #[test]
7524    fn a_required_group_needs_one_of_its_members() {
7525        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7526            .parse()
7527            .unwrap();
7528
7529        let err = parse(&spec, &input(&["ex"])).unwrap_err();
7530        // The members, because that is what a user has to type; the name, because a
7531        // command with several groups would otherwise report the same sentence twice.
7532        assert!(err.to_string().contains("--file, --url"), "{err}");
7533        assert!(err.to_string().contains("input"), "{err}");
7534
7535        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7536    }
7537
7538    #[test]
7539    fn a_multiple_group_only_polices_requiredness() {
7540        // `multiple` with `required` is "at least one of these", so two is fine and
7541        // none is not.
7542        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7543            .parse()
7544            .unwrap();
7545
7546        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7547        assert!(parse(&spec, &input(&["ex"])).is_err());
7548    }
7549
7550    #[test]
7551    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7552        // The two halves of a group are two kinds of rule, and they read a default
7553        // differently on purpose. Requiredness asks whether a member has a value, and a
7554        // default is a value — the rule `requires` follows. Exclusivity asks what the
7555        // user supplied, because a defaulted member counted as supplied would collide
7556        // with the sibling they actually typed and refuse a correct command line.
7557        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7558            .parse()
7559            .unwrap();
7560
7561        parse(&spec, &input(&["ex"])).expect("the default fills the group");
7562        parse(&spec, &input(&["ex", "--url", "u"]))
7563            .expect("the default must not conflict with the flag the user typed");
7564    }
7565
7566    #[test]
7567    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7568        // `-f` and `--file` are one flag. Counted by selector, giving it once would
7569        // report it as conflicting with itself.
7570        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7571            .parse()
7572            .unwrap();
7573
7574        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7575        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7576
7577        // A genuine collision is still one.
7578        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7579        assert!(err.to_string().contains("group input"), "{err}");
7580    }
7581
7582    #[test]
7583    fn a_group_reads_the_environment_as_given() {
7584        // The environment does count, which is the same asymmetry `conflicts` has: an
7585        // env var is somebody saying something, a default is nobody saying anything.
7586        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7587            .parse()
7588            .unwrap();
7589
7590        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7591    }
7592
7593    #[test]
7594    fn a_requirement_names_the_flag_that_is_missing() {
7595        // Reported as the missing flag rather than as something wrong with `--out`,
7596        // which is what clap says for an unmet `requires` and what a user can act on.
7597        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7598            .parse()
7599            .unwrap();
7600
7601        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7602        assert!(
7603            err.to_string().contains("format"),
7604            "the missing flag should be named: {err}"
7605        );
7606
7607        // Satisfied, in either order.
7608        for words in [
7609            &["ex", "--out", "a.txt", "--format", "json"][..],
7610            &["ex", "--format", "json", "--out", "a.txt"][..],
7611        ] {
7612            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7613        }
7614
7615        // Nothing happens when the flag that imposes the rule is absent: a requirement
7616        // is a consequence of using the flag, not a rule about the command line.
7617        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7618    }
7619
7620    #[test]
7621    fn a_value_activates_only_its_conditional_requirement() {
7622        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"
7623            .parse()
7624            .unwrap();
7625
7626        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7627            .expect("an unrelated value requires nothing");
7628
7629        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7630        assert!(key.to_string().contains("key"), "{key}");
7631        parse(
7632            &spec,
7633            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7634        )
7635        .expect("the matching requirement is satisfied");
7636
7637        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7638        assert!(token.to_string().contains("token"), "{token}");
7639    }
7640
7641    #[test]
7642    fn conditional_requirements_read_explicit_env_but_not_defaults() {
7643        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"
7644            .parse()
7645            .unwrap();
7646        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7647        assert!(err.to_string().contains("key"), "{err}");
7648
7649        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"
7650            .parse()
7651            .unwrap();
7652        parse(&from_default, &input(&["ex"]))
7653            .expect("a default is not an explicit conditional value");
7654    }
7655
7656    #[test]
7657    fn command_line_values_override_env_for_conditional_requirements() {
7658        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7659            .parse()
7660            .unwrap();
7661
7662        parse_with_env(
7663            &spec,
7664            &["ex", "--config", "ordinary.toml"],
7665            &[("EX_CONFIG", "special.toml")],
7666        )
7667        .expect("the command-line value takes precedence over the environment");
7668    }
7669
7670    #[test]
7671    fn conditional_requirements_normalize_boolean_env_values() {
7672        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7673            .parse()
7674            .unwrap();
7675
7676        for value in ["1", "true", "True", "TRUE"] {
7677            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7678            assert!(err.to_string().contains("key"), "{value}: {err}");
7679        }
7680        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7681            .expect("a false environment value does not activate a true condition");
7682    }
7683
7684    #[test]
7685    fn a_default_satisfies_a_requirement() {
7686        // The flag it names has a value, which is the question a requirement asks. Read
7687        // any other way, `--format` would be missing here and present ten lines further
7688        // down, where plain required-ness reads the same default as filling it.
7689        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7690            .parse()
7691            .unwrap();
7692
7693        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7694            .expect("a defaulted flag is not a missing one");
7695    }
7696
7697    #[test]
7698    fn a_present_flag_binds_a_conditional_default() {
7699        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7700            .parse()
7701            .unwrap();
7702
7703        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7704        assert_eq!(
7705            with.as_env().get("usage_bin_names").map(String::as_str),
7706            Some("true")
7707        );
7708
7709        let without = parse(&spec, &input(&["ex"])).unwrap();
7710        assert!(
7711            !without.as_env().contains_key("usage_bin_names"),
7712            "IsPresent does nothing when the selector is absent"
7713        );
7714    }
7715
7716    #[test]
7717    fn an_equals_condition_binds_a_conditional_default() {
7718        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7719            .parse()
7720            .unwrap();
7721
7722        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7723        assert_eq!(
7724            json.as_env().get("usage_style").map(String::as_str),
7725            Some("pretty")
7726        );
7727        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7728        assert!(!yaml.as_env().contains_key("usage_style"));
7729    }
7730
7731    #[test]
7732    fn an_equals_condition_reads_a_negated_flag() {
7733        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7734            .parse()
7735            .unwrap();
7736
7737        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7738        assert_eq!(
7739            off.as_env().get("usage_pretty").map(String::as_str),
7740            Some("true")
7741        );
7742        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7743        assert!(
7744            !on.as_env().contains_key("usage_pretty"),
7745            "--json is true, so when=false should miss"
7746        );
7747    }
7748
7749    #[test]
7750    fn the_first_matching_conditional_default_wins() {
7751        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"
7752            .parse()
7753            .unwrap();
7754
7755        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7756        assert_eq!(
7757            out.as_env().get("usage_style").map(String::as_str),
7758            Some("compact")
7759        );
7760    }
7761
7762    #[test]
7763    fn argv_and_env_suppress_a_conditional_default() {
7764        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7765            .parse()
7766            .unwrap();
7767
7768        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7769        assert_eq!(
7770            from_env.as_env().get("usage_bin_names").map(String::as_str),
7771            Some("false"),
7772            "the target's environment wins over default_if"
7773        );
7774    }
7775
7776    #[test]
7777    fn a_sibling_env_activates_a_conditional_default() {
7778        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7779            .parse()
7780            .unwrap();
7781
7782        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7783        assert_eq!(
7784            out.as_env().get("usage_bin_names").map(String::as_str),
7785            Some("true")
7786        );
7787    }
7788
7789    #[test]
7790    fn a_default_does_not_activate_a_conditional_default() {
7791        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7792        // bind would put json's default into `out.flags` and then treat it as
7793        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7794        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7795            .parse()
7796            .unwrap();
7797
7798        let out = parse(&spec, &input(&["ex"])).unwrap();
7799        assert_eq!(
7800            out.as_env().get("usage_json").map(String::as_str),
7801            Some("true")
7802        );
7803        assert!(
7804            !out.as_env().contains_key("usage_pretty"),
7805            "a default is not an explicit value for default_if"
7806        );
7807    }
7808
7809    #[test]
7810    fn a_conditional_default_does_not_activate_requires_if() {
7811        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"
7812            .parse()
7813            .unwrap();
7814
7815        parse(&spec, &input(&["ex", "--json"]))
7816            .expect("a default_if value is not explicit for requires_if");
7817        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7818    }
7819
7820    #[test]
7821    fn a_conditional_default_satisfies_a_requirement() {
7822        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7823            .parse()
7824            .unwrap();
7825
7826        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7827            .expect("default_if fills the required flag");
7828        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7829    }
7830
7831    #[test]
7832    fn an_environment_value_satisfies_a_requirement() {
7833        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7834            .parse()
7835            .unwrap();
7836
7837        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7838        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7839            .expect("the environment supplies it");
7840    }
7841
7842    #[test]
7843    fn a_requirement_is_satisfied_by_a_short_form() {
7844        // The selector may spell the other flag any way it answers to, so the check
7845        // resolves it the way every other selector is resolved rather than matching
7846        // text. The error names the flag, not the selector.
7847        let spec: Spec =
7848            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7849                .parse()
7850                .unwrap();
7851
7852        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7853
7854        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7855        assert!(err.to_string().contains("key"), "{err}");
7856    }
7857
7858    #[test]
7859    fn conflicting_flags_are_rejected_in_either_order() {
7860        // Declared once, on `--file`, which is all clap exposes — so the check has to
7861        // be order-independent by looking at every flag that was given rather than at
7862        // the one that declared the conflict.
7863        let spec: Spec =
7864            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7865                .parse()
7866                .unwrap();
7867
7868        for words in [
7869            &["ex", "--file", "a.txt", "--stdin"][..],
7870            &["ex", "--stdin", "--file", "a.txt"][..],
7871        ] {
7872            let err = parse(&spec, &input(words)).unwrap_err();
7873            assert!(
7874                err.to_string().contains("conflicts with --stdin"),
7875                "{words:?} should be refused: {err}"
7876            );
7877        }
7878
7879        // Either one alone is fine.
7880        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7881        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7882    }
7883
7884    #[test]
7885    fn unknown_flags_are_values_by_default() {
7886        // The default, and the reason it is the default: a spec often parses a
7887        // command line whose flags belong to something else.
7888        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7889            .parse()
7890            .unwrap();
7891        let out = parse(
7892            &spec,
7893            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7894        )
7895        .unwrap();
7896        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7897        assert_eq!(out.args[rest].to_string(), "--wat x");
7898    }
7899
7900    #[test]
7901    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7902        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7903            .parse()
7904            .unwrap();
7905        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7906            .expect("a repeat is a correction by default");
7907        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7908        assert_eq!(out.flags[jobs].to_string(), "2");
7909        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7910            .expect("switches use the same default");
7911
7912        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7913            .parse()
7914            .unwrap();
7915        for words in [
7916            &["ex", "--jobs", "1", "--jobs", "2"][..],
7917            &["ex", "--verbose", "--verbose"][..],
7918        ] {
7919            let err = parse(&strict, &input(words)).unwrap_err();
7920            assert!(
7921                err.to_string().contains("cannot be used multiple times"),
7922                "{err}"
7923            );
7924        }
7925
7926        let reparsed: Spec = strict.to_string().parse().unwrap();
7927        assert!(!reparsed.cmd.args_override_self);
7928    }
7929
7930    #[test]
7931    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7932        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7933            .parse()
7934            .unwrap();
7935
7936        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7937            .expect("opposite forms override each other");
7938        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7939        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7940
7941        for words in [
7942            &["ex", "--color", "--color"][..],
7943            &["ex", "--no-color", "--no-color"][..],
7944        ] {
7945            let err = parse(&spec, &input(words)).unwrap_err();
7946            assert!(err.to_string().contains("cannot be used multiple times"));
7947        }
7948    }
7949
7950    #[test]
7951    fn strict_global_flags_may_repeat_across_command_levels() {
7952        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"
7953            .parse()
7954            .unwrap();
7955
7956        let out = parse(
7957            &spec,
7958            &input(&[
7959                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7960            ]),
7961        )
7962        .expect("an inherited global is allowed once at each command level");
7963        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7964        assert_eq!(out.flags[jobs].to_string(), "2");
7965
7966        for words in [
7967            &["ex", "--color", "--color", "run", "--no-color"][..],
7968            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7969        ] {
7970            let err = parse(&spec, &input(words)).unwrap_err();
7971            assert!(err.to_string().contains("cannot be used multiple times"));
7972        }
7973    }
7974
7975    #[test]
7976    fn a_subcommand_can_negate_only_its_parents_requirements() {
7977        let base = r#"name "ex"
7978bin "ex"
7979flag "--config" required=#true
7980flag "--mode" requires="--config"
7981flag "--other"
7982arg "<input>"
7983group "source" "--config" "--other" required=#true
7984cmd "run" { flag "--child" required=#true }
7985"#;
7986        let strict: Spec = base.parse().unwrap();
7987        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7988        let message = err.to_string();
7989        assert!(
7990            message.contains("input") || message.contains("config"),
7991            "{message}"
7992        );
7993
7994        let negated: Spec = base
7995            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7996            .parse()
7997            .unwrap();
7998        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7999        assert!(
8000            err.to_string().contains("child"),
8001            "the selected command keeps its own requirements: {err}"
8002        );
8003
8004        let mut child_optional = negated.clone();
8005        child_optional.cmd.subcommands["run"].flags[0].required = false;
8006        parse(&child_optional, &input(&["ex", "run"]))
8007            .expect("the child selection satisfies all parent requirements");
8008        parse(&child_optional, &input(&["ex", "--mode", "run"]))
8009            .expect("parent requires relationships are negated too");
8010    }
8011
8012    #[test]
8013    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
8014        let spec: Spec = r#"name "ex"
8015bin "ex"
8016args_conflicts_with_subcommands #true
8017flag "--verbose"
8018cmd "run"
8019"#
8020        .parse()
8021        .unwrap();
8022
8023        parse(&spec, &input(&["ex", "run"]))
8024            .expect("the subcommand is valid without a parent argument");
8025        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
8026        assert!(
8027            err.to_string().contains("cannot be used with arguments"),
8028            "{err}"
8029        );
8030    }
8031
8032    #[test]
8033    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
8034        let base = r#"name "ex"
8035bin "ex"
8036flag "--values <value>..."
8037cmd "run"
8038"#;
8039        let plain: Spec = base.parse().unwrap();
8040        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
8041        assert_eq!(out.cmd.name, "ex");
8042
8043        let precedence: Spec = base
8044            .replacen(
8045                "bin \"ex\"",
8046                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
8047                1,
8048            )
8049            .parse()
8050            .unwrap();
8051        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
8052        assert_eq!(out.cmd.name, "run");
8053    }
8054
8055    #[test]
8056    fn a_required_positional_can_follow_an_unfilled_optional_one() {
8057        let base = r#"name "ex"
8058bin "ex"
8059arg "[optional]"
8060arg "<required>"
8061"#;
8062        let plain: Spec = base.parse().unwrap();
8063        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
8064        assert!(err.to_string().contains("required"), "{err}");
8065
8066        let enabled: Spec = base
8067            .replacen(
8068                "bin \"ex\"",
8069                "bin \"ex\"\nallow_missing_positional #true",
8070                1,
8071            )
8072            .parse()
8073            .unwrap();
8074        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
8075        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8076        let value = &out
8077            .args
8078            .iter()
8079            .find(|(arg, _)| arg.name == "required")
8080            .unwrap()
8081            .1;
8082        assert!(matches!(value, ParseValue::String(value) if value == "value"));
8083    }
8084
8085    #[test]
8086    fn sigil_args_do_not_block_optional_positional_skipping() {
8087        let spec: Spec = r#"name "ex"
8088bin "ex"
8089allow_missing_positional #true
8090arg "[optional]"
8091arg "[tool]" sigil="@"
8092arg "<required>"
8093"#
8094        .parse()
8095        .unwrap();
8096
8097        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
8098        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8099        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
8100        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
8101        assert_eq!(out.args[tool].to_string(), "node");
8102        assert_eq!(out.args[required].to_string(), "value");
8103    }
8104
8105    #[test]
8106    fn unknown_env_refuses_every_undeclared_variable_under_the_prefix() {
8107        let spec: Spec = r#"name "ex"
8108bin "ex"
8109unknown_env prefix="EX_" {
8110    allow "EX_LIBRARY"
8111}
8112flag "--port <port>" env="EX_PORT"
8113cmd "serve" {
8114    flag "--tls" deprecated_env="EX_SSL"
8115}
8116cmd "publish"
8117"#
8118        .parse()
8119        .unwrap();
8120        let env = |names: &[&str]| -> HashMap<String, String> {
8121            names.iter().map(|n| (n.to_string(), "1".into())).collect()
8122        };
8123        let argv = input(&["ex", "publish"]);
8124
8125        // A variable for `serve` is known while running `publish`, and a deprecated name
8126        // keeps its deprecation warning instead of becoming an error.
8127        let out = Parser::new(&spec)
8128            .with_env(env(&["EX_PORT", "EX_SSL", "EX_LIBRARY", "OTHER"]))
8129            .parse(&argv)
8130            .unwrap();
8131        assert!(out.errors.is_empty());
8132
8133        let err = Parser::new(&spec)
8134            .with_env(env(&["EX_PROT", "EX_PORT", "EX_TSL"]))
8135            .parse(&argv)
8136            .unwrap_err()
8137            .to_string();
8138        assert_eq!(
8139            err,
8140            "Unknown environment variables under EX_: EX_PROT, EX_TSL"
8141        );
8142
8143        // Help is still help: the user asking how to fix the typo should get the answer.
8144        let err = Parser::new(&spec)
8145            .with_env(env(&["EX_PROT"]))
8146            .parse(&input(&["ex", "--help"]))
8147            .unwrap_err()
8148            .to_string();
8149        assert!(!err.contains("EX_PROT"), "{err}");
8150        assert!(err.contains("Usage:"), "{err}");
8151    }
8152
8153    #[test]
8154    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
8155        let spec: Spec =
8156            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
8157                .parse()
8158                .unwrap();
8159        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
8160        assert!(
8161            err.to_string().contains("--wat"),
8162            "the message should name the token: {err}"
8163        );
8164
8165        // The positional opts into the narrower negative-number carve-out without
8166        // accepting arbitrary unknown flags.
8167        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
8168        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8169        assert_eq!(out.args[rest].to_string(), "-1");
8170
8171        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
8172        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8173        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8174    }
8175
8176    #[test]
8177    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
8178        let spec: Spec = r#"
8179name "ex"
8180bin "ex"
8181unknown_flags "error"
8182flag "-0 --print0" global=#true
8183cmd "run" {
8184  flag "--force"
8185}
8186"#
8187        .parse()
8188        .unwrap();
8189        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
8190        assert_eq!(out.cmd.name, "run");
8191        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8192        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
8193        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8194        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
8195    }
8196
8197    #[test]
8198    fn a_command_may_override_the_cli_wide_setting() {
8199        // Strict overall, lenient for the one command that forwards options.
8200        let spec: Spec = r#"
8201name "ex"
8202bin "ex"
8203unknown_flags "error"
8204cmd "exec" unknown_flags="value" {
8205  arg "[rest]..."
8206}
8207cmd "build" {
8208  arg "[rest]..."
8209}
8210"#
8211        .parse()
8212        .unwrap();
8213
8214        let out = parse(
8215            &spec,
8216            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
8217        )
8218        .unwrap();
8219        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8220        assert_eq!(out.args[rest].to_string(), "--wat");
8221
8222        assert!(
8223            parse(
8224                &spec,
8225                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
8226            )
8227            .is_err(),
8228            "a command that says nothing inherits the CLI's choice"
8229        );
8230    }
8231
8232    #[test]
8233    fn the_setting_survives_a_round_trip() {
8234        let spec: Spec =
8235            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
8236                .parse()
8237                .unwrap();
8238        let reparsed: Spec = spec.to_string().parse().unwrap();
8239        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
8240        assert_eq!(
8241            reparsed.cmd.subcommands["x"].unknown_flags,
8242            Some(UnknownFlags::Value)
8243        );
8244    }
8245
8246    #[test]
8247    fn test_default_subcommand() {
8248        // Test that default_subcommand routes to the specified subcommand
8249        let run_cmd = SpecCommand::builder()
8250            .name("run")
8251            .arg(SpecArg::builder().name("task").build())
8252            .build();
8253        let mut cmd = SpecCommand::builder().name("test").build();
8254        cmd.subcommands.insert("run".to_string(), run_cmd);
8255
8256        let spec = Spec {
8257            name: "test".to_string(),
8258            bin: "test".to_string(),
8259            cmd,
8260            default_subcommand: Some("run".to_string()),
8261            ..Default::default()
8262        };
8263
8264        // "test mytask" should be parsed as if it were "test run mytask"
8265        let input = vec!["test".to_string(), "mytask".to_string()];
8266        let parsed = parse(&spec, &input).unwrap();
8267
8268        // Should have two commands: root and "run"
8269        assert_eq!(parsed.cmds.len(), 2);
8270        assert_eq!(parsed.cmds[1].name, "run");
8271
8272        // Should have parsed the task argument
8273        assert_eq!(parsed.args.len(), 1);
8274        let arg = parsed.args.keys().next().unwrap();
8275        assert_eq!(arg.name, "task");
8276        let value = parsed.args.values().next().unwrap();
8277        assert_eq!(value.to_string(), "mytask");
8278    }
8279
8280    #[test]
8281    fn default_subcommand_outranks_root_sigil_arg() {
8282        let spec: Spec = r#"
8283name "test"
8284bin "test"
8285default_subcommand "run"
8286arg "[tools]..." sigil="+"
8287cmd "run" { arg "<task>" }
8288"#
8289        .parse()
8290        .unwrap();
8291
8292        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
8293        assert_eq!(parsed.cmd.name, "run");
8294        let value = |name| {
8295            parsed
8296                .args
8297                .iter()
8298                .find(|(arg, _)| arg.name == name)
8299                .map(|(_, value)| value.to_string())
8300                .unwrap()
8301        };
8302        assert_eq!(value("tools"), "node");
8303        assert_eq!(value("task"), "node");
8304    }
8305
8306    #[test]
8307    fn test_default_subcommand_explicit_still_works() {
8308        // Test that explicit subcommand takes precedence
8309        let run_cmd = SpecCommand::builder()
8310            .name("run")
8311            .arg(SpecArg::builder().name("task").build())
8312            .build();
8313        let other_cmd = SpecCommand::builder()
8314            .name("other")
8315            .arg(SpecArg::builder().name("other_arg").build())
8316            .build();
8317        let mut cmd = SpecCommand::builder().name("test").build();
8318        cmd.subcommands.insert("run".to_string(), run_cmd);
8319        cmd.subcommands.insert("other".to_string(), other_cmd);
8320
8321        let spec = Spec {
8322            name: "test".to_string(),
8323            bin: "test".to_string(),
8324            cmd,
8325            default_subcommand: Some("run".to_string()),
8326            ..Default::default()
8327        };
8328
8329        // "test other foo" should use "other" subcommand, not default
8330        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
8331        let parsed = parse(&spec, &input).unwrap();
8332
8333        // Should have used "other" subcommand
8334        assert_eq!(parsed.cmds.len(), 2);
8335        assert_eq!(parsed.cmds[1].name, "other");
8336    }
8337
8338    #[test]
8339    fn test_default_subcommand_applies_only_at_the_root() {
8340        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
8341        // was being looked up wherever the parser happened to be standing, so a command with
8342        // an unrelated subcommand of the same name acquired a default of its own: with
8343        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
8344        // `zzz` there. Nothing declared that, and nothing could have.
8345        let mut config_ls = SpecCommand::builder().name("ls").build();
8346        config_ls.args.push(SpecArg::builder().name("what").build());
8347        let mut config_cmd = SpecCommand::builder().name("config").build();
8348        config_cmd.subcommands.insert("ls".to_string(), config_ls);
8349
8350        // The root's own `ls`, which is what its default points at. It takes an argument so
8351        // that a routed word has somewhere to land.
8352        let mut root_ls = SpecCommand::builder().name("ls").build();
8353        root_ls.args.push(SpecArg::builder().name("what").build());
8354        let mut cmd = SpecCommand::builder().name("ex").build();
8355        cmd.subcommands.insert("ls".to_string(), root_ls);
8356        cmd.subcommands.insert("config".to_string(), config_cmd);
8357
8358        let spec = Spec {
8359            name: "ex".to_string(),
8360            bin: "ex".to_string(),
8361            cmd,
8362            default_subcommand: Some("ls".to_string()),
8363            ..Default::default()
8364        };
8365
8366        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
8367        // own business — and `config` takes no argument, so this is an error rather than a
8368        // silent descent.
8369        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
8370        assert!(
8371            parse(&spec, &input).is_err(),
8372            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
8373        );
8374
8375        // At the root, where it is declared, it still applies.
8376        let input = vec!["ex".to_string(), "zzz".to_string()];
8377        let parsed = parse(&spec, &input).expect("the root's default applies");
8378        assert_eq!(
8379            parsed
8380                .cmds
8381                .iter()
8382                .map(|c| c.name.as_str())
8383                .collect::<Vec<_>>(),
8384            ["ex", "ls"]
8385        );
8386        assert_eq!(
8387            parsed.args.values().next().map(|v| v.to_string()),
8388            Some("zzz".to_string()),
8389            "and the word binds inside the command it reached"
8390        );
8391    }
8392
8393    #[test]
8394    fn test_default_subcommand_with_nested_subcommands() {
8395        // Test that default_subcommand works when the default subcommand has nested subcommands.
8396        // This is the mise use case: "mise say" should be parsed as "mise run say"
8397        // where "say" is a subcommand of "run" (a task).
8398        let say_cmd = SpecCommand::builder()
8399            .name("say")
8400            .arg(SpecArg::builder().name("name").build())
8401            .build();
8402        let mut run_cmd = SpecCommand::builder().name("run").build();
8403        run_cmd.subcommands.insert("say".to_string(), say_cmd);
8404
8405        let mut cmd = SpecCommand::builder().name("test").build();
8406        cmd.subcommands.insert("run".to_string(), run_cmd);
8407
8408        let spec = Spec {
8409            name: "test".to_string(),
8410            bin: "test".to_string(),
8411            cmd,
8412            default_subcommand: Some("run".to_string()),
8413            ..Default::default()
8414        };
8415
8416        // "test say hello" should be parsed as "test run say hello"
8417        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8418        let parsed = parse(&spec, &input).unwrap();
8419
8420        // Should have three commands: root, "run", and "say"
8421        assert_eq!(parsed.cmds.len(), 3);
8422        assert_eq!(parsed.cmds[0].name, "test");
8423        assert_eq!(parsed.cmds[1].name, "run");
8424        assert_eq!(parsed.cmds[2].name, "say");
8425
8426        // Should have parsed the "name" argument
8427        assert_eq!(parsed.args.len(), 1);
8428        let arg = parsed.args.keys().next().unwrap();
8429        assert_eq!(arg.name, "name");
8430        let value = parsed.args.values().next().unwrap();
8431        assert_eq!(value.to_string(), "hello");
8432    }
8433
8434    /// Build a spec equivalent to the post-mount structure produced by mise's
8435    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
8436    /// subcommand that re-declares the same flag as NON-global, and a mounted task
8437    /// (`sample:run`) carrying a positional arg with `choices`.
8438    ///
8439    /// We construct the merged structure directly instead of executing a real mount so the
8440    /// test stays hermetic and cross-platform while still exercising the parser defect.
8441    fn mounted_global_flag_spec() -> Spec {
8442        let task_cmd = SpecCommand::builder()
8443            .name("sample:run")
8444            .arg(
8445                SpecArg::builder()
8446                    .name("profile")
8447                    .choices(["alpha", "beta", "gamma"])
8448                    .build(),
8449            )
8450            .build();
8451        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
8452        let mut run_cmd = SpecCommand::builder()
8453            .name("run")
8454            .flag(
8455                SpecFlag::builder()
8456                    .name("cd")
8457                    .short('C')
8458                    .long("cd")
8459                    .arg(SpecArg::builder().name("dir").build())
8460                    .global(false)
8461                    .build(),
8462            )
8463            .build();
8464        run_cmd
8465            .subcommands
8466            .insert("sample:run".to_string(), task_cmd);
8467
8468        let mut cmd = SpecCommand::builder()
8469            .name("test")
8470            .flag(
8471                SpecFlag::builder()
8472                    .name("cd")
8473                    .short('C')
8474                    .long("cd")
8475                    .arg(SpecArg::builder().name("dir").build())
8476                    .global(true)
8477                    .build(),
8478            )
8479            .build();
8480        cmd.subcommands.insert("run".to_string(), run_cmd);
8481
8482        Spec {
8483            name: "test".to_string(),
8484            bin: "test".to_string(),
8485            cmd,
8486            ..Default::default()
8487        }
8488    }
8489
8490    #[test]
8491    fn test_prefix_global_flag_does_not_pollute_choices() {
8492        // Regression for the parser-side root cause referenced by jdx/mise#10069.
8493        //
8494        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
8495        // then into the mounted `sample:run`) used to drop the inherited global flag from
8496        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
8497        // mis-validated against the task's `choices` positional arg.
8498        let spec = mounted_global_flag_spec();
8499
8500        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
8501        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
8502        for words in [
8503            &["test", "-C", "/tmp", "run", "sample:run"][..],
8504            // Embedded-value form must behave identically.
8505            &["test", "--cd=/tmp", "run", "sample:run"][..],
8506        ] {
8507            let parsed = parse_partial(&spec, &input(words)).unwrap();
8508            assert_eq!(
8509                parsed
8510                    .cmds
8511                    .iter()
8512                    .map(|c| c.name.as_str())
8513                    .collect::<Vec<_>>(),
8514                vec!["test", "run", "sample:run"],
8515            );
8516            // No positional arg should have been consumed by the leftover global-flag tokens.
8517            assert!(
8518                parsed.args.is_empty(),
8519                "args should be empty, got {:?}",
8520                parsed.args
8521            );
8522
8523            // Fix (B): the inherited global flag survives the descent even though `run`
8524            // re-declares `-C/--cd` as non-global.
8525            let cd = parsed
8526                .available_flags
8527                .get("--cd")
8528                .expect("--cd should remain available after descending into the subcommand");
8529            assert!(cd.global, "--cd must stay global after descent");
8530            assert!(
8531                parsed.available_flags.get("-C").is_some_and(|f| f.global),
8532                "-C must stay global after descent",
8533            );
8534
8535            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
8536            // for normal execution and for the env passed to mount scripts. (Removing the
8537            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
8538            assert_eq!(
8539                parsed.as_env().get("usage_cd").map(String::as_str),
8540                Some("/tmp"),
8541                "global flag value must survive in as_env(), got {:?}",
8542                parsed.as_env(),
8543            );
8544        }
8545
8546        // A real, valid choice still parses through the global flag prefix.
8547        let parsed = parse_partial(
8548            &spec,
8549            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8550        )
8551        .unwrap();
8552        assert_eq!(parsed.args.len(), 1);
8553        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8554
8555        // And genuinely invalid choices are still rejected (we didn't disable validation).
8556        assert_parse_err(
8557            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8558            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8559        );
8560    }
8561
8562    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
8563    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
8564    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
8565    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
8566    fn mounted_orphan_short_spec() -> Spec {
8567        let task_cmd = SpecCommand::builder()
8568            .name("sample:run")
8569            .arg(
8570                SpecArg::builder()
8571                    .name("profile")
8572                    .choices(["alpha", "beta", "gamma"])
8573                    .build(),
8574            )
8575            .build();
8576        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
8577        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
8578        let mut run_cmd = SpecCommand::builder()
8579            .name("run")
8580            .flag(
8581                SpecFlag::builder()
8582                    .name("raw")
8583                    .short('r')
8584                    .long("raw")
8585                    .global(false)
8586                    .build(),
8587            )
8588            .flag(
8589                SpecFlag::builder()
8590                    .name("force")
8591                    .short('f')
8592                    .long("force")
8593                    .global(false)
8594                    .build(),
8595            )
8596            .build();
8597        run_cmd
8598            .subcommands
8599            .insert("sample:run".to_string(), task_cmd);
8600
8601        // Root global is LONG-ONLY: `--raw` with no short.
8602        let mut cmd = SpecCommand::builder()
8603            .name("test")
8604            .flag(
8605                SpecFlag::builder()
8606                    .name("raw")
8607                    .long("raw")
8608                    .global(true)
8609                    .build(),
8610            )
8611            .build();
8612        cmd.subcommands.insert("run".to_string(), run_cmd);
8613
8614        Spec {
8615            name: "test".to_string(),
8616            bin: "test".to_string(),
8617            cmd,
8618            ..Default::default()
8619        }
8620    }
8621
8622    #[test]
8623    fn test_orphan_short_alias_survives_merge() {
8624        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
8625        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
8626        // added short `-r` must be unioned onto the surviving inherited global flag instead of
8627        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
8628        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
8629        let spec = mounted_orphan_short_spec();
8630
8631        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8632        assert_eq!(
8633            parsed
8634                .cmds
8635                .iter()
8636                .map(|c| c.name.as_str())
8637                .collect::<Vec<_>>(),
8638            vec!["test", "run", "sample:run"],
8639        );
8640
8641        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
8642        // and the original long `--raw` is still global too.
8643        assert!(
8644            parsed.available_flags.get("-r").is_some_and(|f| f.global),
8645            "-r must be merged onto the inherited global flag and stay global after descent",
8646        );
8647        assert!(
8648            parsed
8649                .available_flags
8650                .get("--raw")
8651                .is_some_and(|f| f.global),
8652            "--raw must stay global after descent",
8653        );
8654
8655        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
8656        assert!(
8657            parsed.args.is_empty(),
8658            "args should be empty, got {:?}",
8659            parsed.args
8660        );
8661
8662        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
8663        assert_eq!(
8664            parsed.as_env().get("usage_raw").map(String::as_str),
8665            Some("true"),
8666            "merged short's value must survive in as_env(), got {:?}",
8667            parsed.as_env(),
8668        );
8669
8670        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
8671        // promoted/merged — it is correctly dropped when descending into the mount.
8672        assert!(
8673            !parsed.available_flags.contains_key("-f"),
8674            "purely-local -f must not be promoted onto a global",
8675        );
8676        assert!(
8677            !parsed.available_flags.contains_key("--force"),
8678            "purely-local --force must not be promoted onto a global",
8679        );
8680
8681        // A real, valid choice still parses through the merged short prefix.
8682        let parsed =
8683            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8684        assert_eq!(parsed.args.len(), 1);
8685        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8686
8687        // And genuinely invalid choices are still rejected.
8688        assert_parse_err(
8689            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8690            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8691        );
8692    }
8693
8694    #[test]
8695    fn test_orphan_short_does_not_clobber_unrelated_global() {
8696        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
8697        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
8698        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
8699        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
8700        let run_cmd = SpecCommand::builder()
8701            .name("run")
8702            .flag(
8703                SpecFlag::builder()
8704                    .name("raw")
8705                    .short('r')
8706                    .long("raw")
8707                    .global(false)
8708                    .build(),
8709            )
8710            .build();
8711        let mut cmd = SpecCommand::builder()
8712            .name("test")
8713            .flag(
8714                SpecFlag::builder()
8715                    .name("raw")
8716                    .long("raw")
8717                    .global(true)
8718                    .build(),
8719            )
8720            .flag(
8721                SpecFlag::builder()
8722                    .name("restrict")
8723                    .short('r')
8724                    .long("restrict")
8725                    .global(true)
8726                    .build(),
8727            )
8728            .build();
8729        cmd.subcommands.insert("run".to_string(), run_cmd);
8730        let spec = Spec {
8731            name: "test".to_string(),
8732            bin: "test".to_string(),
8733            cmd,
8734            ..Default::default()
8735        };
8736
8737        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8738        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8739        assert_eq!(
8740            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8741            Some("restrict"),
8742            "-r must remain owned by the unrelated global it already belonged to",
8743        );
8744        // Both globals are still recognized and global after the descent.
8745        assert!(
8746            parsed
8747                .available_flags
8748                .get("--raw")
8749                .is_some_and(|f| f.global)
8750        );
8751        assert!(
8752            parsed
8753                .available_flags
8754                .get("--restrict")
8755                .is_some_and(|f| f.global)
8756        );
8757    }
8758
8759    #[test]
8760    fn test_redeclared_global_aliases_share_one_flag() {
8761        // A global declared with BOTH a short and a long, re-declared non-globally by a
8762        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8763        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8764        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8765        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8766        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8767        let spec = r#"
8768flag "-y --yes" global=#true effect="write"
8769cmd "run" {
8770    flag "-y --yes --assume-yes"
8771}
8772"#
8773        .parse::<Spec>()
8774        .unwrap();
8775
8776        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8777
8778        for key in ["-y", "--yes", "--assume-yes"] {
8779            let flag = parsed
8780                .available_flags
8781                .get(key)
8782                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8783            assert!(flag.global, "{key} must stay global after the descent");
8784            assert_eq!(
8785                flag.long,
8786                vec!["yes".to_string(), "assume-yes".to_string()],
8787                "{key} must resolve to the flag carrying every alias",
8788            );
8789            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8790        }
8791
8792        // One logical flag means one object: all three keys share a single `Arc`.
8793        assert_eq!(
8794            unique_flags(parsed.available_flags.values()).count(),
8795            1,
8796            "all aliases must point at one flag object, got {:?}",
8797            parsed.available_flags,
8798        );
8799
8800        // The global's effect survives the merge, so `-y` still marks the command as writing.
8801        assert_eq!(
8802            parsed.available_flags["-y"].effect,
8803            Some(crate::SpecCommandEffect::Write),
8804        );
8805    }
8806
8807    #[test]
8808    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8809        let spec = r#"
8810flag "--yes" global=#true {
8811    alias "-q" "--quietly" hide=#true
8812}
8813cmd "run" {
8814    flag "--yes --assume-yes" {
8815        alias "-s" "--secret" hide=#true
8816    }
8817}
8818"#
8819        .parse::<Spec>()
8820        .unwrap();
8821
8822        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8823        let merged = &parsed.available_flags["--yes"];
8824        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8825        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8826        for key in ["-q", "-s", "--quietly", "--secret"] {
8827            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8828        }
8829    }
8830
8831    #[test]
8832    fn test_redeclared_global_can_promote_hidden_aliases() {
8833        let spec = r#"
8834flag "--yes" global=#true {
8835    alias "-q" "--quietly" hide=#true
8836}
8837cmd "run" {
8838    flag "-q --yes --quietly"
8839}
8840"#
8841        .parse::<Spec>()
8842        .unwrap();
8843
8844        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8845        let merged = &parsed.available_flags["--yes"];
8846        assert!(merged.hidden_short_aliases.is_empty());
8847        assert!(merged.hidden_aliases.is_empty());
8848        for key in ["-q", "--quietly"] {
8849            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8850        }
8851    }
8852
8853    #[test]
8854    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8855        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8856        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8857        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8858        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8859        // pre-merge global and miss the added `assume-yes`.
8860        let spec = r#"
8861flag "-y --yes --confirm" global=#true
8862cmd "run" {
8863    flag "--yes --assume-yes"
8864}
8865"#
8866        .parse::<Spec>()
8867        .unwrap();
8868
8869        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8870
8871        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8872            let flag = parsed
8873                .available_flags
8874                .get(key)
8875                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8876            assert!(flag.global, "{key} must stay global after the descent");
8877            assert_eq!(
8878                flag.long,
8879                vec![
8880                    "yes".to_string(),
8881                    "confirm".to_string(),
8882                    "assume-yes".to_string()
8883                ],
8884                "{key} must resolve to the flag carrying every alias",
8885            );
8886        }
8887
8888        assert_eq!(
8889            unique_flags(parsed.available_flags.values()).count(),
8890            1,
8891            "all aliases must point at one flag object, got {:?}",
8892            parsed.available_flags,
8893        );
8894    }
8895
8896    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8897    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8898    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8899    ///
8900    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8901    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8902    fn mounted_task_flag_spec() -> Spec {
8903        let mut task_cmd = SpecCommand::builder()
8904            .name("mytask")
8905            .flag(
8906                SpecFlag::builder()
8907                    .name("env")
8908                    .long("env")
8909                    .arg(
8910                        SpecArg::builder()
8911                            .name("name")
8912                            .choices(["dev", "stage", "prod"])
8913                            .build(),
8914                    )
8915                    .global(false)
8916                    .build(),
8917            )
8918            .flag(
8919                SpecFlag::builder()
8920                    .name("bump")
8921                    .long("bump")
8922                    .arg(
8923                        SpecArg::builder()
8924                            .name("type")
8925                            .choices(["auto", "major"])
8926                            .build(),
8927                    )
8928                    .global(false)
8929                    .build(),
8930            )
8931            .build();
8932        task_cmd.mounted = true;
8933
8934        let mut run_cmd = SpecCommand::builder()
8935            .name("run")
8936            .flag(
8937                SpecFlag::builder()
8938                    .name("force")
8939                    .short('f')
8940                    .long("force")
8941                    .global(false)
8942                    .build(),
8943            )
8944            .build();
8945        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8946
8947        let mut cmd = SpecCommand::builder()
8948            .name("test")
8949            .flag(
8950                SpecFlag::builder()
8951                    .name("env")
8952                    .short('E')
8953                    .long("env")
8954                    .arg(SpecArg::builder().name("ENV").build())
8955                    .global(true)
8956                    .build(),
8957            )
8958            .flag(
8959                SpecFlag::builder()
8960                    .name("silent")
8961                    .long("silent")
8962                    .global(true)
8963                    .build(),
8964            )
8965            .build();
8966        cmd.subcommands.insert("run".to_string(), run_cmd);
8967
8968        Spec {
8969            name: "test".to_string(),
8970            bin: "test".to_string(),
8971            cmd,
8972            ..Default::default()
8973        }
8974    }
8975
8976    #[test]
8977    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8978        // The mounted program's own commands are ordinary commands relative to each other, so
8979        // descending *within* the mounted tree must follow the normal rules — including keeping
8980        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8981        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8982        // global, which the next descent's `retain(global)` then dropped entirely.
8983        let deep = SpecCommand::builder().name("deep").build();
8984        let mut sub = SpecCommand::builder()
8985            .name("sub")
8986            // Re-declares the mounted program's own global as non-global.
8987            .flag(
8988                SpecFlag::builder()
8989                    .name("cd")
8990                    .short('C')
8991                    .long("cd")
8992                    .arg(SpecArg::builder().name("dir").build())
8993                    .global(false)
8994                    .build(),
8995            )
8996            .build();
8997        sub.subcommands.insert("deep".to_string(), deep);
8998        let mut task = SpecCommand::builder()
8999            .name("task")
9000            .flag(
9001                SpecFlag::builder()
9002                    .name("cd")
9003                    .short('C')
9004                    .long("cd")
9005                    .arg(SpecArg::builder().name("dir").build())
9006                    .global(true)
9007                    .build(),
9008            )
9009            .build();
9010        task.subcommands.insert("sub".to_string(), sub);
9011        task.mark_mounted();
9012
9013        let mut run_cmd = SpecCommand::builder().name("run").build();
9014        run_cmd.subcommands.insert("task".to_string(), task);
9015        let mut cmd = SpecCommand::builder().name("test").build();
9016        cmd.subcommands.insert("run".to_string(), run_cmd);
9017        let spec = Spec {
9018            name: "test".to_string(),
9019            bin: "test".to_string(),
9020            cmd,
9021            ..Default::default()
9022        };
9023
9024        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
9025        assert!(
9026            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
9027            "the mounted program's own global must survive descents inside the mounted tree",
9028        );
9029        assert!(
9030            parsed.completion_flags().contains_key("--cd"),
9031            "and must still be offered there: it belongs to the mounted program",
9032        );
9033        assert!(
9034            parsed.completion_flags().contains_key("-C"),
9035            "including the short the nested command re-declared",
9036        );
9037    }
9038
9039    #[test]
9040    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
9041        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
9042        // into the command the mount sits on. They belong to the mounted program, so they must
9043        // be offered inside the mounted commands rather than filtered out with the mounting
9044        // CLI's own flags.
9045        let mut task = SpecCommand::builder()
9046            .name("task")
9047            .flag(
9048                SpecFlag::builder()
9049                    .name("bump")
9050                    .long("bump")
9051                    .global(false)
9052                    .build(),
9053            )
9054            .build();
9055        task.mark_mounted();
9056
9057        let mut run_cmd = SpecCommand::builder().name("run").build();
9058        run_cmd.subcommands.insert("task".to_string(), task);
9059        // What `mount()` leaves behind when the mounted spec's root declares flags.
9060        run_cmd.flags = vec![
9061            SpecFlag::builder()
9062                .name("tglobal")
9063                .long("tglobal")
9064                .global(true)
9065                .build(),
9066            SpecFlag::builder()
9067                .name("tlocal")
9068                .long("tlocal")
9069                .global(false)
9070                .build(),
9071        ];
9072        run_cmd.flags_from_mount = true;
9073
9074        let mut cmd = SpecCommand::builder()
9075            .name("test")
9076            .flag(
9077                SpecFlag::builder()
9078                    .name("silent")
9079                    .long("silent")
9080                    .global(true)
9081                    .build(),
9082            )
9083            .build();
9084        cmd.subcommands.insert("run".to_string(), run_cmd);
9085        let spec = Spec {
9086            name: "test".to_string(),
9087            bin: "test".to_string(),
9088            cmd,
9089            ..Default::default()
9090        };
9091
9092        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
9093        assert_eq!(
9094            parsed.completion_flags().keys().collect::<Vec<_>>(),
9095            vec!["--bump", "--tglobal"],
9096            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
9097             `--silent` does not, and the mount's non-global root flag is not inherited",
9098        );
9099    }
9100
9101    #[test]
9102    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
9103        // Regression for jdx/mise#11282. A mounted command describes another program, which
9104        // does not accept the mounting CLI's globals (mise forwards everything after a task
9105        // name to the task). They must stay recognized — they may appear before the mounted
9106        // command — but must not be offered in completions there.
9107        let spec = mounted_task_flag_spec();
9108        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
9109
9110        // Still recognized for parsing...
9111        assert!(parsed.available_flags.contains_key("--silent"));
9112        assert!(parsed.available_flags.contains_key("-E"));
9113        // ...but belonging to a command above the mount, so not offered.
9114        assert_eq!(
9115            parsed.completion_flags().keys().collect::<Vec<_>>(),
9116            vec!["--bump", "--env"],
9117            "only the mounted command's own flags may be offered",
9118        );
9119
9120        // `run`'s own non-global flag is dropped on descent, as it always was.
9121        assert!(!parsed.available_flags.contains_key("--force"));
9122    }
9123
9124    #[test]
9125    fn test_mounted_cmd_flag_wins_over_inherited_global() {
9126        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
9127        // by the root's `--env` global, so completing its value fell back to file completion.
9128        let spec = mounted_task_flag_spec();
9129        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
9130
9131        let awaiting = parsed
9132            .flag_awaiting_value
9133            .first()
9134            .expect("--env should await a value");
9135        assert_eq!(
9136            awaiting
9137                .arg
9138                .as_ref()
9139                .and_then(|a| a.choices.as_ref())
9140                .map(|c| c.choices.clone()),
9141            Some(vec![
9142                "dev".to_string(),
9143                "stage".to_string(),
9144                "prod".to_string()
9145            ]),
9146            "the mounted command's own --env must win over the inherited global",
9147        );
9148
9149        // The global's short is not declared by the mounted command, so it keeps pointing at
9150        // the global and a value passed before the mounted command still parses.
9151        let parsed =
9152            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
9153        assert!(
9154            parsed.args.is_empty(),
9155            "prefix global tokens must not be consumed as positionals, got {:?}",
9156            parsed.args
9157        );
9158        assert_eq!(
9159            parsed.as_env().get("usage_env").map(String::as_str),
9160            Some("anything"),
9161        );
9162    }
9163
9164    #[test]
9165    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
9166        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
9167        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
9168        // global's value would be validated against the mounted flag's choices and a legitimate
9169        // value would be rejected.
9170        let spec = mounted_task_flag_spec();
9171        let parsed = parse_partial(
9172            &spec,
9173            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
9174        )
9175        .unwrap();
9176        assert!(
9177            parsed.errors.is_empty(),
9178            "prefix global value must not be validated against the mounted flag: {:?}",
9179            parsed
9180                .errors
9181                .iter()
9182                .map(|e| e.to_string())
9183                .collect::<Vec<_>>(),
9184        );
9185        assert_eq!(
9186            parsed.as_env().get("usage_env").map(String::as_str),
9187            Some("not-a-task-choice"),
9188        );
9189
9190        // The embedded-value form binds the same way.
9191        let parsed = parse_partial(
9192            &spec,
9193            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
9194        )
9195        .unwrap();
9196        assert!(parsed.errors.is_empty());
9197        assert_eq!(
9198            parsed.as_env().get("usage_env").map(String::as_str),
9199            Some("not-a-task-choice"),
9200        );
9201
9202        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
9203        // the same name was already used before it.
9204        let parsed = parse_partial(
9205            &spec,
9206            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
9207        )
9208        .unwrap();
9209        let awaiting = parsed
9210            .flag_awaiting_value
9211            .first()
9212            .expect("--env should await a value");
9213        assert_eq!(
9214            awaiting
9215                .arg
9216                .as_ref()
9217                .and_then(|a| a.choices.as_ref())
9218                .map(|c| c.choices.clone()),
9219            Some(vec![
9220                "dev".to_string(),
9221                "stage".to_string(),
9222                "prod".to_string()
9223            ]),
9224            "the mounted command's --env must own the name after the mounted command",
9225        );
9226    }
9227
9228    #[test]
9229    fn test_non_global_flag_does_not_hide_subcommand() {
9230        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
9231        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
9232        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
9233        let spec = mounted_task_flag_spec();
9234
9235        for words in [
9236            // `run` declares `-f/--force` as non-global.
9237            &["test", "run", "--force", "mytask"][..],
9238            &["test", "run", "-f", "mytask"][..],
9239            // Mixed with a global before the subcommand.
9240            &["test", "-E", "prod", "run", "--force", "mytask"][..],
9241        ] {
9242            let parsed = parse_partial(&spec, &input(words)).unwrap();
9243            assert_eq!(
9244                parsed
9245                    .cmds
9246                    .iter()
9247                    .map(|c| c.name.as_str())
9248                    .collect::<Vec<_>>(),
9249                vec!["test", "run", "mytask"],
9250                "{words:?} should descend into the mounted command",
9251            );
9252            assert!(
9253                parsed.args.is_empty(),
9254                "{words:?} should not consume a positional, got {:?}",
9255                parsed.args,
9256            );
9257            assert_eq!(
9258                parsed.as_env().get("usage_force").map(String::as_str),
9259                Some("true"),
9260                "the non-global flag must still be recorded for {words:?}",
9261            );
9262        }
9263
9264        // A non-global flag that takes a value consumes it, rather than reading the value as the
9265        // subcommand.
9266        let mut run_cmd = SpecCommand::builder()
9267            .name("run")
9268            .flag(
9269                SpecFlag::builder()
9270                    .name("output")
9271                    .short('o')
9272                    .long("output")
9273                    .arg(SpecArg::builder().name("mode").build())
9274                    .global(false)
9275                    .build(),
9276            )
9277            .build();
9278        run_cmd.subcommands.insert(
9279            "task".to_string(),
9280            SpecCommand::builder().name("task").build(),
9281        );
9282        let mut cmd = SpecCommand::builder().name("test").build();
9283        cmd.subcommands.insert("run".to_string(), run_cmd);
9284        let spec = Spec {
9285            name: "test".to_string(),
9286            bin: "test".to_string(),
9287            cmd,
9288            ..Default::default()
9289        };
9290
9291        let parsed =
9292            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
9293        assert_eq!(
9294            parsed
9295                .cmds
9296                .iter()
9297                .map(|c| c.name.as_str())
9298                .collect::<Vec<_>>(),
9299            vec!["test", "run", "task"],
9300        );
9301        assert_eq!(
9302            parsed.as_env().get("usage_output").map(String::as_str),
9303            Some("quiet"),
9304        );
9305
9306        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
9307        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
9308        assert_parse_err(
9309            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
9310            "unexpected word: --nope",
9311        );
9312    }
9313
9314    #[test]
9315    fn test_non_mounted_subcommand_offers_inherited_globals() {
9316        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
9317        // still both recognized and offered.
9318        let mut run_cmd = SpecCommand::builder().name("run").build();
9319        run_cmd.subcommands.insert(
9320            "nested".to_string(),
9321            SpecCommand::builder().name("nested").build(),
9322        );
9323        let mut cmd = SpecCommand::builder()
9324            .name("test")
9325            .flag(
9326                SpecFlag::builder()
9327                    .name("silent")
9328                    .long("silent")
9329                    .global(true)
9330                    .build(),
9331            )
9332            .build();
9333        cmd.subcommands.insert("run".to_string(), run_cmd);
9334        let spec = Spec {
9335            name: "test".to_string(),
9336            bin: "test".to_string(),
9337            cmd,
9338            ..Default::default()
9339        };
9340
9341        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
9342        assert_eq!(
9343            parsed.completion_flags().keys().collect::<Vec<_>>(),
9344            parsed.available_flags.keys().collect::<Vec<_>>(),
9345        );
9346        assert!(parsed.completion_flags().contains_key("--silent"));
9347    }
9348
9349    #[test]
9350    fn test_subcommand_alias_collision_keeps_last_owner() {
9351        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
9352        // share an alias are resolved. Historically the flattened flag map gave the shared
9353        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
9354        let run_cmd = SpecCommand::builder()
9355            .name("run")
9356            .flag(
9357                SpecFlag::builder()
9358                    .name("alpha")
9359                    .short('x')
9360                    .long("alpha")
9361                    .global(false)
9362                    .build(),
9363            )
9364            .flag(
9365                SpecFlag::builder()
9366                    .name("beta")
9367                    .short('x')
9368                    .long("beta")
9369                    .global(false)
9370                    .build(),
9371            )
9372            .build();
9373        let mut cmd = SpecCommand::builder().name("test").build();
9374        cmd.subcommands.insert("run".to_string(), run_cmd);
9375        let spec = Spec {
9376            name: "test".to_string(),
9377            bin: "test".to_string(),
9378            cmd,
9379            ..Default::default()
9380        };
9381
9382        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
9383        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
9384        assert_eq!(
9385            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
9386            Some("beta"),
9387            "the last-declared flag must keep a shared short alias",
9388        );
9389        // Both distinct long aliases remain recognized and point to their own flag.
9390        assert_eq!(
9391            parsed
9392                .available_flags
9393                .get("--alpha")
9394                .map(|f| f.name.as_str()),
9395            Some("alpha"),
9396        );
9397        assert_eq!(
9398            parsed
9399                .available_flags
9400                .get("--beta")
9401                .map(|f| f.name.as_str()),
9402            Some("beta"),
9403        );
9404    }
9405
9406    #[test]
9407    fn test_default_subcommand_same_name_child() {
9408        // Test that default_subcommand doesn't cause issues when the default subcommand
9409        // has a child with the same name (e.g., "run" has a task named "run").
9410        // This verifies we don't switch multiple times or get stuck in a loop.
9411        let run_task = SpecCommand::builder()
9412            .name("run")
9413            .arg(SpecArg::builder().name("args").build())
9414            .build();
9415        let mut run_cmd = SpecCommand::builder().name("run").build();
9416        run_cmd.subcommands.insert("run".to_string(), run_task);
9417
9418        let mut cmd = SpecCommand::builder().name("test").build();
9419        cmd.subcommands.insert("run".to_string(), run_cmd);
9420
9421        let spec = Spec {
9422            name: "test".to_string(),
9423            bin: "test".to_string(),
9424            cmd,
9425            default_subcommand: Some("run".to_string()),
9426            ..Default::default()
9427        };
9428
9429        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
9430        let input = vec!["test".to_string(), "run".to_string()];
9431        let parsed = parse(&spec, &input).unwrap();
9432
9433        // Should have two commands: root and "run"
9434        assert_eq!(parsed.cmds.len(), 2);
9435        assert_eq!(parsed.cmds[0].name, "test");
9436        assert_eq!(parsed.cmds[1].name, "run");
9437
9438        // "test run run" should descend into the "run" task (child of "run" subcommand)
9439        let input = vec![
9440            "test".to_string(),
9441            "run".to_string(),
9442            "run".to_string(),
9443            "hello".to_string(),
9444        ];
9445        let parsed = parse(&spec, &input).unwrap();
9446
9447        assert_eq!(parsed.cmds.len(), 3);
9448        assert_eq!(parsed.cmds[0].name, "test");
9449        assert_eq!(parsed.cmds[1].name, "run");
9450        assert_eq!(parsed.cmds[2].name, "run");
9451        assert_eq!(parsed.args.len(), 1);
9452        let value = parsed.args.values().next().unwrap();
9453        assert_eq!(value.to_string(), "hello");
9454
9455        // Key test case: "test other" should switch to default subcommand "run"
9456        // and treat "other" as a positional arg (not try to switch again because
9457        // "run" also has a "run" child).
9458        let mut run_cmd = SpecCommand::builder()
9459            .name("run")
9460            .arg(SpecArg::builder().name("task").build())
9461            .build();
9462        let run_task = SpecCommand::builder().name("run").build();
9463        run_cmd.subcommands.insert("run".to_string(), run_task);
9464
9465        let mut cmd = SpecCommand::builder().name("test").build();
9466        cmd.subcommands.insert("run".to_string(), run_cmd);
9467
9468        let spec = Spec {
9469            name: "test".to_string(),
9470            bin: "test".to_string(),
9471            cmd,
9472            default_subcommand: Some("run".to_string()),
9473            ..Default::default()
9474        };
9475
9476        let input = vec!["test".to_string(), "other".to_string()];
9477        let parsed = parse(&spec, &input).unwrap();
9478
9479        // Should have two commands: root and "run" (the default)
9480        // We should NOT have switched again to the "run" task child
9481        assert_eq!(parsed.cmds.len(), 2);
9482        assert_eq!(parsed.cmds[0].name, "test");
9483        assert_eq!(parsed.cmds[1].name, "run");
9484
9485        // "other" should be parsed as a positional arg
9486        assert_eq!(parsed.args.len(), 1);
9487        let value = parsed.args.values().next().unwrap();
9488        assert_eq!(value.to_string(), "other");
9489    }
9490
9491    #[test]
9492    fn test_restart_token() {
9493        // Test that restart_token resets argument parsing
9494        let run_cmd = SpecCommand::builder()
9495            .name("run")
9496            .arg(SpecArg::builder().name("task").build())
9497            .restart_token(":::".to_string())
9498            .build();
9499        let mut cmd = SpecCommand::builder().name("test").build();
9500        cmd.subcommands.insert("run".to_string(), run_cmd);
9501
9502        let spec = Spec {
9503            name: "test".to_string(),
9504            bin: "test".to_string(),
9505            cmd,
9506            ..Default::default()
9507        };
9508
9509        // "test run task1 ::: task2" - should end up with task2 as the arg
9510        let input = vec![
9511            "test".to_string(),
9512            "run".to_string(),
9513            "task1".to_string(),
9514            ":::".to_string(),
9515            "task2".to_string(),
9516        ];
9517        let parsed = parse(&spec, &input).unwrap();
9518
9519        // After restart, args were cleared and task2 was parsed
9520        assert_eq!(parsed.args.len(), 1);
9521        let value = parsed.args.values().next().unwrap();
9522        assert_eq!(value.to_string(), "task2");
9523    }
9524
9525    #[test]
9526    fn test_restart_token_multiple() {
9527        // Test multiple restart tokens
9528        let run_cmd = SpecCommand::builder()
9529            .name("run")
9530            .arg(SpecArg::builder().name("task").build())
9531            .restart_token(":::".to_string())
9532            .build();
9533        let mut cmd = SpecCommand::builder().name("test").build();
9534        cmd.subcommands.insert("run".to_string(), run_cmd);
9535
9536        let spec = Spec {
9537            name: "test".to_string(),
9538            bin: "test".to_string(),
9539            cmd,
9540            ..Default::default()
9541        };
9542
9543        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
9544        let input = vec![
9545            "test".to_string(),
9546            "run".to_string(),
9547            "task1".to_string(),
9548            ":::".to_string(),
9549            "task2".to_string(),
9550            ":::".to_string(),
9551            "task3".to_string(),
9552        ];
9553        let parsed = parse(&spec, &input).unwrap();
9554
9555        // After multiple restarts, args were cleared and task3 was parsed
9556        assert_eq!(parsed.args.len(), 1);
9557        let value = parsed.args.values().next().unwrap();
9558        assert_eq!(value.to_string(), "task3");
9559    }
9560
9561    #[test]
9562    fn test_restart_token_clears_flag_awaiting_value() {
9563        // Test that restart_token clears pending flag values
9564        let run_cmd = SpecCommand::builder()
9565            .name("run")
9566            .arg(SpecArg::builder().name("task").build())
9567            .flag(
9568                SpecFlag::builder()
9569                    .name("jobs")
9570                    .long("jobs")
9571                    .arg(SpecArg::builder().name("count").build())
9572                    .build(),
9573            )
9574            .restart_token(":::".to_string())
9575            .build();
9576        let mut cmd = SpecCommand::builder().name("test").build();
9577        cmd.subcommands.insert("run".to_string(), run_cmd);
9578
9579        let spec = Spec {
9580            name: "test".to_string(),
9581            bin: "test".to_string(),
9582            cmd,
9583            ..Default::default()
9584        };
9585
9586        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
9587        let input = vec![
9588            "test".to_string(),
9589            "run".to_string(),
9590            "task1".to_string(),
9591            "--jobs".to_string(),
9592            ":::".to_string(),
9593            "task2".to_string(),
9594        ];
9595        let parsed = parse(&spec, &input).unwrap();
9596
9597        // task2 should be parsed as the task arg, not as --jobs value
9598        assert_eq!(parsed.args.len(), 1);
9599        let value = parsed.args.values().next().unwrap();
9600        assert_eq!(value.to_string(), "task2");
9601        // --jobs should not have a value
9602        assert!(parsed.flag_awaiting_value.is_empty());
9603    }
9604
9605    #[test]
9606    fn test_restart_token_resets_double_dash() {
9607        // Test that restart_token resets the -- separator effect
9608        let run_cmd = SpecCommand::builder()
9609            .name("run")
9610            .arg(SpecArg::builder().name("task").build())
9611            .arg(SpecArg::builder().name("extra_args").var(true).build())
9612            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9613            .restart_token(":::".to_string())
9614            .build();
9615        let mut cmd = SpecCommand::builder().name("test").build();
9616        cmd.subcommands.insert("run".to_string(), run_cmd);
9617
9618        let spec = Spec {
9619            name: "test".to_string(),
9620            bin: "test".to_string(),
9621            cmd,
9622            ..Default::default()
9623        };
9624
9625        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
9626        let input = vec![
9627            "test".to_string(),
9628            "run".to_string(),
9629            "task1".to_string(),
9630            "--".to_string(),
9631            "extra".to_string(),
9632            ":::".to_string(),
9633            "--verbose".to_string(),
9634            "task2".to_string(),
9635        ];
9636        let parsed = parse(&spec, &input).unwrap();
9637
9638        // --verbose should be parsed as a flag (not an arg) after the restart
9639        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9640        // task2 should be the arg after restart
9641        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9642        let value = parsed.args.get(task_arg).unwrap();
9643        assert_eq!(value.to_string(), "task2");
9644    }
9645
9646    #[test]
9647    fn test_double_dashes_without_preserve() {
9648        // Only the first `--` is a separator; a later one is a value, because flag
9649        // parsing has already stopped and there is nothing left for it to do.
9650        // `preserve` is about the *first* one — see the test below, where none is
9651        // consumed at all.
9652        let run_cmd = SpecCommand::builder()
9653            .name("run")
9654            .arg(SpecArg::builder().name("args").var(true).build())
9655            .build();
9656        let mut cmd = SpecCommand::builder().name("test").build();
9657        cmd.subcommands.insert("run".to_string(), run_cmd);
9658
9659        let spec = Spec {
9660            name: "test".to_string(),
9661            bin: "test".to_string(),
9662            cmd,
9663            ..Default::default()
9664        };
9665
9666        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
9667        let input = vec![
9668            "test".to_string(),
9669            "run".to_string(),
9670            "arg1".to_string(),
9671            "--".to_string(),
9672            "arg2".to_string(),
9673            "--".to_string(),
9674            "arg3".to_string(),
9675        ];
9676        let parsed = parse(&spec, &input).unwrap();
9677
9678        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9679        let value = parsed.args.get(args_arg).unwrap();
9680        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9681    }
9682
9683    #[test]
9684    fn test_double_dashes_with_preserve() {
9685        // Test that variadic args WITH `preserve` keep all double dashes
9686        let run_cmd = SpecCommand::builder()
9687            .name("run")
9688            .arg(
9689                SpecArg::builder()
9690                    .name("args")
9691                    .var(true)
9692                    .double_dash(SpecDoubleDashChoices::Preserve)
9693                    .build(),
9694            )
9695            .build();
9696        let mut cmd = SpecCommand::builder().name("test").build();
9697        cmd.subcommands.insert("run".to_string(), run_cmd);
9698
9699        let spec = Spec {
9700            name: "test".to_string(),
9701            bin: "test".to_string(),
9702            cmd,
9703            ..Default::default()
9704        };
9705
9706        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
9707        let input = vec![
9708            "test".to_string(),
9709            "run".to_string(),
9710            "arg1".to_string(),
9711            "--".to_string(),
9712            "arg2".to_string(),
9713            "--".to_string(),
9714            "arg3".to_string(),
9715        ];
9716        let parsed = parse(&spec, &input).unwrap();
9717
9718        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9719        let value = parsed.args.get(args_arg).unwrap();
9720        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9721    }
9722
9723    #[test]
9724    fn test_double_dashes_with_preserve_only_dashes() {
9725        // Test that variadic args WITH `preserve` keep all double dashes even
9726        // if the values are just double dashes
9727        let run_cmd = SpecCommand::builder()
9728            .name("run")
9729            .arg(
9730                SpecArg::builder()
9731                    .name("args")
9732                    .var(true)
9733                    .double_dash(SpecDoubleDashChoices::Preserve)
9734                    .build(),
9735            )
9736            .build();
9737        let mut cmd = SpecCommand::builder().name("test").build();
9738        cmd.subcommands.insert("run".to_string(), run_cmd);
9739
9740        let spec = Spec {
9741            name: "test".to_string(),
9742            bin: "test".to_string(),
9743            cmd,
9744            ..Default::default()
9745        };
9746
9747        // "test run -- --" - all double dashes should be preserved
9748        let input = vec![
9749            "test".to_string(),
9750            "run".to_string(),
9751            "--".to_string(),
9752            "--".to_string(),
9753        ];
9754        let parsed = parse(&spec, &input).unwrap();
9755
9756        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9757        let value = parsed.args.get(args_arg).unwrap();
9758        assert_eq!(value.to_string(), "-- --");
9759    }
9760
9761    #[test]
9762    fn test_double_dashes_with_preserve_multiple_args() {
9763        // Test with multiple args where only the second has has `preserve`
9764        let run_cmd = SpecCommand::builder()
9765            .name("run")
9766            .arg(SpecArg::builder().name("task").build())
9767            .arg(
9768                SpecArg::builder()
9769                    .name("extra_args")
9770                    .var(true)
9771                    .double_dash(SpecDoubleDashChoices::Preserve)
9772                    .build(),
9773            )
9774            .build();
9775        let mut cmd = SpecCommand::builder().name("test").build();
9776        cmd.subcommands.insert("run".to_string(), run_cmd);
9777
9778        let spec = Spec {
9779            name: "test".to_string(),
9780            bin: "test".to_string(),
9781            cmd,
9782            ..Default::default()
9783        };
9784
9785        // The first arg "task1" is captured normally
9786        // Then extra_args with `preserve` captures everything, including the "--" tokens
9787        let input = vec![
9788            "test".to_string(),
9789            "run".to_string(),
9790            "task1".to_string(),
9791            "--".to_string(),
9792            "arg1".to_string(),
9793            "--".to_string(),
9794            "--foo".to_string(),
9795        ];
9796        let parsed = parse(&spec, &input).unwrap();
9797
9798        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9799        let task_value = parsed.args.get(task_arg).unwrap();
9800        assert_eq!(task_value.to_string(), "task1");
9801
9802        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9803        let extra_value = parsed.args.get(extra_arg).unwrap();
9804        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9805    }
9806
9807    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9808        let cmd = SpecCommand::builder().name("test").args(args).build();
9809        Spec {
9810            name: "test".to_string(),
9811            bin: "test".to_string(),
9812            cmd,
9813            ..Default::default()
9814        }
9815    }
9816
9817    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9818        let arg = parsed
9819            .args
9820            .keys()
9821            .find(|a| a.name == name)
9822            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9823        parsed.args.get(arg).unwrap().to_string()
9824    }
9825
9826    fn required_arg(name: &str) -> SpecArg {
9827        SpecArg::builder()
9828            .name(name)
9829            .var(true)
9830            .required(false)
9831            .double_dash(SpecDoubleDashChoices::Required)
9832            .build()
9833    }
9834
9835    #[test]
9836    fn test_double_dash_required_reports_error_once_for_variadic() {
9837        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9838        let spec = spec_with_args([required_arg("files")]);
9839
9840        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9841
9842        assert!(parsed.args.is_empty());
9843        assert_eq!(parsed.errors.len(), 1);
9844        assert!(
9845            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9846        );
9847    }
9848
9849    #[test]
9850    fn test_double_dash_required_suppresses_missing_arg() {
9851        // The arg is never filled, so the end-of-parse check would also call it missing.
9852        let spec = spec_with_args([SpecArg::builder()
9853            .name("file")
9854            .required(true)
9855            .double_dash(SpecDoubleDashChoices::Required)
9856            .build()]);
9857
9858        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9859
9860        assert_eq!(parsed.errors.len(), 1);
9861        assert!(matches!(
9862            &parsed.errors[0],
9863            UsageErr::ArgRequiresDoubleDash(_)
9864        ));
9865        // The cursor stays put, so a completion keeps offering the same arg.
9866        assert_eq!(
9867            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9868            Some("file")
9869        );
9870        assert!(!parsed.double_dash_seen);
9871    }
9872
9873    #[test]
9874    fn test_double_dash_routes_to_required_arg() {
9875        // Everything after `--` belongs to the arg that requires it, even though the greedy
9876        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9877        let spec = spec_with_args([
9878            SpecArg::builder()
9879                .name("tool")
9880                .var(true)
9881                .required(false)
9882                .build(),
9883            required_arg("command"),
9884        ]);
9885
9886        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9887
9888        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9889        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9890        assert!(parsed.double_dash_seen);
9891    }
9892
9893    #[test]
9894    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9895        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9896        // "is it filled?" check cannot be a count of how many args were filled.
9897        let spec = spec_with_args([
9898            SpecArg::builder()
9899                .name("tool")
9900                .var(true)
9901                .required(true)
9902                .build(),
9903            required_arg("command"),
9904        ]);
9905
9906        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9907
9908        assert_eq!(arg_value(&parsed, "command"), "ls");
9909        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9910        assert!(
9911            parsed
9912                .errors
9913                .iter()
9914                .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool"))
9915        );
9916    }
9917
9918    #[test]
9919    fn test_double_dash_gap_applies_defaults() {
9920        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9921        let spec = spec_with_args([
9922            SpecArg::builder()
9923                .name("tool")
9924                .var(true)
9925                .required(false)
9926                .default_value("node@20")
9927                .build(),
9928            required_arg("command"),
9929        ]);
9930
9931        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9932
9933        assert_eq!(arg_value(&parsed, "command"), "ls");
9934        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9935    }
9936
9937    fn spec_with_restart_token_and_required_arg() -> Spec {
9938        let run_cmd = SpecCommand::builder()
9939            .name("run")
9940            .arg(SpecArg::builder().name("task").build())
9941            .arg(required_arg("run_args"))
9942            .restart_token(":::".to_string())
9943            .build();
9944        let mut cmd = SpecCommand::builder().name("test").build();
9945        cmd.subcommands.insert("run".to_string(), run_cmd);
9946        Spec {
9947            name: "test".to_string(),
9948            bin: "test".to_string(),
9949            cmd,
9950            ..Default::default()
9951        }
9952    }
9953
9954    #[test]
9955    fn test_double_dash_required_restart_token_resets_separator() {
9956        // The `--` before `:::` belongs to the previous invocation only.
9957        let spec = spec_with_restart_token_and_required_arg();
9958
9959        let parsed = parse_partial(
9960            &spec,
9961            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9962        )
9963        .unwrap();
9964
9965        assert_eq!(arg_value(&parsed, "task"), "task2");
9966        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9967        // Reported once even though the arg was violated after already succeeding once.
9968        assert_eq!(
9969            parsed
9970                .errors
9971                .iter()
9972                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9973                .count(),
9974            1
9975        );
9976    }
9977
9978    #[test]
9979    fn test_double_dash_required_restart_token_accepts_new_separator() {
9980        let spec = spec_with_restart_token_and_required_arg();
9981
9982        let parsed = parse(
9983            &spec,
9984            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9985        )
9986        .unwrap();
9987
9988        assert_eq!(arg_value(&parsed, "task"), "task2");
9989        assert_eq!(arg_value(&parsed, "run_args"), "c");
9990    }
9991
9992    #[test]
9993    fn test_double_dash_preserve_is_not_a_separator() {
9994        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9995        // arg that requires a separator. Deliberate: one token cannot be both.
9996        let spec = spec_with_args([
9997            SpecArg::builder()
9998                .name("kept")
9999                .var(true)
10000                .var_max(1)
10001                .required(false)
10002                .double_dash(SpecDoubleDashChoices::Preserve)
10003                .build(),
10004            required_arg("rest"),
10005        ]);
10006
10007        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
10008
10009        assert_eq!(arg_value(&parsed, "kept"), "--");
10010        assert!(parsed.args.keys().all(|a| a.name != "rest"));
10011        assert!(!parsed.double_dash_seen);
10012        assert_eq!(parsed.errors.len(), 1);
10013    }
10014
10015    #[test]
10016    fn test_double_dash_required_does_not_bail_in_parse_partial() {
10017        // Completions parse half-typed command lines; they must still get a result.
10018        let spec = spec_with_args([required_arg("file")]);
10019
10020        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
10021        assert!(parse(&spec, &input(&["test", "x"])).is_err());
10022    }
10023
10024    #[test]
10025    fn test_double_dash_without_required_arg_does_not_move_cursor() {
10026        // Specs with no `double_dash="required"` arg are untouched by the jump.
10027        let spec = spec_with_args([
10028            SpecArg::builder().name("first").required(false).build(),
10029            SpecArg::builder().name("second").required(false).build(),
10030        ]);
10031
10032        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
10033
10034        assert_eq!(arg_value(&parsed, "first"), "a");
10035        assert_eq!(arg_value(&parsed, "second"), "b");
10036        assert!(parsed.next_arg.is_none());
10037    }
10038
10039    #[test]
10040    fn test_parser_with_custom_env_for_required_arg() {
10041        let spec = spec_with_arg(
10042            SpecArg::builder()
10043                .name("name")
10044                .env("NAME")
10045                .required(true)
10046                .build(),
10047        );
10048        // FIXME: Audit that the environment access only happens in single-threaded code.
10049        unsafe { std::env::remove_var("NAME") };
10050
10051        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
10052            .expect("parse should succeed with custom env");
10053        assert_eq!(parsed.args.len(), 1);
10054        assert_eq!(first_string_value(&parsed), "john");
10055    }
10056
10057    #[test]
10058    fn test_parser_with_custom_env_for_required_flag() {
10059        let spec = spec_with_flag(
10060            SpecFlag::builder()
10061                .long("name")
10062                .env("NAME")
10063                .required(true)
10064                .arg(SpecArg::builder().name("name").build())
10065                .build(),
10066        );
10067        // FIXME: Audit that the environment access only happens in single-threaded code.
10068        unsafe { std::env::remove_var("NAME") };
10069
10070        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
10071            .expect("parse should succeed with custom env");
10072        assert_eq!(parsed.flags.len(), 1);
10073        assert_eq!(first_string_value(&parsed), "jane");
10074    }
10075
10076    #[test]
10077    fn test_flag_environment_fallbacks_preserve_declaration_order() {
10078        let spec = spec_with_flag(
10079            SpecFlag::builder()
10080                .long("name")
10081                .env("NAME")
10082                .env_fallback("OLD_NAME")
10083                .env_fallback("OLDER_NAME")
10084                .deprecated_env("DEPRECATED_NAME")
10085                .arg(SpecArg::builder().name("name").build())
10086                .build(),
10087        );
10088
10089        let parsed = parse_with_env(
10090            &spec,
10091            &["test"],
10092            &[
10093                ("NAME", "canonical"),
10094                ("OLD_NAME", "fallback"),
10095                ("DEPRECATED_NAME", "deprecated"),
10096            ],
10097        )
10098        .unwrap();
10099        assert_eq!(first_string_value(&parsed), "canonical");
10100
10101        let parsed = parse_with_env(
10102            &spec,
10103            &["test"],
10104            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
10105        )
10106        .unwrap();
10107        assert_eq!(first_string_value(&parsed), "old");
10108
10109        let parsed =
10110            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
10111        assert_eq!(first_string_value(&parsed), "deprecated");
10112    }
10113
10114    #[test]
10115    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
10116        let spec = spec_with_flag(
10117            SpecFlag::builder()
10118                .long("name")
10119                .env("NAME")
10120                .deprecated_env("DEPRECATED_NAME")
10121                .arg(SpecArg::builder().name("name").build())
10122                .build(),
10123        );
10124
10125        // The current name is not a deprecated one, and says nothing.
10126        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
10127        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10128
10129        let parsed =
10130            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
10131        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10132        assert_eq!(
10133            parsed.warnings[0].kind,
10134            crate::warn::WarningKind::DeprecatedEnv
10135        );
10136        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
10137        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
10138        // Reported, not printed, and the value still arrives.
10139        assert_eq!(first_string_value(&parsed), "deprecated");
10140    }
10141
10142    #[test]
10143    fn a_deprecated_flag_reports_only_when_it_was_used() {
10144        let spec = spec_with_flag(
10145            SpecFlag::builder()
10146                .long("output")
10147                .deprecated("use --out")
10148                .deprecated_remove_at("3.0.0")
10149                .arg(SpecArg::builder().name("output").build())
10150                .build(),
10151        );
10152
10153        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
10154        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10155
10156        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10157        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10158        assert_eq!(
10159            parsed.warnings[0].kind,
10160            crate::warn::WarningKind::DeprecatedFlag
10161        );
10162        // Named the way it was typed, dashes and all.
10163        assert_eq!(parsed.warnings[0].name, "--output");
10164        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
10165        assert_eq!(
10166            parsed.warnings[0].render(),
10167            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
10168        );
10169    }
10170
10171    #[test]
10172    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
10173        let flag = SpecFlag::builder()
10174            .long("output")
10175            .deprecated("use --out")
10176            .deprecated_warn_at("9.0.0")
10177            .arg(SpecArg::builder().name("output").build())
10178            .build();
10179        let mut spec = spec_with_flag(flag);
10180        spec.version = Some("2.0.0".to_string());
10181
10182        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10183        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10184
10185        // And once the CLI is the release that was named, it speaks up.
10186        spec.version = Some("9.0.0".to_string());
10187        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10188        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10189    }
10190
10191    #[test]
10192    fn test_parser_with_custom_env_still_fails_when_missing() {
10193        let spec = spec_with_arg(
10194            SpecArg::builder()
10195                .name("name")
10196                .env("NAME")
10197                .required(true)
10198                .build(),
10199        );
10200        // FIXME: Audit that the environment access only happens in single-threaded code.
10201        unsafe { std::env::remove_var("NAME") };
10202        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
10203    }
10204
10205    #[test]
10206    fn test_parser_does_not_treat_env_choice_value_as_help() {
10207        let spec = spec_with_arg(
10208            SpecArg::builder()
10209                .name("env")
10210                .env("CURRENT_ENV")
10211                .choices(["dev", "staging"])
10212                .required(false)
10213                .build(),
10214        );
10215
10216        assert_parse_err(
10217            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
10218            "Invalid choice for arg env: --help, expected one of dev, staging",
10219        );
10220    }
10221
10222    #[test]
10223    fn test_parser_does_not_treat_default_choice_value_as_help() {
10224        let spec = spec_with_flag(
10225            SpecFlag::builder()
10226                .long("env")
10227                .arg(
10228                    SpecArg::builder()
10229                        .name("env")
10230                        .choices(["dev", "staging"])
10231                        .build(),
10232                )
10233                .default_value("--help")
10234                .build(),
10235        );
10236
10237        assert_parse_err(
10238            parse_with_env(&spec, &["test"], &[]),
10239            "Invalid choice for option env: --help, expected one of dev, staging",
10240        );
10241    }
10242
10243    /// argv as `parse` wants it, program name included.
10244    fn words(of: &[&str]) -> Vec<String> {
10245        of.iter().map(|s| s.to_string()).collect()
10246    }
10247
10248    #[test]
10249    fn a_command_that_needs_a_subcommand_says_so() {
10250        // The spec has carried `subcommand_required` since the derive needed it, and this parser
10251        // never read it — so `mise generate`, which declares it, parsed as a complete
10252        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
10253        let spec: Spec = r#"
10254name "ex"
10255bin "ex"
10256cmd "gen" subcommand_required=#true {
10257    cmd "two" {}
10258    cmd "one" {}
10259    cmd "secret" hide=#true {}
10260    alias "g"
10261}
10262cmd "open" {
10263    cmd "sub" {}
10264}
10265"#
10266        .parse()
10267        .unwrap();
10268
10269        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
10270        // Sorted, so the message does not depend on map order; hidden commands left out,
10271        // because a message telling someone to type a hidden name is worse than a vague one;
10272        // and the alias not listed beside the name it points at.
10273        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
10274
10275        // Reached through its alias, and still about the command rather than the spelling.
10276        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
10277        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
10278
10279        // Given one: fine.
10280        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
10281
10282        // And a command that has subcommands without declaring them required is untouched —
10283        // this is the half that keeps the check from being "any command with children".
10284        parse(&spec, &words(&["ex", "open"])).unwrap();
10285        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
10286    }
10287
10288    #[test]
10289    fn arg_required_else_help_observes_the_selected_commands_argv() {
10290        let spec: Spec = r#"
10291name "ex"
10292bin "ex"
10293flag "--verbose" global=#true
10294cmd "run" arg_required_else_help=#true {
10295    flag "--all"
10296}
10297"#
10298        .parse()
10299        .unwrap();
10300        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10301
10302        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
10303        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10304
10305        // A global before the command belongs to the ancestor. It selected `run`, but did not
10306        // give `run` an argument of its own.
10307        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
10308        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10309
10310        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
10311    }
10312
10313    #[test]
10314    fn empty_default_with_parent_flags_observes_child_argv() {
10315        let spec: Spec = r#"
10316name "ex"
10317bin "ex"
10318default_subcommand "run"
10319default_subcommand_on_empty #true
10320flag "--verbose" global=#true
10321cmd "run" arg_required_else_help=#true {}
10322"#
10323        .parse()
10324        .unwrap();
10325        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10326        let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
10327        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10328    }
10329
10330    #[test]
10331    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
10332        let spec: Spec = r#"
10333name "ex"
10334bin "ex"
10335unknown_flags "error"
10336external_subcommand #true
10337cmd "install"
10338flag "-v --verbose" global=#true
10339"#
10340        .parse()
10341        .unwrap();
10342
10343        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
10344        assert_eq!(
10345            parsed.external,
10346            Some(vec!["foo".into(), "--help".into(), "bar".into()])
10347        );
10348        assert!(parsed.flags.is_empty());
10349
10350        // Known subcommands still win.
10351        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
10352        assert_eq!(parsed.cmd.name, "install");
10353        assert!(parsed.external.is_none());
10354
10355        // A global flag before the unmatched word still binds on the parent.
10356        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
10357        assert_eq!(
10358            parsed.external,
10359            Some(vec!["foo".into(), "--verbose".into()])
10360        );
10361        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
10362
10363        // An unknown flag on the parent is still an error, which is what clap does.
10364        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
10365
10366        // A negative number is a value, not a flag, so it can be the unmatched word.
10367        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
10368        // token as a flag and never reach the catch-all.
10369        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
10370        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
10371    }
10372
10373    #[test]
10374    fn an_external_subcommand_satisfies_subcommand_required() {
10375        let mut spec: Spec = r#"
10376name "ex"
10377bin "ex"
10378external_subcommand #true
10379cmd "install"
10380"#
10381        .parse()
10382        .unwrap();
10383        spec.cmd.subcommand_required = true;
10384
10385        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
10386        assert!(parse(&spec, &input(&["ex"])).is_err());
10387    }
10388
10389    #[test]
10390    fn a_default_subcommand_outranks_an_external_one() {
10391        let spec: Spec = r#"
10392name "ex"
10393bin "ex"
10394default_subcommand "run"
10395external_subcommand #true
10396cmd "run" {
10397    arg "[task]"
10398}
10399"#
10400        .parse()
10401        .unwrap();
10402
10403        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10404        assert_eq!(parsed.cmd.name, "run");
10405        assert!(parsed.external.is_none());
10406        assert_eq!(first_string_value(&parsed), "build");
10407    }
10408
10409    #[test]
10410    fn multicall_basename_strips_a_path_and_exe() {
10411        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10412        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10413        assert_eq!(multicall_basename("LS.EXE"), "LS");
10414        assert_eq!(multicall_basename("busybox"), "busybox");
10415    }
10416
10417    #[test]
10418    fn a_multicall_applet_is_the_first_word() {
10419        let spec: Spec = r#"
10420name "busybox"
10421bin "busybox"
10422multicall #true
10423cmd "ls" {
10424    arg "[ARGS]" var=#true
10425}
10426cmd "cat"
10427"#
10428        .parse()
10429        .unwrap();
10430
10431        // A symlink: argv[0] is the applet.
10432        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10433        assert_eq!(parsed.cmd.name, "ls");
10434        match parsed.args.values().next() {
10435            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10436            other => panic!("expected ARGS to collect -l, got {other:?}"),
10437        }
10438
10439        // A dispatcher invocation still skips argv[0].
10440        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10441        assert_eq!(parsed.cmd.name, "ls");
10442
10443        // Configured dispatcher values receive the same path and extension normalization.
10444        let mut configured = spec.clone();
10445        configured.name = "BusyBox".to_string();
10446        configured.bin = "/opt/bin/busybox.exe".to_string();
10447        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10448        assert_eq!(parsed.cmd.name, "ls");
10449
10450        // `.exe` is stripped so Windows and Unix agree.
10451        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10452        assert_eq!(parsed.cmd.name, "ls");
10453
10454        // Without the property, argv[0] is discarded as usual.
10455        let mut plain = spec.clone();
10456        plain.multicall = false;
10457        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10458        assert_eq!(parsed.cmd.name, "ls");
10459    }
10460
10461    #[test]
10462    fn a_multicall_unknown_applet_can_be_external() {
10463        let spec: Spec = r#"
10464name "busybox"
10465bin "busybox"
10466multicall #true
10467unknown_flags "error"
10468external_subcommand #true
10469cmd "ls"
10470"#
10471        .parse()
10472        .unwrap();
10473
10474        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10475        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10476
10477        let mut closed = spec.clone();
10478        closed.cmd.external_subcommand = false;
10479        assert!(parse(&closed, &input(&["wat"])).is_err());
10480    }
10481
10482    #[cfg(feature = "unstable_choices_env")]
10483    #[test]
10484    fn test_parser_arg_choices_from_custom_env() {
10485        let spec = spec_arg_choices_env("DEPLOY_ENVS");
10486
10487        let parsed =
10488            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10489        assert_eq!(first_string_value(&parsed), "bar");
10490
10491        assert_parse_err(
10492            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10493            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10494        );
10495        assert_parse_err(
10496            parse_with_env(&spec, &["test", "prod"], &[]),
10497            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10498        );
10499    }
10500
10501    #[cfg(feature = "unstable_choices_env")]
10502    #[test]
10503    fn test_parser_validates_flag_choices_from_custom_env() {
10504        let spec = spec_flag_choices_env("DEPLOY_ENVS");
10505        let parsed = parse_with_env(
10506            &spec,
10507            &["test", "--env", "baz"],
10508            &[("DEPLOY_ENVS", "foo,bar baz")],
10509        )
10510        .unwrap();
10511        assert_eq!(first_string_value(&parsed), "baz");
10512    }
10513
10514    #[cfg(feature = "unstable_choices_env")]
10515    #[test]
10516    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10517        let arg_env_spec = spec_with_arg(
10518            SpecArg::builder()
10519                .name("env")
10520                .env("CURRENT_ENV")
10521                .choices_env("DEPLOY_ENVS")
10522                .build(),
10523        );
10524        assert_parse_err(
10525            parse_with_env(
10526                &arg_env_spec,
10527                &["test"],
10528                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10529            ),
10530            "Invalid choice for arg env: prod, expected one of dev, staging",
10531        );
10532
10533        let flag_default_spec = spec_with_flag(
10534            SpecFlag::builder()
10535                .long("env")
10536                .arg(
10537                    SpecArg::builder()
10538                        .name("env")
10539                        .choices_env("DEPLOY_ENVS")
10540                        .build(),
10541                )
10542                .default_value("prod")
10543                .build(),
10544        );
10545        assert_parse_err(
10546            parse_with_env(
10547                &flag_default_spec,
10548                &["test"],
10549                &[("DEPLOY_ENVS", "dev,staging")],
10550            ),
10551            "Invalid choice for option env: prod, expected one of dev, staging",
10552        );
10553    }
10554
10555    #[test]
10556    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10557        let spec: Spec = r#"
10558            flag "-v --verbose" var=#true
10559            arg "[database]" default="myapp_dev"
10560            arg "[args...]"
10561        "#
10562        .parse()
10563        .unwrap();
10564        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10565            .into_iter()
10566            .map(String::from)
10567            .collect();
10568        let parsed = parse(&spec, &input).unwrap();
10569        let env = parsed.as_env();
10570        assert_eq!(env.get("usage_database").unwrap(), "mydb");
10571        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10572    }
10573
10574    #[test]
10575    fn test_variadic_arg_captures_unknown_flags() {
10576        let cmd = SpecCommand::builder()
10577            .name("test")
10578            .flag(SpecFlag::builder().short('v').long("verbose").build())
10579            .arg(SpecArg::builder().name("database").required(false).build())
10580            .arg(
10581                SpecArg::builder()
10582                    .name("args")
10583                    .required(false)
10584                    .var(true)
10585                    .build(),
10586            )
10587            .build();
10588        let spec = Spec {
10589            name: "test".to_string(),
10590            bin: "test".to_string(),
10591            cmd,
10592            ..Default::default()
10593        };
10594
10595        // Unknown --host flag and its value should be captured by [args...]
10596        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10597            .into_iter()
10598            .map(String::from)
10599            .collect();
10600        let parsed = parse(&spec, &input).unwrap();
10601        assert_eq!(parsed.args.len(), 2);
10602        let args_val = parsed
10603            .args
10604            .iter()
10605            .find(|(a, _)| a.name == "args")
10606            .unwrap()
10607            .1;
10608        match args_val {
10609            ParseValue::MultiString(v) => {
10610                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10611            }
10612            _ => panic!("Expected MultiString, got {:?}", args_val),
10613        }
10614    }
10615
10616    #[test]
10617    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10618        let cmd = SpecCommand::builder()
10619            .name("test")
10620            .flag(SpecFlag::builder().short('v').long("verbose").build())
10621            .arg(SpecArg::builder().name("database").required(false).build())
10622            .arg(
10623                SpecArg::builder()
10624                    .name("args")
10625                    .required(false)
10626                    .var(true)
10627                    .build(),
10628            )
10629            .build();
10630        let spec = Spec {
10631            name: "test".to_string(),
10632            bin: "test".to_string(),
10633            cmd,
10634            ..Default::default()
10635        };
10636
10637        // With explicit -- separator
10638        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10639            .into_iter()
10640            .map(String::from)
10641            .collect();
10642        let parsed = parse(&spec, &input).unwrap();
10643        assert_eq!(parsed.args.len(), 2);
10644        let args_val = parsed
10645            .args
10646            .iter()
10647            .find(|(a, _)| a.name == "args")
10648            .unwrap()
10649            .1;
10650        match args_val {
10651            ParseValue::MultiString(v) => {
10652                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10653            }
10654            _ => panic!("Expected MultiString, got {:?}", args_val),
10655        }
10656    }
10657
10658    #[test]
10659    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10660        // Regression: --flag=value should be treated as a single positional token when
10661        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
10662        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10663
10664        // Single unknown --flag=value: must not produce a stray "3" positional.
10665        // as_env() shell-joins values, so "=" gets quoted.
10666        let input: Vec<String> = vec!["test", "--option=3"]
10667            .into_iter()
10668            .map(String::from)
10669            .collect();
10670        let parsed = parse(&spec, &input).unwrap();
10671        let env = parsed.as_env();
10672        assert_eq!(
10673            env.get("usage_other_args").map(String::as_str),
10674            Some("'--option=3'"),
10675            "expected a single --option=3 token, got {:?}",
10676            env.get("usage_other_args"),
10677        );
10678
10679        // Multiple unknown --flag=value args should each be kept intact
10680        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10681            .into_iter()
10682            .map(String::from)
10683            .collect();
10684        let parsed2 = parse(&spec, &input2).unwrap();
10685        let env2 = parsed2.as_env();
10686        assert_eq!(
10687            env2.get("usage_other_args").map(String::as_str),
10688            Some("'--foo=bar' '--baz=qux'"),
10689            "expected two intact tokens, got {:?}",
10690            env2.get("usage_other_args"),
10691        );
10692
10693        // Mix of plain positional args and unknown --flag=value tokens
10694        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10695            .into_iter()
10696            .map(String::from)
10697            .collect();
10698        let parsed3 = parse(&spec, &input3).unwrap();
10699        let env3 = parsed3.as_env();
10700        assert_eq!(
10701            env3.get("usage_other_args").map(String::as_str),
10702            Some("positional1 '--option=3' positional2"),
10703            "expected positional args and intact flag token, got {:?}",
10704            env3.get("usage_other_args"),
10705        );
10706    }
10707
10708    #[test]
10709    fn test_allow_hyphen_values_consumes_short_flag_collision() {
10710        let spec = r#"
10711flag "-d --working-dir <DIR>"
10712flag "-a --args <ARGS>" allow_hyphen_values=#true
10713"#
10714        .parse::<Spec>()
10715        .unwrap();
10716
10717        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10718
10719        assert_eq!(parsed.flags.len(), 1);
10720        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10721    }
10722
10723    #[test]
10724    fn test_allow_hyphen_values_consumes_embedded_long_value() {
10725        let spec = r#"
10726flag "-d --working-dir <DIR>"
10727flag "-a --args <ARGS>" allow_hyphen_values=#true
10728"#
10729        .parse::<Spec>()
10730        .unwrap();
10731
10732        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10733
10734        assert_eq!(parsed.flags.len(), 1);
10735        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10736    }
10737
10738    #[test]
10739    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10740        // The flag is declared to accept a token that looks like a flag, and `--` looks
10741        // like one, so it binds — which is what clap does with the same declaration.
10742        // Letting the separator arm run first consumed it and left the flag hungry, and
10743        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10744        let spec = r#"
10745flag "-a --args <ARGS>" allow_hyphen_values=#true
10746arg "[rest]..."
10747"#
10748        .parse::<Spec>()
10749        .unwrap();
10750
10751        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10752
10753        assert_eq!(flag_string_value(&parsed, "args"), "--");
10754        let rest = parsed
10755            .args
10756            .values()
10757            .next()
10758            .expect("expected the word after the separator to reach the argument");
10759        assert_eq!(rest.to_string(), "-x");
10760    }
10761
10762    #[test]
10763    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10764        // Which token supplied the first value says nothing about how many the argument
10765        // takes, so collection carries on from a hyphenated one exactly as from a plain
10766        // one. It still stops at the next flag-like token, which is what keeps a second
10767        // occurrence of the flag from being eaten as a value.
10768        let spec = r#"
10769flag "-a --args <ARGS>..." allow_hyphen_values=#true
10770"#
10771        .parse::<Spec>()
10772        .unwrap();
10773
10774        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10775
10776        let flag = parsed
10777            .flags
10778            .keys()
10779            .find(|flag| flag.name == "args")
10780            .expect("expected args flag");
10781        match parsed.flags.get(flag).expect("expected args value") {
10782            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10783            other => panic!("expected a list of values, got {other:?}"),
10784        }
10785    }
10786
10787    #[test]
10788    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10789        let spec = r#"
10790flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10791"#
10792        .parse::<Spec>()
10793        .unwrap();
10794
10795        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10796
10797        let flag = parsed
10798            .flags
10799            .keys()
10800            .find(|flag| flag.name == "args")
10801            .expect("expected args flag");
10802        let value = parsed.flags.get(flag).expect("expected args value");
10803        match value {
10804            ParseValue::MultiString(values) => {
10805                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10806            }
10807            _ => panic!("expected MultiString, got {value:?}"),
10808        }
10809    }
10810
10811    #[test]
10812    fn test_require_equals_accepts_attached_and_refuses_detached() {
10813        let spec = r#"
10814flag "--inspect <PORT>" require_equals=#true
10815"#
10816        .parse::<Spec>()
10817        .unwrap();
10818
10819        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10820        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10821
10822        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10823        let msg = format!("{err}");
10824        assert!(
10825            msg.contains("requires an argument") || msg.contains("inspect"),
10826            "detached value must be refused: {msg}"
10827        );
10828    }
10829
10830    #[test]
10831    fn boolean_flags_can_accept_attached_values_when_enabled() {
10832        let spec: Spec = r#"
10833name "ex"
10834bin "ex"
10835flag "--color" negate="--no-color" bool_value=#true
10836arg "[rest]"
10837"#
10838        .parse()
10839        .unwrap();
10840
10841        for (token, expected) in [
10842            ("--color", true),
10843            ("--color=true", true),
10844            ("--color=false", false),
10845            ("--no-color", false),
10846            ("--no-color=false", true),
10847        ] {
10848            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10849            assert!(
10850                matches!(
10851                    parsed.flags.get(&spec.cmd.flags[0]),
10852                    Some(ParseValue::Bool(value)) if *value == expected
10853                ),
10854                "{token}"
10855            );
10856        }
10857
10858        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10859        assert!(matches!(
10860            parsed.args.get(&spec.cmd.args[0]),
10861            Some(ParseValue::String(value)) if value == "word"
10862        ));
10863        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10864        assert!(err.to_string().contains("expected `true` or `false`"));
10865
10866        let strict: Spec = r#"
10867name "ex"
10868bin "ex"
10869args_override_self #false
10870flag "--color" negate="--no-color" bool_value=#true
10871"#
10872        .parse()
10873        .unwrap();
10874        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10875        let parsed = parse(
10876            &strict,
10877            &input(&["ex", "--color=false", "--no-color=false"]),
10878        )
10879        .unwrap();
10880        assert!(matches!(
10881            parsed.flags.get(&strict.cmd.flags[0]),
10882            Some(ParseValue::Bool(true))
10883        ));
10884    }
10885
10886    #[test]
10887    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10888        let spec = r#"
10889flag "-a --all"
10890flag "-i --inspect <PORT>" require_equals=#true
10891"#
10892        .parse::<Spec>()
10893        .unwrap();
10894
10895        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10896        let msg = format!("{err}");
10897        assert!(
10898            msg.contains("requires an argument") || msg.contains("inspect"),
10899            "bundled short must refuse the following word: {msg}"
10900        );
10901    }
10902
10903    #[test]
10904    fn test_default_missing_binds_when_the_value_is_left_off() {
10905        let spec = r#"
10906flag "-c --color <WHEN>" default_missing="always"
10907flag "-v --verbose"
10908"#
10909        .parse::<Spec>()
10910        .unwrap();
10911
10912        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10913        assert_eq!(flag_string_value(&parsed, "color"), "always");
10914
10915        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10916        assert_eq!(flag_string_value(&parsed, "color"), "never");
10917
10918        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10919        assert_eq!(flag_string_value(&parsed, "color"), "never");
10920
10921        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10922        assert_eq!(flag_string_value(&parsed, "color"), "always");
10923        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10924
10925        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10926        assert_eq!(flag_string_value(&parsed, "color"), "");
10927
10928        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10929        assert_eq!(flag_string_value(&parsed, "color"), "never");
10930
10931        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10932        assert_eq!(flag_string_value(&parsed, "color"), "always");
10933        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10934    }
10935
10936    #[test]
10937    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10938        let spec = r#"
10939flag "--apps <N>"
10940flag "--jobs <N>" default_missing="default missing"
10941flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10942"#
10943        .parse::<Spec>()
10944        .unwrap();
10945
10946        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10947        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10948
10949        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10950        assert!(
10951            err.to_string().contains("unexpected word: -1"),
10952            "default_missing must keep an unopted negative value separate: {err}"
10953        );
10954
10955        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10956        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10957
10958        let external_spec = r#"
10959external_subcommand #true
10960flag "--apps <N>"
10961"#
10962        .parse::<Spec>()
10963        .unwrap();
10964        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10965        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10966        assert!(parsed.external.is_none());
10967
10968        for words in [
10969            &["test", "--apps", "--jobs", "1"][..],
10970            &["test", "--apps", "--kids", "-1"][..],
10971        ] {
10972            let err = parse(&spec, &input(words)).unwrap_err();
10973            let message = err.to_string();
10974            assert!(
10975                message.contains("--apps") && message.contains("requires an argument"),
10976                "the earlier flag must report its missing value for {words:?}: {message}"
10977            );
10978        }
10979    }
10980
10981    #[test]
10982    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10983        let spec = r#"
10984flag "--bump [LEVEL]" value_optional=#true
10985flag "--verbose"
10986arg "[FILE]"
10987"#
10988        .parse::<Spec>()
10989        .unwrap();
10990
10991        let absent = parse(&spec, &input(&["test"])).unwrap();
10992        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10993
10994        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10995        let bump = bare
10996            .flags
10997            .iter()
10998            .find(|(flag, _)| flag.name == "bump")
10999            .map(|(_, value)| value)
11000            .unwrap();
11001        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
11002        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
11003        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
11004
11005        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
11006        assert_eq!(flag_string_value(&explicit, "bump"), "");
11007
11008        let corrected = parse(
11009            &spec,
11010            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
11011        )
11012        .unwrap();
11013        let bump = corrected
11014            .flags
11015            .iter()
11016            .find(|(flag, _)| flag.name == "bump")
11017            .map(|(_, value)| value)
11018            .unwrap();
11019        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
11020
11021        let collecting = r#"
11022flag "--tag [TAG]..." value_optional=#true
11023flag "--verbose"
11024"#
11025        .parse::<Spec>()
11026        .unwrap();
11027        let valued = parse(
11028            &collecting,
11029            &input(&["test", "--tag", "one", "two", "--verbose"]),
11030        )
11031        .unwrap();
11032        let tag = valued
11033            .flags
11034            .iter()
11035            .find(|(flag, _)| flag.name == "tag")
11036            .map(|(_, value)| value)
11037            .unwrap();
11038        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
11039    }
11040
11041    #[test]
11042    fn test_repeatable_bare_optional_values_count_each_occurrence() {
11043        let spec = r#"
11044flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
11045"#
11046        .parse::<Spec>()
11047        .unwrap();
11048
11049        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
11050        let tag = parsed
11051            .flags
11052            .iter()
11053            .find(|(flag, _)| flag.name == "tag")
11054            .map(|(_, value)| value)
11055            .unwrap();
11056        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
11057
11058        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
11059        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
11060    }
11061
11062    #[test]
11063    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
11064        let spec = r#"
11065flag "--tag [TAG]..." var=#true value_optional=#true
11066flag "--verbose"
11067"#
11068        .parse::<Spec>()
11069        .unwrap();
11070
11071        for argv in [
11072            &["test", "--tag", "one", "two"][..],
11073            &["test", "--tag", "one", "two", "--verbose"][..],
11074            &["test", "--tag", "one", "--tag", "two"][..],
11075        ] {
11076            let parsed = parse(&spec, &input(argv)).unwrap();
11077            let tag = parsed
11078                .flags
11079                .iter()
11080                .find(|(flag, _)| flag.name == "tag")
11081                .map(|(_, value)| value)
11082                .unwrap();
11083            assert!(
11084                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
11085                "argv={argv:?}: {tag:?}"
11086            );
11087        }
11088
11089        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
11090        let tag = bare
11091            .flags
11092            .iter()
11093            .find(|(flag, _)| flag.name == "tag")
11094            .map(|(_, value)| value)
11095            .unwrap();
11096        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
11097    }
11098
11099    #[test]
11100    fn test_default_missing_with_require_equals_refuses_the_following_word() {
11101        let spec = r#"
11102flag "--inspect <PORT>" require_equals=#true default_missing="9229"
11103arg "[rest]"
11104"#
11105        .parse::<Spec>()
11106        .unwrap();
11107
11108        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
11109        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
11110
11111        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
11112        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
11113
11114        // The following word is not the value; the missing value is, and 80 is a positional.
11115        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
11116        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
11117        assert_eq!(
11118            parsed
11119                .args
11120                .values()
11121                .next()
11122                .map(|v| v.to_string())
11123                .as_deref(),
11124            Some("80")
11125        );
11126
11127        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
11128        assert_eq!(flag_string_value(&parsed, "inspect"), "");
11129    }
11130
11131    #[test]
11132    fn test_default_missing_must_be_a_choice() {
11133        let spec = r#"
11134flag "--color <WHEN>" default_missing="always" {
11135    choices "auto" "always" "never"
11136}
11137"#
11138        .parse::<Spec>()
11139        .unwrap();
11140
11141        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
11142        assert_eq!(flag_string_value(&parsed, "color"), "always");
11143
11144        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
11145        assert_eq!(flag_string_value(&parsed, "color"), "never");
11146
11147        let spec = r#"
11148flag "--color <WHEN>" default_missing="wat" {
11149    choices "auto" "always" "never"
11150}
11151"#
11152        .parse::<Spec>()
11153        .unwrap();
11154
11155        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
11156        let msg = format!("{err}");
11157        assert!(
11158            msg.contains("Invalid choice for option color: wat"),
11159            "missing default has to pass choices the same way a typed value does: {msg}"
11160        );
11161
11162        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
11163        let msg = format!("{err}");
11164        assert!(
11165            msg.contains("Invalid choice for option color: wat"),
11166            "an attached value that is not a choice is still refused: {msg}"
11167        );
11168
11169        let spec = r#"
11170flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
11171    choices "9229" "80"
11172}
11173arg "[rest]"
11174"#
11175        .parse::<Spec>()
11176        .unwrap();
11177
11178        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
11179        let msg = format!("{err}");
11180        assert!(
11181            msg.contains("Invalid choice for option inspect: wat"),
11182            "require_equals still binds the missing string, so the error is the choice: {msg}"
11183        );
11184    }
11185
11186    #[test]
11187    fn test_hyphen_values_still_start_short_flag_parsing() {
11188        let spec = r#"
11189flag "-d --working-dir <DIR>"
11190flag "-a --args <ARGS>"
11191"#
11192        .parse::<Spec>()
11193        .unwrap();
11194
11195        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
11196        let message = err.to_string();
11197        assert!(
11198            message.contains("--args") && message.contains("requires an argument"),
11199            "the recognized -d must leave the earlier -a missing: {message}"
11200        );
11201    }
11202
11203    /// `available_flags` has to agree with what an actual parse accepts, since
11204    /// its whole reason to exist is answering that question without one.
11205    mod available_flags {
11206        use super::*;
11207
11208        fn spec() -> Spec {
11209            r#"
11210bin "test"
11211flag "-v --verbose" global=#true
11212flag "--raw" global=#true effect="write"
11213flag "--local-only"
11214cmd "run" {
11215    flag "-r --raw"
11216    flag "-w --watch"
11217    cmd "once"
11218}
11219"#
11220            .parse::<Spec>()
11221            .unwrap()
11222        }
11223
11224        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
11225            let mut chain = vec![&spec.cmd];
11226            for segment in path {
11227                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
11228            }
11229            chain
11230        }
11231
11232        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
11233            let mut names: Vec<_> = available_flags(&chain(spec, path))
11234                .iter()
11235                .map(|f| f.name.clone())
11236                .collect();
11237            names.sort();
11238            names
11239        }
11240
11241        #[test]
11242        fn an_empty_chain_yields_nothing() {
11243            assert!(available_flags(&[]).is_empty());
11244        }
11245
11246        #[test]
11247        fn the_root_gets_its_own_flags() {
11248            let spec = spec();
11249            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
11250        }
11251
11252        #[test]
11253        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
11254            let spec = spec();
11255            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
11256        }
11257
11258        #[test]
11259        fn a_re_declared_global_is_listed_once() {
11260            // The merge can leave the long key on the merged flag and the short
11261            // key on the pre-merge one. Same flag; it must not be listed twice.
11262            let spec = r#"
11263bin "test"
11264flag "-y --yes" global=#true effect="write"
11265cmd "rm" {
11266    flag "-y --yes"
11267}
11268"#
11269            .parse::<Spec>()
11270            .unwrap();
11271            let flags = available_flags(&chain(&spec, &["rm"]));
11272            assert_eq!(flags.len(), 1, "{flags:?}");
11273            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
11274        }
11275
11276        #[test]
11277        fn a_re_declared_global_keeps_the_globals_declaration() {
11278            // `run` re-declares the long-only global `--raw` as `-r --raw`
11279            // without `global`. That is the same flag: the global's `effect`
11280            // survives, the orphan short is unioned in, and it stays global.
11281            let spec = spec();
11282            let flags = available_flags(&chain(&spec, &["run"]));
11283            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
11284            assert!(raw.global);
11285            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
11286            assert_eq!(raw.short, ['r']);
11287        }
11288
11289        #[test]
11290        fn it_matches_what_a_parse_accepts() {
11291            // The invariant. If these ever disagree, one of them is lying to a
11292            // caller about which flags a command takes.
11293            let spec = spec();
11294            for path in [vec![], vec!["run"], vec!["run", "once"]] {
11295                let argv = std::iter::once("test".to_string())
11296                    .chain(path.iter().map(|s| s.to_string()))
11297                    .collect::<Vec<_>>();
11298                let parsed = parse_partial(&spec, &argv).unwrap();
11299
11300                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
11301                    .map(|f| f.name.clone())
11302                    .collect();
11303                from_parse.sort();
11304                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
11305            }
11306        }
11307    }
11308
11309    // Provenance: which token bound what, and where a value came from when no token did.
11310
11311    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
11312    /// assert on the whole picture rather than on one field at a time.
11313    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
11314        parsed
11315            .tokens
11316            .iter()
11317            .find(|token| token.index == index)
11318            .unwrap_or_else(|| panic!("no token at {index}"))
11319            .roles
11320            .iter()
11321            .map(render_role)
11322            .collect()
11323    }
11324
11325    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
11326        parsed
11327            .flag_origins
11328            .iter()
11329            .find(|(f, _)| f.name == flag)
11330            .map(|(_, origins)| origins.clone())
11331            .unwrap_or_default()
11332    }
11333
11334    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
11335        let env = env
11336            .iter()
11337            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
11338            .collect();
11339        Parser::new(spec)
11340            .with_env(env)
11341            .explain(&input(words))
11342            .unwrap()
11343    }
11344
11345    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
11346        explain_with_env(spec, words, &[])
11347    }
11348
11349    #[test]
11350    fn an_attached_long_value_is_recorded_on_the_flag_token() {
11351        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11352            .parse()
11353            .unwrap();
11354
11355        let parsed = explain(&spec, &["ex", "--env=prod"]);
11356
11357        assert_eq!(roles(&parsed, 0), ["program"]);
11358        assert_eq!(
11359            roles(&parsed, 1),
11360            ["flag env as --env", "value of env = [\"prod\"], attached"]
11361        );
11362        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
11363        // form while the detached one worked, and nothing could show the difference.
11364        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
11365    }
11366
11367    #[test]
11368    fn a_detached_long_value_is_recorded_on_its_own_token() {
11369        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11370            .parse()
11371            .unwrap();
11372
11373        let parsed = explain(&spec, &["ex", "--env", "prod"]);
11374
11375        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
11376        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
11377    }
11378
11379    #[test]
11380    fn a_short_bundle_is_attributed_to_the_bundle_token() {
11381        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
11382            .parse()
11383            .unwrap();
11384
11385        let parsed = explain(&spec, &["ex", "-abj8"]);
11386
11387        // One word the caller wrote, four things it did — and the re-queued tails are
11388        // folded back onto it rather than appearing as tokens nobody typed.
11389        assert_eq!(
11390            roles(&parsed, 1),
11391            [
11392                "flag a as -a",
11393                "flag b as -b",
11394                "flag j as -j",
11395                "value of j = [\"8\"], attached",
11396            ]
11397        );
11398        assert_eq!(parsed.tokens.len(), 2);
11399    }
11400
11401    #[test]
11402    fn a_delimiter_splits_one_token_into_several_values() {
11403        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11404            .parse()
11405            .unwrap();
11406
11407        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11408
11409        assert_eq!(
11410            roles(&parsed, 2),
11411            ["value of tags = [\"a\", \"b\", \"c\"]"],
11412            "the values meant, not the word typed"
11413        );
11414    }
11415
11416    #[test]
11417    fn a_separator_and_the_words_after_it_are_distinguished() {
11418        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11419            .parse()
11420            .unwrap();
11421
11422        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11423
11424        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11425        assert_eq!(roles(&parsed, 2), ["separator"]);
11426        // Past the separator `-x` is data, not an unknown flag.
11427        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11428    }
11429
11430    #[test]
11431    fn a_second_separator_is_data() {
11432        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11433            .parse()
11434            .unwrap();
11435
11436        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11437
11438        assert_eq!(roles(&parsed, 1), ["separator"]);
11439        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11440    }
11441
11442    #[test]
11443    fn an_unknown_flag_says_what_took_it() {
11444        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11445            .parse()
11446            .unwrap();
11447
11448        let parsed = explain(&spec, &["ex", "--wat"]);
11449
11450        // The default is lax, so the word became data. Which is the useful thing to be
11451        // told: the alternative reading is "you have a typo".
11452        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11453    }
11454
11455    #[test]
11456    fn a_subcommand_word_is_not_a_positional() {
11457        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
11458            .parse()
11459            .unwrap();
11460
11461        let parsed = explain(&spec, &["ex", "build", "a"]);
11462
11463        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11464        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11465    }
11466
11467    #[test]
11468    fn a_multicall_applet_is_read_at_argv0() {
11469        let spec: Spec =
11470            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
11471                .parse()
11472                .unwrap();
11473
11474        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11475
11476        // argv[0] is both the program and the word that selected the applet, and the word
11477        // read there is not the word the caller wrote.
11478        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11479        assert!(parsed.tokens[0].synthesized);
11480        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11481    }
11482
11483    #[test]
11484    fn words_the_parse_never_reached_say_so() {
11485        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11486            .parse()
11487            .unwrap();
11488
11489        let parsed = Parser::new(&spec)
11490            .explain(&input(&["ex", "--help", "a"]))
11491            .unwrap();
11492
11493        assert_eq!(roles(&parsed, 2), ["unread"]);
11494    }
11495
11496    #[test]
11497    fn an_env_origin_names_the_variable_that_fired() {
11498        let spec: Spec =
11499            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11500                .parse()
11501                .unwrap();
11502
11503        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11504        assert_eq!(
11505            origins(&primary, "token"),
11506            [ValueOrigin::Env("EX_TOKEN".to_string())]
11507        );
11508
11509        // The fallback firing is a different fact from the primary firing, and which one it
11510        // was is what says which declaration to delete.
11511        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11512        assert_eq!(
11513            origins(&fallback, "token"),
11514            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11515        );
11516    }
11517
11518    #[test]
11519    fn a_default_origin_is_recorded_for_flags_and_args() {
11520        let spec: Spec =
11521            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11522                .parse()
11523                .unwrap();
11524
11525        let parsed = explain(&spec, &["ex"]);
11526
11527        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11528        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11529        assert_eq!(arg.name, "src");
11530        assert_eq!(origins, &[ValueOrigin::Default]);
11531    }
11532
11533    #[test]
11534    fn a_default_if_origin_carries_the_condition_that_fired() {
11535        let spec: Spec = r#"
11536name "ex"
11537bin "ex"
11538flag "--profile <p>"
11539flag "--strict" {
11540    default_if "--profile" "prod" "true"
11541}
11542        "#
11543        .parse()
11544        .unwrap();
11545
11546        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11547
11548        // The selector alone is ambiguous: several conditions may name it with different
11549        // `when` values, so the report has to say which one matched.
11550        assert_eq!(
11551            origins(&parsed, "strict"),
11552            [ValueOrigin::DefaultIf {
11553                selector: "--profile".to_string(),
11554                when: Some("prod".to_string()),
11555            }]
11556        );
11557    }
11558
11559    #[test]
11560    fn a_bare_optional_value_flag_records_default_missing() {
11561        let spec: Spec =
11562            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11563                .parse()
11564                .unwrap();
11565
11566        let parsed = explain(&spec, &["ex", "--color", "-v"]);
11567
11568        // The flag was typed and the value was not, which is the distinction a spec author
11569        // is asking about when they ask why `--color` came out `always`.
11570        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11571        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11572        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11573    }
11574
11575    #[test]
11576    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11577        let spec: Spec =
11578            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11579                .parse()
11580                .unwrap();
11581
11582        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11583
11584        // Why origins are a list: one declaration, two occurrences, two different answers.
11585        assert_eq!(
11586            roles(&parsed, 1),
11587            [
11588                "flag color as --color",
11589                "value of color = [\"red\"], attached"
11590            ]
11591        );
11592        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11593    }
11594
11595    #[test]
11596    fn an_override_names_the_flag_that_did_it() {
11597        let spec: Spec =
11598            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11599                .parse()
11600                .unwrap();
11601
11602        let parsed = explain(&spec, &["ex", "--loud"]);
11603
11604        // Without the overriding name, "`--quiet` is unset despite its default" has no
11605        // answer: the fallback phase silently declines to fill an overridden flag.
11606        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11607        assert!(origins(&parsed, "quiet").is_empty());
11608    }
11609
11610    #[test]
11611    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11612        let spec: Spec = r#"
11613name "ex"
11614bin "ex"
11615cmd "run" restart_token=":::" {
11616    arg "<task>" default="build"
11617}
11618        "#
11619        .parse()
11620        .unwrap();
11621
11622        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11623
11624        // The values belong to the last invocation, so provenance must too — but the words
11625        // of the first were still read, and a report that dropped them would show a command
11626        // line with a hole in it.
11627        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11628        // And the token that did the resetting says so: without a role of its own it reads
11629        // as a word that did nothing, next to a `lint` that filled an arg now empty.
11630        assert_eq!(roles(&parsed, 3), ["restart"]);
11631        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11632        assert!(parsed.arg_origins.is_empty());
11633    }
11634
11635    #[test]
11636    fn a_value_terminator_says_which_run_it_ended() {
11637        let spec: Spec = r#"
11638name "ex"
11639bin "ex"
11640flag "--exec <cmd>..." value_terminator=";"
11641arg "<src>"
11642        "#
11643        .parse()
11644        .unwrap();
11645
11646        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11647
11648        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11649        // The terminator is consumed and is not one of the values, which is the whole reason
11650        // it was declared — so it needs a row saying that rather than an empty one.
11651        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11652        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11653    }
11654
11655    #[test]
11656    fn an_args_value_terminator_says_which_run_it_ended() {
11657        let spec: Spec = r#"
11658name "ex"
11659bin "ex"
11660arg "<files>..." value_terminator=";"
11661arg "[dest]"
11662        "#
11663        .parse()
11664        .unwrap();
11665
11666        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11667
11668        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11669        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11670        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11671    }
11672
11673    #[test]
11674    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11675        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11676            .parse()
11677            .unwrap();
11678
11679        let parsed = Parser::new(&spec)
11680            .explain(&input(&["ex", "--env=prod"]))
11681            .unwrap();
11682
11683        // `parse` reports "missing required <src>" and nothing else, which is the report the
11684        // caller already had. This is the case the whole thing exists for.
11685        assert!(
11686            Parser::new(&spec)
11687                .parse(&input(&["ex", "--env=prod"]))
11688                .is_err()
11689        );
11690        assert_eq!(
11691            roles(&parsed, 1),
11692            ["flag env as --env", "value of env = [\"prod\"], attached"]
11693        );
11694        assert!(
11695            parsed.errors.iter().any(|e| e.to_string().contains("src")),
11696            "{:?}",
11697            parsed.errors
11698        );
11699    }
11700
11701    #[test]
11702    fn an_external_subcommand_forwards_whole_tokens() {
11703        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11704            .parse()
11705            .unwrap();
11706
11707        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11708
11709        assert_eq!(roles(&parsed, 1), ["external"]);
11710        assert_eq!(roles(&parsed, 2), ["external"]);
11711    }
11712
11713    #[test]
11714    fn a_view_keeps_the_callers_argv_positions() {
11715        let spec: Spec = r#"
11716bin "ex"
11717view "runner" root="run"
11718cmd "run" {
11719    flag "--token <token>"
11720}
11721        "#
11722        .parse()
11723        .unwrap();
11724
11725        let parsed = explain(&spec, &["runner", "--token", "secret"]);
11726
11727        // A view re-enters the parse with the same argv, so the positions still mean what
11728        // the caller wrote.
11729        assert_eq!(roles(&parsed, 0), ["program"]);
11730        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11731    }
11732
11733    #[test]
11734    fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11735        let spec: Spec = r#"
11736name "ex"
11737bin "ex"
11738default_subcommand "run"
11739default_subcommand_on_empty #true
11740flag "--value <value>"
11741cmd "run" arg_required_else_help=#true
11742"#
11743        .parse()
11744        .unwrap();
11745        let err = Parser::new(&spec)
11746            .parse(&input(&["ex", "--value"]))
11747            .unwrap_err();
11748        assert!(err.to_string().contains("requires an argument"), "{err}");
11749        assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11750    }
11751
11752    #[test]
11753    fn empty_default_honors_parent_args_conflict_policy() {
11754        let spec: Spec = r#"
11755name "ex"
11756bin "ex"
11757default_subcommand "run"
11758default_subcommand_on_empty #true
11759args_conflicts_with_subcommands #true
11760flag "--verbose"
11761cmd "run"
11762"#
11763        .parse()
11764        .unwrap();
11765        let err = Parser::new(&spec)
11766            .parse(&input(&["ex", "--verbose"]))
11767            .unwrap_err();
11768        assert!(
11769            err.to_string().contains("cannot be used with arguments"),
11770            "{err}"
11771        );
11772    }
11773
11774    #[test]
11775    fn the_parse_records_which_flags_a_mounted_program_declared() {
11776        let mut spec: Spec = r#"
11777bin ex
11778flag "-p --pick <tool>" global=#true
11779cmd run {
11780    mount run=tasks
11781}
11782"#
11783        .parse()
11784        .unwrap();
11785        let mounted = r#"
11786cmd outer {
11787    cmd inner {
11788        flag "-p --other <name>"
11789    }
11790}
11791cmd deep {
11792    flag "--pick <tool>" global=#true
11793    cmd leaf {
11794        flag "-p --pick <tool>"
11795    }
11796}
11797"#;
11798        spec.resolve_mount_outputs(&HashMap::from([("tasks".to_string(), mounted.to_string())]))
11799            .unwrap();
11800        let pending_is_mounted = |words: &[&str]| {
11801            let words: Vec<String> = words.iter().map(|word| word.to_string()).collect();
11802            let out = parse_partial(&spec, &words).unwrap();
11803            let flag = out
11804                .flag_awaiting_value
11805                .first()
11806                .expect("a flag awaiting its value");
11807            out.is_mounted_flag(flag)
11808        };
11809
11810        assert!(!pending_is_mounted(&["ex", "--pick"]));
11811        // `inner`'s own `-p` collides with the host's global `-p`, which the parser keeps.
11812        assert!(!pending_is_mounted(&["ex", "run", "outer", "inner", "-p"]));
11813        assert!(!pending_is_mounted(&[
11814            "ex", "run", "outer", "inner", "--pick"
11815        ]));
11816        assert!(pending_is_mounted(&[
11817            "ex", "run", "outer", "inner", "--other"
11818        ]));
11819        // `leaf` re-declares `deep`'s global `--pick`, and the parser merges the two into one
11820        // flag that matches neither declaration. It is still the mounted program's.
11821        assert!(pending_is_mounted(&["ex", "run", "deep", "--pick"]));
11822        assert!(pending_is_mounted(&["ex", "run", "deep", "leaf", "--pick"]));
11823        assert!(!pending_is_mounted(&["ex", "run", "deep", "leaf", "-p"]));
11824    }
11825
11826    #[test]
11827    fn empty_default_mount_matches_explicit_selection() {
11828        let spec: Spec = r#"
11829name "ex"
11830bin "ex"
11831default_subcommand "run"
11832default_subcommand_on_empty #true
11833cmd "run" { mount run="discover" }
11834"#
11835        .parse()
11836        .unwrap();
11837        let mut mounts = HashMap::new();
11838        mounts.insert(
11839            "discover".to_string(),
11840            "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11841        );
11842        for argv in [
11843            vec!["ex".to_string()],
11844            vec!["ex".to_string(), "run".to_string()],
11845        ] {
11846            let out = Parser::new(&spec)
11847                .with_mount_outputs(mounts.clone())
11848                .parse(&argv)
11849                .unwrap();
11850            assert_eq!(out.cmds.last().unwrap().name, "run");
11851            assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11852        }
11853    }
11854
11855    #[test]
11856    fn pending_parent_value_stays_pending_in_partial_parse() {
11857        let spec: Spec = r#"
11858name "ex"
11859bin "ex"
11860default_subcommand "run"
11861default_subcommand_on_empty #true
11862flag "--value <value>"
11863cmd "run" {}
11864"#
11865        .parse()
11866        .unwrap();
11867        let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11868        assert_eq!(out.cmd.name, "ex");
11869        assert_eq!(out.flag_awaiting_value[0].name, "value");
11870    }
11871
11872    #[test]
11873    fn optional_default_missing_still_selects_empty_child() {
11874        let spec: Spec = r#"
11875name "ex"
11876bin "ex"
11877default_subcommand "run"
11878default_subcommand_on_empty #true
11879flag "--value <value>" default_missing="fallback"
11880cmd "run" {}
11881"#
11882        .parse()
11883        .unwrap();
11884        let out = Parser::new(&spec)
11885            .parse(&input(&["ex", "--value"]))
11886            .unwrap();
11887        assert_eq!(out.cmd.name, "run");
11888        assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11889    }
11890
11891    #[test]
11892    fn empty_default_resets_cursor_after_root_sigil() {
11893        let spec: Spec = r#"
11894name "ex"
11895bin "ex"
11896default_subcommand "run"
11897default_subcommand_on_empty #true
11898arg "[tool]..." sigil="@"
11899cmd "run" { arg "[path]" }
11900"#
11901        .parse()
11902        .unwrap();
11903        let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11904        assert_eq!(out.cmd.name, "run");
11905        assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11906    }
11907
11908    #[test]
11909    fn root_clause_positional_prevents_empty_default() {
11910        let spec: Spec = r#"
11911name "ex"
11912bin "ex"
11913default_subcommand "run"
11914default_subcommand_on_empty #true
11915clause "tasks" {
11916  arg "<number>" allow_negative_numbers=#true
11917}
11918cmd "run" { arg "[task]" }
11919"#
11920        .parse()
11921        .unwrap();
11922        let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11923        assert!(
11924            out.cmds.iter().all(|cmd| cmd.name != "run"),
11925            "cmds={:?} args={:?}",
11926            out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11927            out.args
11928        );
11929        let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11930        assert_eq!(
11931            out.clauses["tasks"][0].get(number).unwrap().to_string(),
11932            "-7"
11933        );
11934    }
11935}