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        // Applied once, here, because this is where the CLI's own version is known: a
1205        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1206        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1207        Ok(out)
1208    }
1209}
1210
1211/// Parse command-line arguments according to a spec.
1212///
1213/// Returns the parsed arguments and flags, with defaults and env vars applied.
1214/// Uses `std::env::var` for environment variable lookups.
1215///
1216/// For custom environment variable handling, use [`Parser`] instead.
1217#[must_use = "parsing result should be used"]
1218pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1219    Parser::new(spec).parse(input)
1220}
1221
1222/// Parse command-line arguments without applying defaults.
1223///
1224/// Use this for help text generation or when you need the raw parsed values.
1225#[must_use = "parsing result should be used"]
1226pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1227    parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1228}
1229
1230/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1231/// `.exe` stripped so Windows and Unix agree.
1232pub fn multicall_basename(argv0: &str) -> &str {
1233    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1234    match name.get(name.len().saturating_sub(4)..) {
1235        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1236        _ => name,
1237    }
1238}
1239
1240/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1241///
1242/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1243/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1244pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1245    let base = multicall_basename(argv0);
1246    if !name.is_empty() && base == multicall_basename(name) {
1247        return None;
1248    }
1249    if let Some(bin) = bin
1250        && !bin.is_empty()
1251        && base == multicall_basename(bin)
1252    {
1253        return None;
1254    }
1255    Some(base)
1256}
1257
1258/// Internal version of parse_partial that accepts an optional custom env map.
1259/// When a command's own `mount` runs, for the root — which nothing descends into.
1260///
1261/// A completion has to know every command before it can offer one, even with
1262/// nothing typed yet, so it resolves up front. An execution knows the word it was
1263/// given, so it only pays for discovery when that word matches nothing declared —
1264/// and a CLI that declares its commands and mounts a few more does not spawn a
1265/// process on every invocation.
1266#[derive(Clone, Copy, PartialEq, Eq)]
1267enum MountTiming {
1268    Eager,
1269    WhenAWordIsUnknown,
1270}
1271
1272/// One word on its way through the parser, with what the parser has learned about it.
1273///
1274/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1275/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1276/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1277/// the word itself there is nothing to keep aligned. The argv position is here for the
1278/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1279/// removed from the middle, so position in the queue stops meaning position in argv on the
1280/// first descent.
1281struct Token {
1282    word: String,
1283    /// Where in the caller's argv this word came from.
1284    ///
1285    /// A word the parser made up points at the token it was derived from — the tail of a
1286    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1287    /// token a reader would point at, and there is nothing else to point at.
1288    argv: usize,
1289    /// The flag Phase 1 read this word as, and the command level it read it at.
1290    ///
1291    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1292    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1293    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1294    /// then the recognized flags have changed, because each descent drops the parent's
1295    /// non-global flags and a mounted command may declare the same name as a global seen
1296    /// here. Recording the owner keeps a word bound to the flag it was read as.
1297    ///
1298    /// The level matters to strict parsing: clap permits an inherited global once on each
1299    /// side of a subcommand boundary.
1300    binding: Option<(Arc<SpecFlag>, usize)>,
1301}
1302
1303/// Flag scope for the one token straddling an implicit default-command boundary.
1304struct DefaultBundle {
1305    argv: usize,
1306    flags: BTreeMap<String, Arc<SpecFlag>>,
1307    parent_keys: HashSet<String>,
1308}
1309
1310impl DefaultBundle {
1311    /// Preserve the declaration and command level when a short tail is requeued.
1312    fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1313        self.flags.get(key).map(|flag| {
1314            (
1315                Arc::clone(flag),
1316                usize::from(!self.parent_keys.contains(key)),
1317            )
1318        })
1319    }
1320}
1321
1322impl Token {
1323    fn new(word: String, argv: usize) -> Self {
1324        Self {
1325            word,
1326            argv,
1327            binding: None,
1328        }
1329    }
1330}
1331
1332/// The token trace, while it is being built.
1333///
1334/// One row per word of the caller's argv, so a role can be recorded against a position
1335/// without the recorder having to know how many words came before it. Words the parser
1336/// made up have no row of their own and fold onto the row they were derived from.
1337struct Trace {
1338    tokens: Vec<TokenBinding>,
1339}
1340
1341impl Trace {
1342    fn new(input: &[String]) -> Self {
1343        Self {
1344            tokens: input
1345                .iter()
1346                .enumerate()
1347                .map(|(index, word)| TokenBinding {
1348                    index,
1349                    word: word.clone(),
1350                    synthesized: false,
1351                    roles: vec![],
1352                })
1353                .collect(),
1354        }
1355    }
1356
1357    fn record(&mut self, argv: usize, role: TokenRole) {
1358        if let Some(token) = self.tokens.get_mut(argv) {
1359            token.roles.push(role);
1360        }
1361    }
1362
1363    /// Note that what was read at this position is not what the caller wrote there.
1364    fn note_synthesized(&mut self, argv: usize) {
1365        if let Some(token) = self.tokens.get_mut(argv) {
1366            token.synthesized = true;
1367        }
1368    }
1369
1370    /// Every word the parse never reached, once it has stopped.
1371    fn close(&mut self, unread: &VecDeque<Token>) {
1372        for token in unread {
1373            self.record(token.argv, TokenRole::Unread);
1374        }
1375    }
1376}
1377
1378fn parse_partial_with_env(
1379    spec: &Spec,
1380    input: &[String],
1381    custom_env: Option<&HashMap<String, String>>,
1382    mount_outputs: Option<&HashMap<String, String>>,
1383    mount_timing: MountTiming,
1384    validate_clauses: bool,
1385) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1386    let mut trace = Trace::new(input);
1387    parse_partial_traced(
1388        spec,
1389        input,
1390        custom_env,
1391        mount_outputs,
1392        mount_timing,
1393        validate_clauses,
1394        &mut trace,
1395    )
1396}
1397
1398/// Locate the first default-only flag, while preserving an explicit sibling selector.
1399/// This only reads declarations: it neither binds values nor executes mount commands.
1400fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1401    let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1402    let parent = gather_flags(root);
1403    let child = gather_flags(default);
1404    let path = std::slice::from_ref(root);
1405    let mut at = None;
1406    let mut i = 0;
1407    while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1408        let scope = if at.is_some() { default } else { root };
1409        if token == "--"
1410            || token == "-"
1411            || scope
1412                .clause
1413                .as_ref()
1414                .is_some_and(|c| c.separator.as_deref() == Some(token))
1415        {
1416            return at;
1417        }
1418        if !is_flag_like(token) {
1419            if at.is_none()
1420                && (root.find_subcommand(token).is_some()
1421                    || (token == "help"
1422                        && spec.disable_help != Some(true)
1423                        && !root.disable_help_subcommand))
1424            {
1425                return None;
1426            }
1427            return at;
1428        }
1429        let mut value_flag = None;
1430        let mut attached = None;
1431        if token.starts_with("--") {
1432            let (name, value) = token
1433                .split_once('=')
1434                .map_or((token, None), |(n, v)| (n, Some(v)));
1435            if !parent.contains_key(name)
1436                && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1437            {
1438                i += 1;
1439                continue;
1440            }
1441            let flag = parent.get(name).or_else(|| child.get(name));
1442            let Some(flag) = flag else {
1443                return at;
1444            };
1445            if !parent.contains_key(name) {
1446                at.get_or_insert(i);
1447            }
1448            if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1449                value_flag = Some(flag);
1450                attached = value;
1451            }
1452        } else {
1453            for (offset, letter) in token.char_indices().skip(1) {
1454                let key = format!("-{letter}");
1455                if !parent.contains_key(&key) && supplies_short(spec, path, letter) {
1456                    continue;
1457                }
1458                let flag = parent.get(&key).or_else(|| child.get(&key));
1459                let Some(flag) = flag else {
1460                    return at;
1461                };
1462                if !parent.contains_key(&key) {
1463                    at.get_or_insert(i);
1464                }
1465                if flag.arg.is_some() {
1466                    value_flag = Some(flag);
1467                    let rest = &token[offset + letter.len_utf8()..];
1468                    attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1469                    break;
1470                }
1471            }
1472        }
1473        i += 1;
1474        if let Some(flag) = value_flag {
1475            let scope = if at.is_some() { default } else { root };
1476            let is_separator = |next: &str| {
1477                scope
1478                    .clause
1479                    .as_ref()
1480                    .is_some_and(|c| c.separator.as_deref() == Some(next))
1481            };
1482            let first = if attached.is_some() {
1483                attached
1484            } else {
1485                input
1486                    .get(i)
1487                    .map(|t| t.word.as_str())
1488                    .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1489                    .inspect(|_| i += 1)
1490            };
1491            if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1492                return at;
1493            }
1494            let arg = flag.arg.as_ref().expect("value flag");
1495            if arg.var {
1496                let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1497                let mut count = first.map_or(0, count_values);
1498                while arg.var_max.is_none_or(|max| count < max) {
1499                    let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1500                        break;
1501                    };
1502                    if is_separator(next) {
1503                        break;
1504                    }
1505                    if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1506                        if next != "--" {
1507                            i += 1;
1508                        }
1509                        break;
1510                    }
1511                    if is_flag_like(next)
1512                        && !(arg.allow_negative_numbers && is_negative_number(next))
1513                    {
1514                        break;
1515                    }
1516                    if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1517                        break;
1518                    }
1519                    count += count_values(next);
1520                    i += 1;
1521                }
1522            }
1523        }
1524    }
1525    at
1526}
1527
1528/// The binding phase, with the trace left somewhere the caller can still read it.
1529///
1530/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1531/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1532/// words read before the failure are most of what a report wants, so the caller owns the
1533/// trace instead and keeps them. See [`Parser::explain_refused`].
1534fn parse_partial_traced(
1535    spec: &Spec,
1536    input: &[String],
1537    custom_env: Option<&HashMap<String, String>>,
1538    mount_outputs: Option<&HashMap<String, String>>,
1539    mount_timing: MountTiming,
1540    validate_clauses: bool,
1541    trace: &mut Trace,
1542) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1543    // Each `choices run=` runs once per parse, however many values are checked against it.
1544    let _runs = crate::spec::choices::RunScope::enter();
1545    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1546        let viewed = spec.for_view(view)?;
1547        return parse_partial_traced(
1548            &viewed,
1549            input,
1550            custom_env,
1551            mount_outputs,
1552            mount_timing,
1553            validate_clauses,
1554            trace,
1555        );
1556    }
1557    trace!("parse_partial: {input:?}");
1558    let mut input = input
1559        .iter()
1560        .enumerate()
1561        .map(|(argv, word)| Token::new(word.clone(), argv))
1562        .collect::<VecDeque<_>>();
1563    let argv0 = input.pop_front();
1564    if let Some(argv0) = argv0.as_ref() {
1565        trace.record(argv0.argv, TokenRole::Program);
1566    }
1567    if spec.multicall
1568        && let Some(raw) = argv0
1569        && let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str()))
1570    {
1571        // A symlink invocation reads a word the caller never typed — the basename of
1572        // the program itself — so argv[0] is both the program and, below, whatever
1573        // that word selects.
1574        trace.note_synthesized(raw.argv);
1575        input.push_front(Token::new(applet.to_string(), raw.argv));
1576    }
1577    // The policy observes the selected command's own argv, not values eventually filled from
1578    // env/default. Start at the root, then reset on every explicit descent. A default
1579    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1580    let mut command_has_argv = !input.is_empty();
1581    // Unlike `out.args`, this survives clause finalization and restart rewinds. It records
1582    // whether the root actually consumed a positional token before empty-default routing.
1583    let mut root_positional_arg_found = false;
1584
1585    let mut out = ParseOutput {
1586        cmd: spec.cmd.clone(),
1587        cmds: vec![spec.cmd.clone()],
1588        args: IndexMap::new(),
1589        clauses: IndexMap::new(),
1590        clause_flags: IndexMap::new(),
1591        flags: IndexMap::new(),
1592        tokens: vec![],
1593        flag_origins: IndexMap::new(),
1594        arg_origins: IndexMap::new(),
1595        overridden_flags: BTreeMap::new(),
1596        available_flags: gather_flags(&spec.cmd),
1597        mounted_flags: vec![],
1598        flag_awaiting_value: vec![],
1599        errors: vec![],
1600        warnings: vec![],
1601        next_arg: None,
1602        double_dash_seen: false,
1603        external: None,
1604    };
1605    // A spec whose mounts were resolved ahead of time can start inside a mounted program.
1606    if spec.cmd.mounted || spec.cmd.flags_from_mount {
1607        out.mounted_flags = out.available_flags.values().cloned().collect();
1608    }
1609    // Keep this internal so adding relationship support remains semver-compatible. The full
1610    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1611    let mut overridden_flags = HashSet::new();
1612    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1613    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1614    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1615    // or an inherited `-c` belonged to the ancestor.
1616    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1617
1618    // Phase 1: Scan for subcommands and collect global flags
1619    //
1620    // This phase identifies subcommands early because they may have mount points
1621    // that need to be executed with the global flags that appeared before them.
1622    //
1623    // Example: "usage --verbose run task"
1624    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1625    //   -> then finds "task" as a subcommand of "run" (if it exists)
1626    //
1627    // We only collect global flags for mounts because:
1628    // - Non-global flags are specific to the current command, not subcommands
1629    // - Global flags affect all commands and should be passed to mount points
1630    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1631    // Which flag each word skipped here belongs to is recorded on the word — see
1632    // `Token::binding`.
1633    let mut command_arg_found = false;
1634    let mut variadic_flag_active = false;
1635    let mut idx = 0;
1636    // Track whether we've already applied the default_subcommand to prevent
1637    // multiple switches (e.g., if default is "run" and there's a task named "run")
1638    let mut used_default_subcommand = false;
1639    let mut default_bundle: Option<DefaultBundle> = None;
1640    // Whether the command in scope has had its own mounts run. A mount on the root
1641    // is the case that needs this: a subcommand's mounts are run when the parser
1642    // descends into it, but nothing descends into the root.
1643    let mut mounts_resolved = false;
1644    // A completion needs the whole command list before it can offer anything, and
1645    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1646    // mean a root mount never contributed to the very thing it exists for.
1647    //
1648    // The default-subcommand gate applies here too, and has to: offering a discovered
1649    // command that a real parse would hand to the default instead would be worse than
1650    // not offering it. A root mount under a `default_subcommand` that does not say
1651    // `overrides_default` therefore contributes nothing anywhere, which is what
1652    // "the default outranks discovery" means.
1653    let default_outranks_mounts =
1654        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1655    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1656    {
1657        mounts_resolved = true;
1658        let mut mounted = out.cmd.clone();
1659        mounted.mount(&[], mount_outputs)?;
1660        out.merge_command_flags(&mounted, false);
1661        if let Some(last) = out.cmds.last_mut() {
1662            *last = mounted.clone();
1663        }
1664        out.cmd = mounted;
1665    }
1666
1667    let default_flag_at = spec
1668        .default_subcommand_flags
1669        .then(|| default_flag_route(spec, &out.cmd, &input))
1670        .flatten();
1671
1672    while idx < input.len() {
1673        // Only for a word that could name a command, and only when it matches
1674        // nothing already declared. A CLI that declares its commands and mounts more
1675        // does not spawn a process for every invocation, and a flag — `--help`, or
1676        // anything unrecognized — never triggers discovery at all, which it would
1677        // otherwise do simply by not being a subcommand.
1678        // A declared `default_subcommand` already says what an unmatched word means,
1679        // and it costs nothing — so discovery waits behind it unless a mount asks to
1680        // outrank it. Without this, a task runner would spawn its discovery process
1681        // once per task invocation.
1682        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1683            default_accepts_word(&out.cmd, name, &input[idx].word)
1684                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1685        });
1686        if !mounts_resolved
1687            && !out.cmd.mounts.is_empty()
1688            && !default_catches_it
1689            && is_command_word(&input[idx].word)
1690            && !is_negative_number(&input[idx].word)
1691            && out.cmd.find_subcommand(&input[idx].word).is_none()
1692        {
1693            mounts_resolved = true;
1694            let mut mounted = out.cmd.clone();
1695            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1696            out.merge_command_flags(&mounted, false);
1697            if let Some(last) = out.cmds.last_mut() {
1698                *last = mounted.clone();
1699            }
1700            out.cmd = mounted;
1701        }
1702        if variadic_flag_active
1703            && out.cmd.find_subcommand(&input[idx].word).is_some()
1704            && !out.cmd.subcommand_precedence_over_arg
1705        {
1706            break;
1707        }
1708        let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1709        let selected_command = if implicit_default {
1710            out.cmd
1711                .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1712        } else {
1713            out.cmd.find_subcommand(&input[idx].word)
1714        };
1715        if let Some(subcommand) = selected_command {
1716            let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1717                && implicit_default
1718                && input[idx].word.starts_with('-')
1719                && !input[idx].word.starts_with("--")
1720                && {
1721                    let child_flags = gather_flags(subcommand);
1722                    let mut found = false;
1723                    for short in input[idx].word[1..].chars() {
1724                        let key = format!("-{short}");
1725                        if out.available_flags.contains_key(&key)
1726                            || supplies_short(spec, &out.cmds, short)
1727                        {
1728                            found = true;
1729                            break;
1730                        }
1731                        match child_flags.get(&key) {
1732                            Some(flag) if flag.arg.is_none() => {}
1733                            _ => break,
1734                        }
1735                    }
1736                    found
1737                };
1738            if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1739            {
1740                bail!(
1741                    "subcommand '{}' cannot be used with arguments on its parent command",
1742                    subcommand.name
1743                );
1744            }
1745            let boundary_parent = (implicit_default
1746                && input[idx].word.starts_with('-')
1747                && !input[idx].word.starts_with("--"))
1748            .then(|| out.available_flags.clone());
1749            let mut subcommand = subcommand.clone();
1750            // Pass prefix words (global flags before this subcommand) to mount
1751            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1752            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1753            // commands are ordinary commands relative to each other.
1754            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1755            out.merge_command_flags(&subcommand, crossing_mount);
1756            if let Some(parent) = boundary_parent {
1757                let parent_keys = parent.keys().cloned().collect();
1758                let mut flags = out.available_flags.clone();
1759                flags.extend(parent);
1760                default_bundle = Some(DefaultBundle {
1761                    argv: input[idx].argv,
1762                    flags,
1763                    parent_keys,
1764                });
1765            }
1766            // Remove subcommand from input
1767            let selected = if implicit_default {
1768                used_default_subcommand = true;
1769                None
1770            } else {
1771                input.remove(idx)
1772            };
1773            if let Some(selected) = selected {
1774                trace.record(
1775                    selected.argv,
1776                    TokenRole::Command {
1777                        name: subcommand.name.clone(),
1778                    },
1779                );
1780            }
1781            command_has_argv = idx < input.len();
1782            out.cmds.push(subcommand.clone());
1783            out.cmd = subcommand.clone();
1784            // A descent already ran the new command's mounts, above.
1785            mounts_resolved = true;
1786            prefix_flags.clear();
1787            command_arg_found = false;
1788            variadic_flag_active = false;
1789            // Continue from current position (don't reset to 0)
1790            // After remove(), idx now points to the next element
1791        } else if !is_command_word(&input[idx].word)
1792            || declared_numeric_short(&out.available_flags, &input[idx].word)
1793        {
1794            // Check if this is a known flag
1795            let word = input[idx].word.clone();
1796            let flag_key = get_flag_key(&word);
1797
1798            // A short token keys on its first letter, so `-az` would be recorded as
1799            // `-a` and its tail left over. Check the whole token here, where it is
1800            // first read: a token containing an unrecognized letter is not a bundle,
1801            // and recording it as one is what let `-a` be applied from a token that
1802            // never named it.
1803            let boundary = default_bundle
1804                .as_ref()
1805                .filter(|bundle| bundle.argv == input[idx].argv);
1806            let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1807            let is_bundle =
1808                word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1809            if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1810                command_arg_found = true;
1811                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1812                // Skip the flag and keep scanning. Both global and non-global flags may precede
1813                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1814                // stopping at one would hide the subcommand — and any mount on it — from the
1815                // parse, leaving the subcommand name to be mis-read as a positional argument.
1816                //
1817                // Only globals are forwarded to mounts: a non-global flag belongs to the
1818                // command that declared it, not to what is mounted below it.
1819                input[idx].binding = boundary
1820                    .and_then(|bundle| bundle.binding(flag_key))
1821                    .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1822                let mut forwarded = f.global.then(|| vec![word.clone()]);
1823                idx += 1;
1824
1825                // Only consume next word if flag takes an argument AND value isn't embedded
1826                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1827                if f.arg.is_some()
1828                    && !word.contains('=')
1829                    && idx < input.len()
1830                    && accepts_detached_flag_value(&f, &input[idx].word)
1831                {
1832                    if let Some(words) = forwarded.as_mut() {
1833                        words.push(input[idx].word.clone());
1834                    }
1835                    idx += 1;
1836                }
1837                if let Some(words) = forwarded {
1838                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1839                    prefix_flags.push((f, words));
1840                }
1841            } else {
1842                // Unknown flag - stop looking for subcommands
1843                // Let the main parsing phase handle the error
1844                break;
1845            }
1846        } else {
1847            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1848                idx += 1;
1849                continue;
1850            }
1851            // Found a word that's not a flag or subcommand
1852            // Check if we should use the default_subcommand (only once, and only at the
1853            // root, which is the only place a spec can declare one — `out.cmds` holds just
1854            // the root until something descends). Without that second condition the one
1855            // declared name is looked up wherever the parser happens to be standing, so an
1856            // unrelated command acquires a default because a name matched one level down:
1857            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1858            // `config ls`.
1859            if !used_default_subcommand
1860                && out.cmds.len() == 1
1861                && let Some(default_name) = &spec.default_subcommand
1862                && let Some(subcommand) = out
1863                    .cmd
1864                    .find_subcommand(default_name)
1865                    .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1866            {
1867                if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1868                    bail!(
1869                        "subcommand '{}' cannot be used with arguments on its parent command",
1870                        subcommand.name
1871                    );
1872                }
1873                let mut subcommand = subcommand.clone();
1874                // Pass prefix words (global flags before this) to mount
1875                subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1876                let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1877                out.merge_command_flags(&subcommand, crossing_mount);
1878                out.cmds.push(subcommand.clone());
1879                out.cmd = subcommand.clone();
1880                command_has_argv = true;
1881                prefix_flags.clear();
1882                command_arg_found = false;
1883                variadic_flag_active = false;
1884                // This descent ran the new command's mounts, so lazy
1885                // discovery must not run them a second time.
1886                mounts_resolved = true;
1887                used_default_subcommand = true;
1888                // Continue the loop to check if this word is a subcommand of the
1889                // default subcommand (e.g., a task name added via mount).
1890                // If it's not a subcommand, the next iteration will break and
1891                // Phase 2 will handle it as a positional arg.
1892                continue;
1893            }
1894            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1895            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1896            // default subcommand gets first refusal so interpreted and compiled routing agree
1897            // when the root sigil and the default command can both accept this word.
1898            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1899                idx += 1;
1900                continue;
1901            }
1902            // An unmatched word that names no subcommand is forwarded as an external
1903            // command: this word, then every token after it, including flags. Known
1904            // subcommands already won above, and a default_subcommand already caught.
1905            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1906            if out.cmd.external_subcommand {
1907                let rest: Vec<Token> = input.drain(idx..).collect();
1908                for token in &rest {
1909                    trace.record(token.argv, TokenRole::External);
1910                }
1911                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1912                break;
1913            }
1914            // This could be a positional argument, so stop subcommand search
1915            break;
1916        }
1917    }
1918
1919    // Phase 2: Main argument and flag parsing
1920    //
1921    // Now that we've identified all subcommands and executed their mounts,
1922    // we can parse the remaining arguments, flags, and their values.
1923
1924    // The cursor into the active positional arguments, kept as an index rather than a reference
1925    // because an explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"`
1926    // arm).
1927    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1928    // argument filled?" has to consult `out.args` by key instead of counting.
1929    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1930    let mut enable_flags = true;
1931    let mut grouped_flag = false;
1932    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1933    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1934    // words that come after it — see `report_double_dash_violation`.
1935    let mut seen_double_dash = false;
1936    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1937    // reopen sigil classification for this invocation.
1938    let mut restart_seen = false;
1939    // Args already reported as having been offered a word before the `--` they require, so a
1940    // variadic one does not report the same violation for every word it is offered.
1941    let mut double_dash_violations: HashSet<String> = HashSet::new();
1942    // A root clause separator is user-supplied syntax, so it must suppress the
1943    // empty-input default just like an explicit `--`. This is separate from
1944    // `seen_double_dash`, which resets for each clause instance.
1945    let mut root_clause_separator_seen = false;
1946    // Scalar occurrences are scoped to the command level where they were written. Inherited
1947    // globals may therefore appear once before and once after a subcommand under clap's strict
1948    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1949    // opposite forms may override one another, while repeating either spelling is an error.
1950    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1951
1952    while !input.is_empty() {
1953        let token = input.pop_front().unwrap();
1954        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1955        let binding = token.binding;
1956        let argv = token.argv;
1957        let mut w = token.word;
1958        // A short's attached value is re-queued with `grouped_flag` set, and that
1959        // continuation is not a following word. `require_equals` refuses only the
1960        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1961        // only when the value is actually missing, so `-cnever` is still `never`.
1962        let attached_continuation = grouped_flag;
1963        // The opt-in lookahead uses declared value arity, so binding must consume
1964        // the same attached value even after the default changed flag scope.
1965        // Other specs retain the reference parser's legacy short-bundle rules.
1966        if spec.default_subcommand_flags
1967            && attached_continuation
1968            && !out.flag_awaiting_value.is_empty()
1969        {
1970            grouped_flag = false;
1971            w.remove(0);
1972            if w.starts_with('=') {
1973                w.remove(0);
1974            }
1975            if bind_pending_flag_value(
1976                spec,
1977                &out.cmd,
1978                &mut out.errors,
1979                &mut out.flags,
1980                &mut out.flag_awaiting_value,
1981                &mut w,
1982                &mut input,
1983                custom_env,
1984                trace,
1985                argv,
1986                true,
1987            )? {
1988                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1989                return Ok((out, overridden_flags));
1990            }
1991            continue;
1992        }
1993
1994        // A clause boundary is syntax even after an automatic trailing argument disabled
1995        // flags. Only an explicit `--` protects a literal separator.
1996        if !seen_double_dash
1997            && let Some(clause) = out.cmd.clause.as_ref()
1998            && clause.separator.as_deref() == Some(w.as_str())
1999        {
2000            while try_bind_default_missing(
2001                &mut out.flags,
2002                &mut out.flag_awaiting_value,
2003                custom_env,
2004                &mut out.flag_origins,
2005            )? {}
2006            if let Some(flag) = out.flag_awaiting_value.first() {
2007                let spelling = flag
2008                    .long
2009                    .first()
2010                    .map(|long| format!("--{long}"))
2011                    .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2012                    .unwrap_or_else(|| flag.name.clone());
2013                return Err(UsageErr::InvalidFlag {
2014                    token: spelling.clone(),
2015                    reason: "requires an argument".to_string(),
2016                    span: (0, spelling.len()).into(),
2017                    input: spelling,
2018                }
2019                .into());
2020            }
2021            let name = clause.name.clone();
2022            if out.cmds.len() == 1 {
2023                root_clause_separator_seen = true;
2024            }
2025            finalize_current_clause(&mut out);
2026            out.arg_origins.clear();
2027            trace.record(argv, TokenRole::ClauseSeparator { name });
2028            next_arg_idx = 0;
2029            out.flag_awaiting_value.clear();
2030            reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2031            enable_flags = true;
2032            seen_double_dash = false;
2033            continue;
2034        }
2035
2036        // Check for restart_token - resets argument parsing for multiple command invocations
2037        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
2038        if let Some(ref restart_token) = out.cmd.restart_token
2039            && w == *restart_token
2040        {
2041            // Reset argument parsing state for a fresh command invocation, keeping the
2042            // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
2043            // is not cleared here either, so clearing it would let one arg report the same
2044            // violation once per invocation.
2045            out.args.clear();
2046            // With the values gone, so is where they came from — otherwise the second
2047            // invocation of `run lint ::: test` reports the first one's provenance. The
2048            // token trace is *not* cleared: those words were read, and a report that
2049            // dropped them would show a command line with a hole in it.
2050            out.arg_origins.clear();
2051            trace.record(argv, TokenRole::Restart);
2052            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2053            restart_seen = true;
2054            out.flag_awaiting_value.clear(); // Clear any pending flag values
2055            enable_flags = true; // Reset -- separator effect
2056            seen_double_dash = false; // The next invocation needs its own `--`
2057            continue;
2058        }
2059
2060        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
2061        // like, and that has to be asked before the separator arm below rather than
2062        // after it. Asked after, a `--` was consumed as a separator while the flag
2063        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
2064        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
2065        // itself, which is what clap does with the same declaration — and no flag can
2066        // still be waiting once the separator has done its job, so the starvation rule
2067        // below has no path around it.
2068        if enable_flags
2069            && !attached_continuation
2070            && w.starts_with('-')
2071            && out
2072                .flag_awaiting_value
2073                .last()
2074                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2075        {
2076            // A variadic argument collects here too: which token supplied its first
2077            // value says nothing about how many it takes.
2078            let should_return = bind_pending_flag_value(
2079                spec,
2080                &out.cmd,
2081                &mut out.errors,
2082                &mut out.flags,
2083                &mut out.flag_awaiting_value,
2084                &mut w,
2085                &mut input,
2086                custom_env,
2087                trace,
2088                argv,
2089                // The token a hyphen-valued flag takes is the following word, never attached.
2090                false,
2091            )?;
2092            if should_return {
2093                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2094                return Ok((out, overridden_flags));
2095            }
2096            continue;
2097        }
2098
2099        // A flag whose value may be omitted that cannot take this token as a detached
2100        // value finishes bare and leaves the token for whatever comes next:
2101        // `--color --verbose` colours with the missing value and still sets verbose,
2102        // and `--inspect 9229` with `require_equals` binds the missing value rather
2103        // than treating 9229 as the port.
2104        if enable_flags
2105            && !attached_continuation
2106            && !out.flag_awaiting_value.is_empty()
2107            && out.flag_awaiting_value.last().is_some_and(|flag| {
2108                (flag.default_missing.is_some() || flag.value_optional)
2109                    && !accepts_detached_flag_value(flag, &w)
2110            })
2111        {
2112            try_bind_default_missing(
2113                &mut out.flags,
2114                &mut out.flag_awaiting_value,
2115                custom_env,
2116                &mut out.flag_origins,
2117            )?;
2118        }
2119
2120        // The first explicit `--` is still a separator after an `automatic` argument has
2121        // stopped flag parsing. Once an explicit separator has done its job, a second one is
2122        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
2123        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
2124        // behavior as the bug it is.
2125        if w == "--" && !seen_double_dash {
2126            enable_flags = false;
2127
2128            // Only preserve the double dash token if we're collecting values for a variadic arg
2129            // in double_dash == `preserve` mode
2130            let should_preserve = active_args(&out.cmd)
2131                .get(next_arg_idx)
2132                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2133                .unwrap_or(false);
2134
2135            if should_preserve {
2136                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
2137                // neither counts as one nor unlocks a `double_dash="required"` arg.
2138            } else {
2139                seen_double_dash = true;
2140                trace.record(argv, TokenRole::Separator);
2141
2142                // Everything after an explicit `--` belongs to the arg that requires one, so
2143                // jump the cursor there — past any earlier arg, including a greedy variadic
2144                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
2145                // which is what `double_dash="required"` is generated from. Specs without such
2146                // an arg find nothing and keep the cursor where it was.
2147                let target = active_args(&out.cmd).iter().position(|arg| {
2148                    arg.double_dash == SpecDoubleDashChoices::Required
2149                        && !out.args.contains_key(arg)
2150                });
2151                if let Some(target) = target {
2152                    // Forward only. An unfilled required arg declared *before* the cursor is
2153                    // left where it is rather than rewound to — words already assigned to
2154                    // later args would have to be taken back for that to mean anything, and
2155                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
2156                    // `Arg::last(true)`, which is the final positional, so a spec that puts
2157                    // one ahead of others is already outside what this models.
2158                    if target > next_arg_idx {
2159                        next_arg_idx = target;
2160                    }
2161                }
2162                continue;
2163            }
2164        }
2165
2166        // long flags
2167        if enable_flags && w.starts_with("--") {
2168            grouped_flag = false;
2169            // `Some` only when an `=` was actually written, so `--jobs=` can supply
2170            // an empty value while `--jobs` supplies none. Collapsing the two lost
2171            // the flag entirely.
2172            let split = w.split_once('=');
2173            let word = split.map(|(word, _)| word).unwrap_or(&w);
2174            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2175            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2176                let command_level = binding
2177                    .as_ref()
2178                    .map(|(_, level)| *level)
2179                    .unwrap_or(out.cmds.len() - 1);
2180                parsed_flag_spellings
2181                    .entry(Arc::as_ptr(f) as usize)
2182                    .or_default()
2183                    .insert(word.to_string());
2184                // Recorded before the action check below: a token that named a flag named it
2185                // whether or not the parse can carry on afterwards.
2186                trace.record(
2187                    argv,
2188                    TokenRole::Flag {
2189                        flag: Arc::clone(f),
2190                        spelling: word.to_string(),
2191                        negated: f.negate.as_deref() == Some(word),
2192                    },
2193                );
2194                if f.action != crate::SpecFlagAction::Set {
2195                    out.errors
2196                        .push(render_action_err(spec, &out.cmd, f, word, custom_env));
2197                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2198                    return Ok((out, overridden_flags));
2199                }
2200                apply_flag_overrides(
2201                    f,
2202                    &out.available_flags,
2203                    &mut out.flags,
2204                    &mut out.flag_awaiting_value,
2205                    &mut overridden_flags,
2206                    &mut out.overridden_flags,
2207                );
2208                if let Some(pending) = out.flag_awaiting_value.first() {
2209                    out.errors.push(render_missing_flag_value(pending, &w));
2210                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2211                    return Ok((out, overridden_flags));
2212                }
2213                // An attached value only means something to a flag that takes one:
2214                // `--jobs=` is an empty string, while `--force=yes` has nothing to
2215                // give a flag that holds no value. Handing that leftover to the
2216                // positionals would re-split one token into two, so `ex --force=yes`
2217                // would fill an argument the caller never typed a word for.
2218                if f.arg.is_some() {
2219                    record_scalar_flag_occurrence(
2220                        &out.cmds,
2221                        f,
2222                        command_level,
2223                        None,
2224                        &mut scalar_occurrences,
2225                        &mut out.errors,
2226                    );
2227                    let f = Arc::clone(f);
2228                    out.flag_awaiting_value.push(Arc::clone(&f));
2229                    // The `=` has already settled that this text is the value, so it
2230                    // binds here rather than going back on the queue to be read as a
2231                    // token again — where `--jobs=--force` looked like a flag of its
2232                    // own and bound `force`, leaving `jobs` unset.
2233                    if let Some((_, val)) = split {
2234                        // The `=` settles where the *first* value came from and nothing
2235                        // more, so a variadic argument goes on collecting from the words
2236                        // after it exactly as the detached form does.
2237                        let mut val = val.to_string();
2238                        let should_return = bind_pending_flag_value(
2239                            spec,
2240                            &out.cmd,
2241                            &mut out.errors,
2242                            &mut out.flags,
2243                            &mut out.flag_awaiting_value,
2244                            &mut val,
2245                            &mut input,
2246                            custom_env,
2247                            trace,
2248                            argv,
2249                            // The `=` settled that this text is the value, so it rode in on
2250                            // the flag's own token.
2251                            true,
2252                        )?;
2253                        if should_return {
2254                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2255                            return Ok((out, overridden_flags));
2256                        }
2257                    }
2258                } else if f.count {
2259                    let arr = out
2260                        .flags
2261                        .entry(Arc::clone(f))
2262                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2263                        .try_as_multi_bool_mut()
2264                        .unwrap();
2265                    arr.push(true);
2266                } else {
2267                    let negate = f.negate.clone().unwrap_or_default();
2268                    let negated_form = word == negate;
2269                    let value = if f.bool_value {
2270                        match split.map(|(_, value)| value) {
2271                            Some("true") => !negated_form,
2272                            Some("false") => negated_form,
2273                            Some(value) => {
2274                                out.errors.push(UsageErr::InvalidValue {
2275                                    name: f.name.clone(),
2276                                    value: value.to_string(),
2277                                    reason: "expected `true` or `false`".to_string(),
2278                                });
2279                                continue;
2280                            }
2281                            None => !negated_form,
2282                        }
2283                    } else {
2284                        !negated_form
2285                    };
2286                    // Which form was typed is a question about the name, so it is
2287                    // asked of `word` rather than the whole token: the attached value
2288                    // is dropped just above, and comparing `--no-color=yes` against
2289                    // `--no-color` would take the negation down with it.
2290                    record_scalar_flag_occurrence(
2291                        &out.cmds,
2292                        f,
2293                        command_level,
2294                        Some(!negated_form),
2295                        &mut scalar_occurrences,
2296                        &mut out.errors,
2297                    );
2298                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2299                }
2300                continue;
2301            }
2302            if is_help_arg(spec, &out.cmd, &w) {
2303                out.errors
2304                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2305                trace.record(
2306                    argv,
2307                    TokenRole::Builtin {
2308                        spelling: w.clone(),
2309                    },
2310                );
2311                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2312                return Ok((out, overridden_flags));
2313            }
2314            if is_version_arg(spec, &out.cmds, &w) {
2315                out.errors.push(render_version_err(spec, w.len() > 2));
2316                trace.record(
2317                    argv,
2318                    TokenRole::Builtin {
2319                        spelling: w.clone(),
2320                    },
2321                );
2322                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2323                return Ok((out, overridden_flags));
2324            }
2325            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2326                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2327                trace.close(&input);
2328                return Err(refused.into());
2329            }
2330        }
2331
2332        // short flags
2333        //
2334        // A fresh token is checked whole before any of it is applied: `-az` with only
2335        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2336        // discovering that `z` names nothing. A grouped continuation is exempt — its
2337        // token was already checked when it arrived.
2338        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2339        let positional_negative_number = !declared_numeric_short
2340            && is_negative_number(&w)
2341            && active_args(&out.cmd)
2342                .get(next_arg_idx)
2343                .is_some_and(|arg| arg.allow_negative_numbers);
2344        if enable_flags
2345            && !grouped_flag
2346            // A word phase 1 already resolved to a flag needs no re-checking, and
2347            // the flags in scope have changed since, so re-checking would be wrong.
2348            && binding.is_none()
2349            && w.starts_with('-')
2350            && w.len() > 1
2351            && is_flag_like(&w)
2352            && !positional_negative_number
2353            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2354        {
2355            // Refused if this command asked for that; otherwise it carries on below
2356            // as one word, with none of its letters applied.
2357            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2358                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2359                trace.close(&input);
2360                return Err(refused.into());
2361            }
2362        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2363            let short = w.chars().nth(1).unwrap();
2364            if let Some(f) = binding
2365                .as_ref()
2366                .map(|(flag, _)| flag)
2367                .or_else(|| out.available_flags.get(&format!("-{short}")))
2368            {
2369                let command_level = binding
2370                    .as_ref()
2371                    .map(|(_, level)| *level)
2372                    .unwrap_or(out.cmds.len() - 1);
2373                if f.action != crate::SpecFlagAction::Set {
2374                    out.errors.push(render_action_err(
2375                        spec,
2376                        &out.cmd,
2377                        f,
2378                        &format!("-{short}"),
2379                        custom_env,
2380                    ));
2381                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2382                    return Ok((out, overridden_flags));
2383                }
2384                parsed_flag_spellings
2385                    .entry(Arc::as_ptr(f) as usize)
2386                    .or_default()
2387                    .insert(format!("-{short}"));
2388                trace.record(
2389                    argv,
2390                    TokenRole::Flag {
2391                        flag: Arc::clone(f),
2392                        spelling: format!("-{short}"),
2393                        // A short spelling is never the negated form: `negate` is a long.
2394                        negated: false,
2395                    },
2396                );
2397                apply_flag_overrides(
2398                    f,
2399                    &out.available_flags,
2400                    &mut out.flags,
2401                    &mut out.flag_awaiting_value,
2402                    &mut overridden_flags,
2403                    &mut out.overridden_flags,
2404                );
2405                if !attached_continuation && let Some(pending) = out.flag_awaiting_value.first() {
2406                    out.errors.push(render_missing_flag_value(pending, &w));
2407                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2408                    return Ok((out, overridden_flags));
2409                }
2410                let rest = &w[1 + short.len_utf8()..];
2411                if !rest.is_empty() {
2412                    // `-abc` is one token that names three flags, so the tail is read at the
2413                    // bundle's own position rather than at one of its own.
2414                    let mut tail = Token::new(format!("-{rest}"), argv);
2415                    if f.arg.is_none() {
2416                        tail.binding = default_bundle
2417                            .as_ref()
2418                            .filter(|bundle| bundle.argv == argv)
2419                            .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2420                    }
2421                    input.push_front(tail);
2422                }
2423                // A fully consumed short is no longer a grouped continuation.
2424                // Leaving this set after `-ai` made `-i` skip `require_equals`
2425                // and bind the following word.
2426                grouped_flag = !rest.is_empty();
2427                if f.arg.is_some() {
2428                    record_scalar_flag_occurrence(
2429                        &out.cmds,
2430                        f,
2431                        command_level,
2432                        None,
2433                        &mut scalar_occurrences,
2434                        &mut out.errors,
2435                    );
2436                    out.flag_awaiting_value.push(Arc::clone(f));
2437                } else if f.count {
2438                    let arr = out
2439                        .flags
2440                        .entry(Arc::clone(f))
2441                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2442                        .try_as_multi_bool_mut()
2443                        .unwrap();
2444                    arr.push(true);
2445                } else {
2446                    let negate = f.negate.clone().unwrap_or_default();
2447                    let value = w != negate;
2448                    record_scalar_flag_occurrence(
2449                        &out.cmds,
2450                        f,
2451                        command_level,
2452                        Some(value),
2453                        &mut scalar_occurrences,
2454                        &mut out.errors,
2455                    );
2456                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2457                }
2458                continue;
2459            }
2460            // The letter nothing declared may still be one the parser supplies, and it may
2461            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2462            // neither reaches the whole-token spellings below.
2463            if let Some(err) = supplied_short(spec, &out.cmds, short, custom_env) {
2464                out.errors.push(err);
2465                trace.record(
2466                    argv,
2467                    TokenRole::Builtin {
2468                        spelling: format!("-{short}"),
2469                    },
2470                );
2471                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2472                return Ok((out, overridden_flags));
2473            }
2474            if is_help_arg(spec, &out.cmd, &w) {
2475                out.errors
2476                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2477                trace.record(
2478                    argv,
2479                    TokenRole::Builtin {
2480                        spelling: w.clone(),
2481                    },
2482                );
2483                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2484                return Ok((out, overridden_flags));
2485            }
2486            if is_version_arg(spec, &out.cmds, &w) {
2487                out.errors.push(render_version_err(spec, w.len() > 2));
2488                trace.record(
2489                    argv,
2490                    TokenRole::Builtin {
2491                        spelling: w.clone(),
2492                    },
2493                );
2494                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2495                return Ok((out, overridden_flags));
2496            }
2497            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2498                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2499                trace.close(&input);
2500                return Err(refused.into());
2501            }
2502            if grouped_flag {
2503                grouped_flag = false;
2504                w.remove(0);
2505                // What is left is a short flag's attached value, and one `=` between
2506                // the letter and the value is a separator: `-j=8` means 8. Only one,
2507                // so `-j==8` still means `=8`.
2508                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2509                    w.remove(0);
2510                }
2511            }
2512        }
2513
2514        // Only while flags are still being read. A flag still waiting when the separator
2515        // was consumed is starved: its value would have to come from after the `--`,
2516        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2517        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2518        // was gone. Left waiting, it is reported as the missing value it is.
2519        // `require_equals` refuses a detached value: `--flag value` is a missing
2520        // value, not a flag of `"value"`. The attached form is still bound above.
2521        // Reported here rather than left waiting until the end of the line: falling
2522        // through would offer `value` to the positionals and call it an unexpected
2523        // word, which is the wrong error and a different one from usage-argv.
2524        if enable_flags
2525            && !attached_continuation
2526            && !out.flag_awaiting_value.is_empty()
2527            && out
2528                .flag_awaiting_value
2529                .last()
2530                .is_some_and(|flag| flag.require_equals)
2531        {
2532            let flag = out.flag_awaiting_value.last().unwrap();
2533            let token = flag
2534                .long
2535                .first()
2536                .map(|l| format!("--{l}"))
2537                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2538                .unwrap_or_else(|| flag.name.clone());
2539            out.errors.push(UsageErr::InvalidFlag {
2540                token: token.clone(),
2541                reason: "requires an argument".to_string(),
2542                span: (0, 0).into(),
2543                input: format!("{token} {w}"),
2544            });
2545            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2546            return Ok((out, overridden_flags));
2547        }
2548        if enable_flags
2549            && !out.flag_awaiting_value.is_empty()
2550            && (attached_continuation
2551                || out
2552                    .flag_awaiting_value
2553                    .last()
2554                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2555        {
2556            // Held before the drain pops it: a flag whose argument is variadic keeps
2557            // taking values after this first one.
2558            let should_return = bind_pending_flag_value(
2559                spec,
2560                &out.cmd,
2561                &mut out.errors,
2562                &mut out.flags,
2563                &mut out.flag_awaiting_value,
2564                &mut w,
2565                &mut input,
2566                custom_env,
2567                trace,
2568                argv,
2569                attached_continuation,
2570            )?;
2571            if should_return {
2572                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2573                return Ok((out, overridden_flags));
2574            }
2575            continue;
2576        }
2577
2578        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2579            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2580            .flatten()
2581        {
2582            if value.is_empty() {
2583                out.errors.push(UsageErr::InvalidValue {
2584                    name: arg.name.clone(),
2585                    value: w.clone(),
2586                    reason: format!("expected a value after sigil {sigil:?}"),
2587                });
2588                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2589                return Ok((out, overridden_flags));
2590            }
2591            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2592            let suppress_trailing_delimiter =
2593                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2594            let delimiter = if suppress_trailing_delimiter && trailing_value {
2595                None
2596            } else {
2597                arg.delimiter
2598            };
2599            let parts = match delimiter {
2600                Some(delimiter) => value
2601                    .split(delimiter)
2602                    .map(str::to_string)
2603                    .collect::<Vec<_>>(),
2604                None => vec![value.to_string()],
2605            };
2606            let mut refused = false;
2607            for part in &parts {
2608                if validate_choices(
2609                    spec,
2610                    &out.cmd,
2611                    &mut out.errors,
2612                    ChoiceTarget::arg(arg),
2613                    part,
2614                    arg.choices.as_ref(),
2615                    custom_env,
2616                )? {
2617                    refused = true;
2618                    break;
2619                }
2620            }
2621            if refused {
2622                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2623                return Ok((out, overridden_flags));
2624            }
2625            trace.record(
2626                argv,
2627                TokenRole::Sigil {
2628                    arg: Arc::new(arg.clone()),
2629                    sigil: sigil.to_string(),
2630                    values: parts.clone(),
2631                },
2632            );
2633            let key = Arc::new(arg.clone());
2634            if arg.var {
2635                let arr = out
2636                    .args
2637                    .entry(key)
2638                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2639                    .try_as_multi_string_mut()
2640                    .unwrap();
2641                arr.extend(parts);
2642            } else {
2643                out.args.insert(key, ParseValue::String(value.to_string()));
2644            }
2645            if out.cmds.len() == 1 {
2646                root_positional_arg_found = true;
2647            }
2648            continue;
2649        }
2650
2651        if out.cmd.allow_missing_positional {
2652            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2653            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2654                if current.required || out.args.contains_key(current) {
2655                    break;
2656                }
2657                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2658                    .iter()
2659                    .filter(|arg| arg.required && arg.sigil.is_none())
2660                    .count();
2661                if required_after == 0 {
2662                    break;
2663                }
2664                let remaining_values = 1 + input
2665                    .iter()
2666                    .filter(|token| {
2667                        (!enable_flags || !is_flag_like(&token.word))
2668                            && (!enable_flags
2669                                || restart_seen
2670                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2671                    })
2672                    .count();
2673                if remaining_values > required_after {
2674                    break;
2675                }
2676                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2677            }
2678        }
2679
2680        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2681            if arg.var
2682                && out.args.contains_key(arg)
2683                && arg.value_terminator.as_deref() == Some(w.as_str())
2684            {
2685                trace.record(
2686                    argv,
2687                    TokenRole::ValueTerminator {
2688                        ends: arg.name.clone(),
2689                    },
2690                );
2691                next_arg_idx += 1;
2692                continue;
2693            }
2694            // Before anything else: an arg that requires `--` accepts nothing until one has been
2695            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2696            // reported as an invalid choice, and from reaching that function's help escape.
2697            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2698                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2699                trace.record(
2700                    argv,
2701                    TokenRole::Refused {
2702                        reason: format!("{} only accepts words after `--`", arg.name),
2703                    },
2704                );
2705                // Drop the word without filling the arg or advancing the cursor: every later
2706                // word hits the same arg and is rejected the same way, so the parse still ends
2707                // in an error rather than in `unexpected word`.
2708                continue;
2709            }
2710            // Split before judging, as the flag path does: after the split the word is
2711            // no longer one value, and `choices` has to be asked about each. Judging
2712            // first rejects `src:docs` against a list that both halves are on, and
2713            // names the whole word rather than the half that was wrong.
2714            let trailing_value =
2715                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2716            let suppress_trailing_delimiter =
2717                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2718            let delimiter = if suppress_trailing_delimiter && trailing_value {
2719                None
2720            } else {
2721                arg.delimiter
2722            };
2723            let parts: Vec<String> = match delimiter {
2724                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2725                None => vec![w.clone()],
2726            };
2727            let mut refused = false;
2728            for part in &parts {
2729                if validate_choices(
2730                    spec,
2731                    &out.cmd,
2732                    &mut out.errors,
2733                    ChoiceTarget::arg(arg),
2734                    part,
2735                    arg.choices.as_ref(),
2736                    custom_env,
2737                )? {
2738                    refused = true;
2739                    break;
2740                }
2741            }
2742            if refused {
2743                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2744                return Ok((out, overridden_flags));
2745            }
2746            // `double_dash="automatic"` means the first value this arg takes is the last
2747            // token read as anything but data: a wrapper declaring it can forward flags
2748            // without its caller typing a `--`. Set before the value is stored, so the
2749            // rest of the command line is already past flag parsing.
2750            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2751                enable_flags = false;
2752            }
2753            // A flag-like word reaching a positional while flags are still being read was
2754            // offered to every declaration and matched none: under the default
2755            // `unknown_flags="value"` it becomes data, and saying so is the difference
2756            // between "you have a typo" and "this argument took your typo".
2757            let unknown_flag = enable_flags
2758                && !positional_negative_number
2759                && is_flag_like(&w)
2760                && binding.is_none();
2761            trace.record(
2762                argv,
2763                if unknown_flag {
2764                    TokenRole::UnknownFlag {
2765                        bound_as: Some(Arc::new(arg.clone())),
2766                    }
2767                } else {
2768                    TokenRole::Arg {
2769                        arg: Arc::new(arg.clone()),
2770                        values: parts.clone(),
2771                    }
2772                },
2773            );
2774            if out.cmds.len() == 1 {
2775                root_positional_arg_found = true;
2776            }
2777            if arg.var {
2778                let arr = out
2779                    .args
2780                    .entry(Arc::new(arg.clone()))
2781                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2782                    .try_as_multi_string_mut()
2783                    .unwrap();
2784                // The values this word carried, split above so that everything
2785                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2786                // counts the values the user meant rather than the words they typed.
2787                arr.extend(parts.iter().cloned());
2788                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2789                    next_arg_idx += 1;
2790                }
2791            } else {
2792                out.args
2793                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2794                next_arg_idx += 1;
2795            }
2796            if out
2797                .cmd
2798                .clause
2799                .as_ref()
2800                .is_some_and(|clause| clause.separator.is_none())
2801                && next_arg_idx >= active_args(&out.cmd).len()
2802            {
2803                while try_bind_default_missing(
2804                    &mut out.flags,
2805                    &mut out.flag_awaiting_value,
2806                    custom_env,
2807                    &mut out.flag_origins,
2808                )? {}
2809                if let Some(flag) = out.flag_awaiting_value.first() {
2810                    let spelling = flag
2811                        .long
2812                        .first()
2813                        .map(|long| format!("--{long}"))
2814                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2815                        .unwrap_or_else(|| flag.name.clone());
2816                    return Err(UsageErr::InvalidFlag {
2817                        token: spelling.clone(),
2818                        reason: "requires an argument".to_string(),
2819                        span: (0, spelling.len()).into(),
2820                        input: spelling,
2821                    }
2822                    .into());
2823                }
2824                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2825                finalize_current_clause(&mut out);
2826                out.arg_origins.clear();
2827                trace.record(argv, TokenRole::ClauseSeparator { name });
2828                next_arg_idx = 0;
2829                out.flag_awaiting_value.clear();
2830                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2831                enable_flags = true;
2832                seen_double_dash = false;
2833            }
2834            continue;
2835        }
2836        if is_help_arg(spec, &out.cmd, &w) {
2837            out.errors
2838                .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2839            trace.record(
2840                argv,
2841                TokenRole::Builtin {
2842                    spelling: w.clone(),
2843                },
2844            );
2845            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2846            return Ok((out, overridden_flags));
2847        }
2848        if is_version_arg(spec, &out.cmds, &w) {
2849            out.errors.push(render_version_err(spec, w.len() > 2));
2850            trace.record(
2851                argv,
2852                TokenRole::Builtin {
2853                    spelling: w.clone(),
2854                },
2855            );
2856            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2857            return Ok((out, overridden_flags));
2858        }
2859        trace.record(
2860            argv,
2861            TokenRole::Refused {
2862                reason: "no declaration takes this word".to_string(),
2863            },
2864        );
2865        trace.close(&input);
2866        bail!("unexpected word: {w}");
2867    }
2868
2869    // Resolve optional defaults before deciding whether argv is empty. This is deliberately
2870    // inside the opt-in gate: completion's partial parser must retain its pending value when
2871    // the feature is disabled.
2872    if spec.default_subcommand_on_empty
2873        && spec.default_subcommand.is_some()
2874        && out.cmds.len() == 1
2875        && !root_positional_arg_found
2876        && !seen_double_dash
2877        && !root_clause_separator_seen
2878        && out.errors.is_empty()
2879    {
2880        while try_bind_default_missing(
2881            &mut out.flags,
2882            &mut out.flag_awaiting_value,
2883            custom_env,
2884            &mut out.flag_origins,
2885        )? {}
2886        if !out.flag_awaiting_value.is_empty() {
2887            // A required flag value is still pending; leave it for full-parse diagnostics or
2888            // completion rather than routing to a synthetic child.
2889        } else if let Some(subcommand) = out
2890            .cmd
2891            .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2892        {
2893            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2894                bail!(
2895                    "subcommand '{}' cannot be used with arguments on its parent command",
2896                    subcommand.name
2897                );
2898            }
2899            let mut subcommand = subcommand.clone();
2900            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2901            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2902            out.merge_command_flags(&subcommand, crossing_mount);
2903            out.cmds.push(subcommand.clone());
2904            out.cmd = subcommand;
2905            command_has_argv = false;
2906            prefix_flags.clear();
2907            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2908        }
2909    }
2910
2911    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2912    if validate_clauses {
2913        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2914    }
2915    let clause_flag_names = out
2916        .cmd
2917        .clause
2918        .iter()
2919        .flat_map(|clause| &clause.flags)
2920        .map(|flag| flag.name.clone())
2921        .collect::<HashSet<_>>();
2922
2923    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2924    // declarations with the same canonical name therefore share one public value entry even
2925    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2926    // both, which is what exclusivity needs.
2927    let flag_was_parsed =
2928        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2929
2930    // The spellings the selected command's own declaration speaks for, on this object.
2931    //
2932    // Empty unless that declaration really is this object's: a parent and child may each
2933    // declare `--clean` without merging, leaving two flags that share a name, and the
2934    // ancestor's must not be read as the child's. The test is whether every spelling the child
2935    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2936    // an ancestor whose long form the child took over.
2937    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2938        let declared: HashSet<String> = out
2939            .cmd
2940            .flags
2941            .iter()
2942            .filter(|declared| declared.name == flag.name)
2943            .flat_map(flag_keys)
2944            .collect();
2945        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2946        // some other inherited global already owns — `-c --clean` beside an inherited
2947        // `-c --config` — and that collision is resolved in the other global's favor, so the
2948        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2949        // unrelated collision disown the child from the `--clean` it plainly does own.
2950        //
2951        // Still enough to tell the two-object case apart, which is what this guards: when a
2952        // child re-declares a global as global, the child's own spellings resolve to the
2953        // child's separate flag, so none of them lands on the ancestor's.
2954        let speaks_for_this_flag = declared.iter().any(|spelling| {
2955            out.available_flags
2956                .get(spelling)
2957                .is_some_and(|available| Arc::ptr_eq(available, flag))
2958        });
2959        if speaks_for_this_flag {
2960            declared
2961        } else {
2962            HashSet::new()
2963        }
2964    };
2965
2966    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2967    //
2968    // A child that re-declares an inherited global merges into one object answering to two
2969    // alias sets whose declarations may disagree, so there is no single owner to name: the
2970    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2971    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2972    // child's in one invocation — and each carries its own declaration's answer.
2973    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2974        let child = child_spellings(flag);
2975        let child_exclusive = !child.is_empty()
2976            && out
2977                .cmd
2978                .flags
2979                .iter()
2980                .any(|declared| declared.name == flag.name && declared.exclusive);
2981        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2982            Some(spellings) => (
2983                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2984                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2985            ),
2986            // An environment value has no spelling to attribute it by. The declaration the
2987            // selected command has in scope is the one that answers — which is the child's
2988            // when it re-declared the flag, and the ancestor's when it did not.
2989            None => (child_exclusive, flag.exclusive && child.is_empty()),
2990        }
2991    };
2992
2993    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2994        let (child, ancestor) = exclusivity_in_play(flag);
2995        child || ancestor
2996    };
2997
2998    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2999    // command that otherwise needs an input. Companions are still diagnosed below, but an
3000    // exclusive occurrence suppresses the missing-value checks that would make it unusable
3001    // whether it was alone or not.
3002    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3003        .filter(|flag| !clause_flag_names.contains(&flag.name))
3004        .any(|flag| {
3005            exclusive_occurrence(flag)
3006                && !overridden_flags.contains(&flag.name)
3007                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
3008        });
3009    let requirements_apply = |command_index: usize| {
3010        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
3011    };
3012
3013    if out.cmd.arg_required_else_help && !command_has_argv {
3014        out.errors
3015            .push(render_help_err(spec, &out.cmd, false, custom_env));
3016    }
3017
3018    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
3019    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
3020    // the child would be here instead. The spec has carried `subcommand_required` since it was
3021    // added for the derive, and this parser never read it — so `mise generate` parsed as a
3022    // complete invocation while usage-argv and clap both refused it.
3023    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
3024        let mut names: Vec<&str> = out
3025            .cmd
3026            .subcommands
3027            .iter()
3028            // Aliases share a map entry with the name they point at; listing both would offer
3029            // the same command twice under two spellings.
3030            .filter(|(name, sub)| sub.name == **name && !sub.hide)
3031            .map(|(name, _)| name.as_str())
3032            .collect();
3033        names.sort_unstable();
3034        out.errors.push(UsageErr::MissingSubcommand(
3035            out.cmd.name.clone(),
3036            names.join(", "),
3037        ));
3038    }
3039
3040    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
3041    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
3042    if !exclusive_present {
3043        for arg in out
3044            .cmds
3045            .iter()
3046            .enumerate()
3047            .filter(|(index, _)| requirements_apply(*index))
3048            .flat_map(|(_, cmd)| &cmd.args)
3049        {
3050            if out.args.contains_key(arg) {
3051                continue;
3052            }
3053            // Already reported as needing a `--`; one mistake should not yield two messages.
3054            if double_dash_violations.contains(&arg.name) {
3055                continue;
3056            }
3057            let required_if = arg.required_if.iter().any(|selector| {
3058                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3059            });
3060            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
3061                selector_explicit_has_value(
3062                    &condition.selector,
3063                    &condition.value,
3064                    &out,
3065                    &overridden_flags,
3066                    custom_env,
3067                )
3068            });
3069            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3070                && arg.required_if_eq_all.iter().all(|condition| {
3071                    selector_explicit_has_value(
3072                        &condition.selector,
3073                        &condition.value,
3074                        &out,
3075                        &overridden_flags,
3076                        custom_env,
3077                    )
3078                });
3079            let unless_any = arg.required_unless.iter().any(|selector| {
3080                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3081            });
3082            let unless_all = !arg.required_unless_all.is_empty()
3083                && arg.required_unless_all.iter().all(|selector| {
3084                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3085                });
3086            let required_unless = !(unless_any
3087                || unless_all
3088                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3089            if (arg.required
3090                || required_if
3091                || required_if_eq
3092                || required_if_eq_all
3093                || required_unless)
3094                && arg.default.is_empty()
3095            {
3096                // Check if there's an env var available (custom env map takes precedence)
3097                let has_env = arg
3098                    .env
3099                    .as_ref()
3100                    .is_some_and(|env_var| env_contains(custom_env, env_var));
3101                if !has_env {
3102                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3103                }
3104            }
3105        }
3106    }
3107
3108    // Conflicts are a question about the invocation as a whole rather than about any one
3109    // token, so they are checked here beside the requirement checks rather than at the
3110    // point a flag is matched — the flag it conflicts with may still be ahead of it.
3111    // Its own loop: the requirement loop below skips the flags that *were* given, which
3112    // is exactly the set this needs.
3113    //
3114    // A value from the environment counts on both sides, matching what
3115    // `selector_is_explicit` says about the other flag: the question is whether a flag
3116    // has a value, not how it got one. That is what clap does, and an asymmetric rule
3117    // would make the same pair of flags a conflict or not depending on which one
3118    // happened to be typed.
3119    for flag in unique_flags(out.available_flags.values())
3120        .filter(|flag| !clause_flag_names.contains(&flag.name))
3121    {
3122        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3123        if !given || overridden_flags.contains(&flag.name) {
3124            continue;
3125        }
3126        for other in &flag.conflicts {
3127            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3128                out.errors.push(UsageErr::InvalidFlag {
3129                    token: format!("--{}", flag.name),
3130                    reason: format!("conflicts with {other}"),
3131                    span: (0, 0).into(),
3132                    input: format!("--{} {other}", flag.name),
3133                });
3134            }
3135        }
3136        // The positive form, checked in the same pass and under the same rule: a value
3137        // from the environment satisfies a requirement, because the question is whether
3138        // the other flag has a value rather than how it got one. A flag that was
3139        // overridden away has not been given, so it cannot satisfy anything either —
3140        // which is what `selector_is_explicit` already accounts for.
3141        //
3142        // Reported as the missing flag rather than as something wrong with the flag that
3143        // named it, which is what clap says too: an unmet `requires` is a required
3144        // argument that was not provided. Named by its own name, resolved through the
3145        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
3146        let owner = out
3147            .cmds
3148            .iter()
3149            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3150            .unwrap_or(out.cmds.len() - 1);
3151        if !exclusive_present && requirements_apply(owner) {
3152            for other in &flag.requires {
3153                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3154                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3155                    if other.starts_with('-') {
3156                        out.errors.push(UsageErr::MissingFlag(name));
3157                    } else {
3158                        out.errors.push(UsageErr::MissingArg(name));
3159                    }
3160                }
3161            }
3162            for condition in &flag.requires_if {
3163                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3164                    && !selector_is_satisfied(
3165                        &condition.requires,
3166                        &out,
3167                        &overridden_flags,
3168                        custom_env,
3169                    )
3170                {
3171                    let name = selector_flag_name(&condition.requires, &out)
3172                        .unwrap_or_else(|| condition.requires.clone());
3173                    out.errors.push(UsageErr::MissingFlag(name));
3174                }
3175            }
3176        }
3177    }
3178
3179    // Positionals can declare the same pairwise conflict as flags. Their selector is
3180    // the bare argument name, while a flag keeps its dashed spelling.
3181    for (command_index, arg) in out
3182        .cmds
3183        .iter()
3184        .enumerate()
3185        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3186    {
3187        let given = arg_is_explicit(arg, &out, custom_env);
3188        if !given {
3189            continue;
3190        }
3191        for other in &arg.conflicts {
3192            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3193                out.errors.push(UsageErr::InvalidFlag {
3194                    token: arg.name.clone(),
3195                    reason: format!("conflicts with {other}"),
3196                    span: (0, 0).into(),
3197                    input: format!("{} {other}", arg.name),
3198                });
3199            }
3200        }
3201        if !exclusive_present && requirements_apply(command_index) {
3202            for other in &arg.requires {
3203                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3204                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3205                    if other.starts_with('-') {
3206                        out.errors.push(UsageErr::MissingFlag(name));
3207                    } else {
3208                        out.errors.push(UsageErr::MissingArg(name));
3209                    }
3210                }
3211            }
3212        }
3213    }
3214
3215    // An exclusive flag is the whole-command form of a conflict: `--version` means the
3216    // rest of the line has nothing to act on. Everything the invocation supplied counts,
3217    // positionals included, which is what distinguishes it from being in a group with
3218    // every other flag.
3219    //
3220    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
3221    // exclusive one is nobody saying anything, and counting it would make the exclusive
3222    // flag unusable on any command that has a default. Environment values do count, also as
3223    // `conflicts` reads them, so the spec parser and the derive agree.
3224    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3225        .filter(|flag| !clause_flag_names.contains(&flag.name))
3226    {
3227        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
3228        // but re-declared aliases can leave distinct declarations with that same name in
3229        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
3230        // compare the parser's `Arc`s by identity here.
3231        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3232        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3233            continue;
3234        }
3235        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3236            .filter(|other| !clause_flag_names.contains(&other.name))
3237            .find(|other| {
3238                !Arc::ptr_eq(other, flag)
3239                    && !overridden_flags.contains(&other.name)
3240                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3241            })
3242            .map(|other| format!("--{}", other.name));
3243        let other_arg = active_args(&out.cmd).iter().find(|arg| {
3244            out.args.keys().any(|given| given.name == arg.name)
3245                || out
3246                    .clauses
3247                    .values()
3248                    .flatten()
3249                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
3250                || arg
3251                    .env
3252                    .as_ref()
3253                    .is_some_and(|env| env_contains(custom_env, env))
3254        });
3255        // Selecting a child is company for an exclusive flag declared by an ancestor. An
3256        // exclusive flag belonging to the child itself does not conflict with the command word
3257        // needed to reach that child — so the question is not who owns the flag but whose
3258        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
3259        // separated.
3260        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3261        let selected_subcommand =
3262            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3263        let other = other_flag
3264            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3265            .or(selected_subcommand);
3266        if let Some(other) = other {
3267            out.errors.push(UsageErr::InvalidFlag {
3268                token: format!("--{}", flag.name),
3269                reason: format!("must be given on its own, and {other} was given too"),
3270                span: (0, 0).into(),
3271                input: format!("--{} {other}", flag.name),
3272            });
3273        }
3274    }
3275
3276    // Groups, checked once per group rather than per flag: both questions a group asks —
3277    // how many members were given, and whether that is enough — are about the set, which
3278    // is the whole reason a group exists rather than a pile of pairwise conflicts.
3279    //
3280    // The same "given" rule as everything else here, so a member filled from the
3281    // environment or a default counts.
3282    // Every command in the chain, not only the selected one: a group may name global
3283    // flags, which belong to an ancestor and are declared there.
3284    let mut group_errors: Vec<UsageErr> = Vec::new();
3285    for (command_index, group) in out
3286        .cmds
3287        .iter()
3288        .enumerate()
3289        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3290    {
3291        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
3292        // `--file` are two spellings of one flag, and a group naming both — or naming one
3293        // flag twice — would otherwise report that flag as conflicting with itself the
3294        // moment it was given. Deduplicated rather than refused where the group is
3295        // written, because listing both spellings is redundant, not wrong.
3296        let mut given: Vec<&str> = Vec::new();
3297        let mut seen: Vec<String> = Vec::new();
3298        for selector in &group.members {
3299            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3300                continue;
3301            }
3302            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3303            if seen.contains(&name) {
3304                continue;
3305            }
3306            seen.push(name);
3307            given.push(selector.as_str());
3308        }
3309        if !group.multiple && given.len() > 1 {
3310            group_errors.push(UsageErr::InvalidFlag {
3311                token: given[1].to_string(),
3312                reason: format!("cannot be used with {} in group {}", given[0], group.name),
3313                span: (0, 0).into(),
3314                input: format!("{} {}", given[0], given[1]),
3315            });
3316        }
3317        // Requiredness is a *positive* rule, so it reads a default as filling a member —
3318        // the rule `requires` follows. That is also why it cannot reuse `given` above:
3319        // exclusivity must count only what was supplied, or a defaulted member would
3320        // collide with the sibling the user actually typed.
3321        let satisfied = group
3322            .members
3323            .iter()
3324            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3325        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3326            // The members are what a user has to type, so they are in the message; the
3327            // group's name is there too, since a command with several groups would
3328            // otherwise report the same sentence twice with nothing to tell them apart.
3329            group_errors.push(UsageErr::MissingGroup {
3330                group: group.name.clone(),
3331                members: group.members.join(", "),
3332            });
3333        }
3334    }
3335    out.errors.extend(group_errors);
3336
3337    if !exclusive_present {
3338        for flag in unique_flags(out.available_flags.values())
3339            .filter(|flag| !clause_flag_names.contains(&flag.name))
3340        {
3341            let owner = out
3342                .cmds
3343                .iter()
3344                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3345                .unwrap_or(out.cmds.len() - 1);
3346            if !requirements_apply(owner) {
3347                continue;
3348            }
3349            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3350                continue;
3351            }
3352            let has_default =
3353                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3354            let has_env = flag_has_env(flag, custom_env);
3355            let required_if = flag.required_if.iter().any(|selector| {
3356                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3357            });
3358            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3359                selector_explicit_has_value(
3360                    &condition.selector,
3361                    &condition.value,
3362                    &out,
3363                    &overridden_flags,
3364                    custom_env,
3365                )
3366            });
3367            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3368                && flag.required_if_eq_all.iter().all(|condition| {
3369                    selector_explicit_has_value(
3370                        &condition.selector,
3371                        &condition.value,
3372                        &out,
3373                        &overridden_flags,
3374                        custom_env,
3375                    )
3376                });
3377            let unless_any = flag.required_unless.iter().any(|selector| {
3378                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3379            });
3380            let unless_all = !flag.required_unless_all.is_empty()
3381                && flag.required_unless_all.iter().all(|selector| {
3382                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3383                });
3384            let required_unless = !(unless_any
3385                || unless_all
3386                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3387            if (flag.required
3388                || required_if
3389                || required_if_eq
3390                || required_if_eq_all
3391                || required_unless)
3392                && !has_default
3393                && !has_env
3394            {
3395                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3396            }
3397        }
3398    }
3399
3400    // Validate var_min/var_max constraints for variadic args
3401    for (arg, value) in &out.args {
3402        if arg.var
3403            && let ParseValue::MultiString(values) = value
3404        {
3405            if let Some(min) = arg.var_min
3406                && values.len() < min
3407            {
3408                out.errors.push(UsageErr::VarArgTooFew {
3409                    name: arg.name.clone(),
3410                    min,
3411                    got: values.len(),
3412                });
3413            }
3414            if let Some(max) = arg.var_max
3415                && values.len() > max
3416            {
3417                out.errors.push(UsageErr::VarArgTooMany {
3418                    name: arg.name.clone(),
3419                    max,
3420                    got: values.len(),
3421                });
3422            }
3423        }
3424    }
3425
3426    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3427    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3428    // by binding once per occurrence, where the per-occurrence count is still available.
3429    for flag in unique_flags(out.available_flags.values())
3430        .filter(|flag| !clause_flag_names.contains(&flag.name))
3431    {
3432        if flag.var {
3433            let bound = match out.flags.get(flag) {
3434                Some(ParseValue::MultiString(values)) => values.len(),
3435                Some(ParseValue::MultiBool(values)) => values.len(),
3436                Some(_) => 1,
3437                None => 0,
3438            };
3439            // A partial parse deliberately leaves the final value-optional flag pending so
3440            // completion can still answer for it. It is nevertheless a real occurrence for
3441            // the repeated flag's bounds; the full parser closes it just after this phase.
3442            let pending = out
3443                .flag_awaiting_value
3444                .iter()
3445                .filter(|pending| {
3446                    Arc::ptr_eq(pending, flag)
3447                        && (pending.value_optional || pending.default_missing.is_some())
3448                })
3449                .count();
3450            let count = bound + pending;
3451            if count == 0 {
3452                continue;
3453            }
3454            if let Some(min) = flag.var_min
3455                && count < min
3456            {
3457                out.errors.push(UsageErr::VarFlagTooFew {
3458                    name: flag.name.clone(),
3459                    min,
3460                    got: count,
3461                });
3462            }
3463            if let Some(max) = flag.var_max
3464                && count > max
3465            {
3466                out.errors.push(UsageErr::VarFlagTooMany {
3467                    name: flag.name.clone(),
3468                    max,
3469                    got: count,
3470                });
3471            }
3472        }
3473    }
3474
3475    Ok((out, overridden_flags))
3476}
3477
3478fn validate_expression(
3479    name: &str,
3480    expression: Option<&str>,
3481    message: Option<&str>,
3482    parsed: &ParseValue,
3483    errors: &mut Vec<UsageErr>,
3484) {
3485    let Some(expression) = expression else {
3486        return;
3487    };
3488    #[cfg(not(feature = "validation"))]
3489    let _ = expression;
3490    let values: &[String] = match parsed {
3491        ParseValue::String(value) => std::slice::from_ref(value),
3492        ParseValue::MultiString(values) => values,
3493        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3494    };
3495    #[cfg(feature = "validation")]
3496    for value in values {
3497        let reason = match usage_validation::validate(expression, value) {
3498            Ok(true) => continue,
3499            Ok(false) => message
3500                .unwrap_or("does not satisfy the validation expression")
3501                .to_string(),
3502            Err(error) => format!("validation expression failed: {error}"),
3503        };
3504        errors.push(UsageErr::InvalidValue {
3505            name: name.to_string(),
3506            value: value.clone(),
3507            reason,
3508        });
3509        break;
3510    }
3511    #[cfg(not(feature = "validation"))]
3512    if let Some(value) = values.first() {
3513        let _ = message;
3514        errors.push(UsageErr::InvalidValue {
3515            name: name.to_string(),
3516            value: value.clone(),
3517            reason: "expression validation requires the `validation` feature".to_string(),
3518        });
3519    }
3520}
3521
3522#[cfg(all(test, not(feature = "validation")))]
3523mod optional_validation_tests {
3524    use crate::{Spec, parse};
3525
3526    #[test]
3527    fn validation_declarations_require_the_opt_in_runtime_feature() {
3528        let spec: Spec = r#"
3529name "ex"
3530bin "ex"
3531arg "<port>" validate="int(value) > 0"
3532        "#
3533        .parse()
3534        .unwrap();
3535        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3536        assert!(
3537            error
3538                .to_string()
3539                .contains("requires the `validation` feature"),
3540            "{error:?}"
3541        );
3542    }
3543}
3544
3545fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3546    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3547}
3548
3549fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3550    overrider
3551        .overrides
3552        .iter()
3553        .any(|selector| flag_matches_selector(overridden, selector))
3554}
3555
3556fn apply_prefix_flag_overrides(
3557    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3558    flag: Arc<SpecFlag>,
3559) {
3560    prefix_flags
3561        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3562}
3563
3564fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3565    prefix_flags
3566        .iter()
3567        .flat_map(|(_, words)| words.iter().cloned())
3568        .collect()
3569}
3570
3571fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3572    match custom_env {
3573        Some(env) => env.contains_key(env_var),
3574        None => std::env::var(env_var).is_ok(),
3575    }
3576}
3577
3578fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3579    flag.env_names()
3580        .any(|env_var| env_contains(custom_env, env_var))
3581}
3582
3583fn fallback_is_true(value: &str) -> bool {
3584    matches!(value, "1" | "true" | "True" | "TRUE")
3585}
3586
3587fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3588    match delimiter {
3589        Some(delimiter) => values
3590            .iter()
3591            .flat_map(|value| value.split(delimiter).map(str::to_string))
3592            .collect(),
3593        None => values.to_vec(),
3594    }
3595}
3596
3597fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3598    if let Some(min) = arg.var_min
3599        && count < min
3600    {
3601        errors.push(UsageErr::VarArgTooFew {
3602            name: arg.name.clone(),
3603            min,
3604            got: count,
3605        });
3606    }
3607    if let Some(max) = arg.var_max
3608        && count > max
3609    {
3610        errors.push(UsageErr::VarArgTooMany {
3611            name: arg.name.clone(),
3612            max,
3613            got: count,
3614        });
3615    }
3616}
3617
3618fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3619    if let Some(min) = flag.var_min
3620        && count < min
3621    {
3622        errors.push(UsageErr::VarFlagTooFew {
3623            name: flag.name.clone(),
3624            min,
3625            got: count,
3626        });
3627    }
3628    if let Some(max) = flag.var_max
3629        && count > max
3630    {
3631        errors.push(UsageErr::VarFlagTooMany {
3632            name: flag.name.clone(),
3633            max,
3634            got: count,
3635        });
3636    }
3637}
3638
3639fn validate_flag_arg_fallback_count(
3640    flag: &SpecFlag,
3641    arg: &SpecArg,
3642    count: usize,
3643    errors: &mut Vec<UsageErr>,
3644) {
3645    if let Some(min) = arg.var_min
3646        && count < min
3647    {
3648        errors.push(UsageErr::VarFlagTooFew {
3649            name: flag.name.clone(),
3650            min,
3651            got: count,
3652        });
3653    }
3654    if let Some(max) = arg.var_max
3655        && count > max
3656    {
3657        errors.push(UsageErr::VarFlagTooMany {
3658            name: flag.name.clone(),
3659            max,
3660            got: count,
3661        });
3662    }
3663}
3664
3665/// Bind a fallback the way an unconditional `default` does: one value, or several
3666/// for `var`, and choices checked the same way.
3667fn bind_flag_fallback(
3668    flag: &Arc<SpecFlag>,
3669    values: &[String],
3670    out: &mut ParseOutput,
3671    custom_env: Option<&HashMap<String, String>>,
3672    origin: ValueOrigin,
3673) -> Result<(), miette::Error> {
3674    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3675        return Ok(());
3676    };
3677    out.flags.insert(Arc::clone(flag), value);
3678    out.flag_origins
3679        .entry(Arc::clone(flag))
3680        .or_default()
3681        .push(origin);
3682    Ok(())
3683}
3684
3685fn flag_fallback_value(
3686    flag: &SpecFlag,
3687    values: &[String],
3688    errors: &mut Vec<UsageErr>,
3689    custom_env: Option<&HashMap<String, String>>,
3690) -> Result<Option<ParseValue>, miette::Error> {
3691    if values.is_empty() {
3692        return Ok(None);
3693    }
3694    if let Some(arg) = flag.arg.as_ref() {
3695        let values = split_fallback_values(values, arg.delimiter);
3696        if flag.var || arg.var {
3697            if flag.var {
3698                validate_flag_fallback_count(flag, values.len(), errors);
3699            }
3700            if arg.var {
3701                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3702            }
3703            validate_choice_values(
3704                ChoiceTarget::option(flag),
3705                &values,
3706                arg.choices.as_ref(),
3707                custom_env,
3708            )?;
3709            Ok(Some(ParseValue::MultiString(values)))
3710        } else {
3711            let value = values.into_iter().next().unwrap_or_default();
3712            validate_choice_value(
3713                ChoiceTarget::option(flag),
3714                &value,
3715                arg.choices.as_ref(),
3716                custom_env,
3717            )?;
3718            Ok(Some(ParseValue::String(value)))
3719        }
3720    } else if flag.var {
3721        validate_flag_fallback_count(flag, values.len(), errors);
3722        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3723        Ok(Some(ParseValue::MultiBool(bools)))
3724    } else {
3725        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3726    }
3727}
3728
3729/// Fill scoped flags inside the clause instances argv created.
3730///
3731/// The returned sets contain argv and environment sources only. Relationship
3732/// validation uses them to keep defaults from becoming explicit conflicts while
3733/// still allowing a fallback value to satisfy a required scoped flag.
3734fn apply_clause_flag_fallbacks(
3735    out: &mut ParseOutput,
3736    overridden_flags: &HashSet<String>,
3737    custom_env: Option<&HashMap<String, String>>,
3738) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3739    let Some(clause) = out.cmd.clause.clone() else {
3740        return Ok(None);
3741    };
3742    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3743        return Ok(None);
3744    };
3745    let positional_instances = positional_instances.clone();
3746    let mut flag_instances = out
3747        .clause_flags
3748        .shift_remove(&clause.name)
3749        .unwrap_or_default();
3750    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3751    let get_env = |key: &str| -> Option<String> {
3752        custom_env
3753            .and_then(|values| values.get(key).cloned())
3754            .or_else(|| {
3755                custom_env
3756                    .is_none()
3757                    .then(|| std::env::var(key).ok())
3758                    .flatten()
3759            })
3760    };
3761    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3762
3763    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3764        let positional = &positional_instances[instance_index];
3765        let mut explicit = scoped
3766            .keys()
3767            .map(|flag| flag.name.clone())
3768            .collect::<HashSet<_>>();
3769
3770        // Environment first so every default_if sees all explicit sources,
3771        // independent of declaration order.
3772        for declared in &clause.flags {
3773            let flag = Arc::new(declared.clone());
3774            if scoped.contains_key(&flag) {
3775                continue;
3776            }
3777            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3778                continue;
3779            };
3780            if let Some(warning) = flag_deprecation(declared) {
3781                out.warnings.push(warning);
3782            }
3783            if flag_env_is_deprecated(declared, env_name) {
3784                out.warnings
3785                    .push(Warning::env(env_name, flag_current_env(declared)));
3786            }
3787            if let Some(value) = flag_fallback_value(
3788                declared,
3789                std::slice::from_ref(&env_value),
3790                &mut out.errors,
3791                custom_env,
3792            )? {
3793                scoped.insert(flag, value);
3794                explicit.insert(declared.name.clone());
3795            }
3796        }
3797
3798        let condition_matches =
3799            |condition: &crate::SpecDefaultIf| {
3800                if let Some(flag) = clause
3801                    .flags
3802                    .iter()
3803                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3804                {
3805                    if !explicit.contains(&flag.name) {
3806                        return false;
3807                    }
3808                    return condition.when.as_ref().is_none_or(|expected| {
3809                        scoped
3810                            .iter()
3811                            .find(|(present, _)| present.name == flag.name)
3812                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3813                    });
3814                }
3815                if let Some(arg) = clause.args.iter().find(|arg| {
3816                    !condition.selector.starts_with('-') && arg.name == condition.selector
3817                }) {
3818                    return positional.get(arg).is_some_and(|value| {
3819                        condition
3820                            .when
3821                            .as_ref()
3822                            .is_none_or(|expected| parse_value_has(value, expected))
3823                    });
3824                }
3825                let command_flag = out
3826                    .available_flags
3827                    .values()
3828                    .chain(out.flags.keys())
3829                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3830                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3831                command_flag.is_some_and(|flag| {
3832                    !overridden_flags.contains(&flag.name)
3833                        && command_flag_has_explicit_source(flag, out)
3834                        && condition.when.as_ref().is_none_or(|expected| {
3835                            out.flags
3836                                .get(flag)
3837                                .is_some_and(|value| parse_value_has(value, expected))
3838                        })
3839                })
3840            };
3841
3842        let conditional = clause
3843            .flags
3844            .iter()
3845            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3846            .filter_map(|flag| {
3847                flag.default_if
3848                    .iter()
3849                    .find(|condition| condition_matches(condition))
3850                    .map(|condition| (flag.clone(), condition.value.clone()))
3851            })
3852            .collect::<Vec<_>>();
3853        for (declared, value) in conditional {
3854            if let Some(value) = flag_fallback_value(
3855                &declared,
3856                std::slice::from_ref(&value),
3857                &mut out.errors,
3858                custom_env,
3859            )? {
3860                scoped.insert(Arc::new(declared), value);
3861            }
3862        }
3863
3864        for declared in &clause.flags {
3865            let flag = Arc::new(declared.clone());
3866            if scoped.contains_key(&flag) {
3867                continue;
3868            }
3869            let values = if !declared.default.is_empty() {
3870                &declared.default
3871            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3872                &arg.default
3873            } else {
3874                continue;
3875            };
3876            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3877            {
3878                scoped.insert(flag, value);
3879            }
3880        }
3881        explicit_instances.push(explicit);
3882    }
3883
3884    out.clause_flags.insert(clause.name, flag_instances);
3885    Ok(Some(explicit_instances))
3886}
3887
3888fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3889    out.flags.contains_key(flag)
3890        && out.flag_origins.get(flag).is_none_or(|origins| {
3891            origins
3892                .iter()
3893                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3894        })
3895}
3896
3897fn default_if_condition_matches(
3898    condition: &crate::SpecDefaultIf,
3899    out: &ParseOutput,
3900    overridden_flags: &HashSet<String>,
3901    custom_env: Option<&HashMap<String, String>>,
3902) -> bool {
3903    match &condition.when {
3904        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3905        Some(when) => {
3906            let Some(flag) = out
3907                .available_flags
3908                .values()
3909                .chain(out.flags.keys())
3910                .find(|flag| flag_matches_selector(flag, &condition.selector))
3911            else {
3912                return false;
3913            };
3914            if overridden_flags.contains(&flag.name) {
3915                return false;
3916            }
3917            explicit_flag_has_value(flag, when, out, custom_env)
3918        }
3919    }
3920}
3921
3922/// Whether an explicitly supplied value of `flag` equals `expected`.
3923///
3924/// clap treats command-line and environment values as explicit for `requires_if`, but
3925/// not defaults. Keep that source distinction here instead of consulting the flag's
3926/// defaults through `selector_is_satisfied`.
3927fn explicit_flag_has_value(
3928    flag: &SpecFlag,
3929    expected: &str,
3930    out: &ParseOutput,
3931    custom_env: Option<&HashMap<String, String>>,
3932) -> bool {
3933    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3934        ParseValue::Bool(value) => value.to_string() == expected,
3935        ParseValue::String(value) => value == expected,
3936        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3937        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3938    });
3939    if out.flags.contains_key(flag) {
3940        return parsed_matches;
3941    }
3942
3943    let value = flag.env_names().find_map(|env| match custom_env {
3944        Some(values) => values.get(env).cloned(),
3945        None => std::env::var(env).ok(),
3946    });
3947    value.is_some_and(
3948        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3949            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3950            None if flag.arg.is_none() => {
3951                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3952            }
3953            None => value == expected,
3954        },
3955    )
3956}
3957
3958fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3959    match value {
3960        ParseValue::Bool(value) => value.to_string() == expected,
3961        ParseValue::String(value) => value == expected,
3962        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3963        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3964    }
3965}
3966
3967/// Clause flags share the command's spelling table so they can be recognized while parsing,
3968/// but their values and validation belong to one clause instance rather than the command.
3969fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3970    out.cmd
3971        .clause
3972        .as_ref()
3973        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3974}
3975
3976fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3977    out.cmd
3978        .clause
3979        .as_ref()?
3980        .flags
3981        .iter()
3982        .find(|flag| flag_matches_selector(flag, selector))
3983}
3984
3985fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3986    out.clause_flags
3987        .values()
3988        .flatten()
3989        .any(|instance| instance.keys().any(|present| present.name == flag.name))
3990        || out.flags.keys().any(|present| present.name == flag.name)
3991}
3992
3993fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3994    out.clause_flags.values().flatten().any(|instance| {
3995        instance
3996            .iter()
3997            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3998    }) || out
3999        .flags
4000        .iter()
4001        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
4002}
4003
4004fn validate_clause_relationships(
4005    out: &mut ParseOutput,
4006    overridden_flags: &HashSet<String>,
4007    custom_env: Option<&HashMap<String, String>>,
4008    explicit_instances: Option<&[HashSet<String>]>,
4009) {
4010    let Some(clause) = out.cmd.clause.as_ref() else {
4011        return;
4012    };
4013    let Some(instances) = out.clauses.get(&clause.name) else {
4014        return;
4015    };
4016    let command_flag_is_explicit = |selector: &str| {
4017        out.available_flags
4018            .values()
4019            .chain(out.flags.keys())
4020            .filter(|flag| !is_clause_scoped_flag(out, flag))
4021            .any(|flag| {
4022                flag_matches_selector(flag, selector)
4023                    && !overridden_flags.contains(&flag.name)
4024                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4025            })
4026    };
4027    let command_flag_matches_value = |selector: &str, expected: &str| {
4028        out.available_flags
4029            .values()
4030            .chain(out.flags.keys())
4031            .filter(|flag| !is_clause_scoped_flag(out, flag))
4032            .find(|flag| flag_matches_selector(flag, selector))
4033            .is_some_and(|flag| {
4034                !overridden_flags.contains(&flag.name)
4035                    && explicit_flag_has_value(flag, expected, out, custom_env)
4036            })
4037    };
4038    let command_flag_is_satisfied = |selector: &str| {
4039        out.available_flags
4040            .values()
4041            .chain(out.flags.keys())
4042            .filter(|flag| !is_clause_scoped_flag(out, flag))
4043            .any(|flag| flag_matches_selector(flag, selector))
4044            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
4045    };
4046    let mut errors = Vec::new();
4047    for (instance_index, instance) in instances.iter().enumerate() {
4048        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
4049        let scoped = out
4050            .clause_flags
4051            .get(&clause.name)
4052            .and_then(|instances| instances.get(instance_index));
4053        let scoped_flag = |selector: &str| {
4054            clause
4055                .flags
4056                .iter()
4057                .find(|flag| flag_matches_selector(flag, selector))
4058        };
4059        let scoped_value = |flag: &SpecFlag| {
4060            scoped.and_then(|values| {
4061                values
4062                    .iter()
4063                    .find(|(present, _)| present.name == flag.name)
4064                    .map(|(_, value)| value)
4065            })
4066        };
4067        let arg_is_explicit = |selector: &str| {
4068            instance
4069                .keys()
4070                .any(|arg| !selector.starts_with('-') && arg.name == selector)
4071        };
4072        let selector_is_explicit = |selector: &str| {
4073            scoped_flag(selector).is_some_and(|flag| {
4074                explicit
4075                    .map(|given| given.contains(&flag.name))
4076                    .unwrap_or_else(|| scoped_value(flag).is_some())
4077            }) || command_flag_is_explicit(selector)
4078                || arg_is_explicit(selector)
4079        };
4080        let selector_has_value = |selector: &str, expected: &str| {
4081            scoped_flag(selector)
4082                .filter(|flag| {
4083                    explicit
4084                        .map(|given| given.contains(&flag.name))
4085                        .unwrap_or_else(|| scoped_value(flag).is_some())
4086                })
4087                .and_then(scoped_value)
4088                .is_some_and(|value| parse_value_has(value, expected))
4089                || command_flag_matches_value(selector, expected)
4090                || instance.iter().any(|(arg, value)| {
4091                    !selector.starts_with('-')
4092                        && arg.name == selector
4093                        && parse_value_has(value, expected)
4094                })
4095        };
4096        let selector_is_satisfied = |selector: &str| {
4097            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4098                || command_flag_is_satisfied(selector)
4099                || arg_is_explicit(selector)
4100        };
4101        let selector_name = |selector: &str| {
4102            scoped_flag(selector)
4103                .map(|flag| flag.name.clone())
4104                .or_else(|| selector_flag_name(selector, out))
4105                .unwrap_or_else(|| selector.to_string())
4106        };
4107
4108        for flag in &clause.flags {
4109            let value = scoped_value(flag);
4110            if value.is_none() {
4111                let required_if = flag
4112                    .required_if
4113                    .iter()
4114                    .any(|selector| selector_is_explicit(selector));
4115                let required_if_eq = flag
4116                    .required_if_eq
4117                    .iter()
4118                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4119                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4120                    && flag
4121                        .required_if_eq_all
4122                        .iter()
4123                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4124                let unless_any = flag
4125                    .required_unless
4126                    .iter()
4127                    .any(|selector| selector_is_explicit(selector));
4128                let unless_all = !flag.required_unless_all.is_empty()
4129                    && flag
4130                        .required_unless_all
4131                        .iter()
4132                        .all(|selector| selector_is_explicit(selector));
4133                let required_unless = !(unless_any
4134                    || unless_all
4135                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4136                if flag.required
4137                    || required_if
4138                    || required_if_eq
4139                    || required_if_eq_all
4140                    || required_unless
4141                {
4142                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
4143                }
4144                continue;
4145            }
4146
4147            let explicitly_provided = explicit
4148                .map(|given| given.contains(&flag.name))
4149                .unwrap_or(true);
4150
4151            if explicitly_provided {
4152                for other in &flag.conflicts {
4153                    if selector_is_explicit(other) {
4154                        errors.push(UsageErr::InvalidFlag {
4155                            token: format!("--{}", flag.name),
4156                            reason: format!("conflicts with {other}"),
4157                            span: (0, 0).into(),
4158                            input: format!("--{} {other}", flag.name),
4159                        });
4160                    }
4161                }
4162                for other in &flag.requires {
4163                    if !selector_is_satisfied(other) {
4164                        errors.push(UsageErr::MissingFlag(selector_name(other)));
4165                    }
4166                }
4167                for condition in &flag.requires_if {
4168                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
4169                        && !selector_is_satisfied(&condition.requires)
4170                    {
4171                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4172                    }
4173                }
4174            }
4175            if let (true, Some(value)) = (flag.var, value) {
4176                let count = match value {
4177                    ParseValue::MultiString(values) => values.len(),
4178                    ParseValue::MultiBool(values) => values.len(),
4179                    _ => 1,
4180                };
4181                if let Some(min) = flag.var_min.filter(|min| count < *min) {
4182                    errors.push(UsageErr::VarFlagTooFew {
4183                        name: flag.name.clone(),
4184                        min,
4185                        got: count,
4186                    });
4187                }
4188                if let Some(max) = flag.var_max.filter(|max| count > *max) {
4189                    errors.push(UsageErr::VarFlagTooMany {
4190                        name: flag.name.clone(),
4191                        max,
4192                        got: count,
4193                    });
4194                }
4195            }
4196            if let (Some(arg), Some(value)) = (&flag.arg, value) {
4197                validate_expression(
4198                    &flag.name,
4199                    arg.validate.as_deref(),
4200                    arg.validate_error.as_deref(),
4201                    value,
4202                    &mut errors,
4203                );
4204            }
4205        }
4206        for arg in &clause.args {
4207            let given = instance.keys().any(|present| present.name == arg.name);
4208            if !given {
4209                let required_if = arg
4210                    .required_if
4211                    .iter()
4212                    .any(|selector| selector_is_explicit(selector));
4213                let required_if_eq = arg
4214                    .required_if_eq
4215                    .iter()
4216                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4217                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4218                    && arg
4219                        .required_if_eq_all
4220                        .iter()
4221                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4222                let unless_any = arg
4223                    .required_unless
4224                    .iter()
4225                    .any(|selector| selector_is_explicit(selector));
4226                let unless_all = !arg.required_unless_all.is_empty()
4227                    && arg
4228                        .required_unless_all
4229                        .iter()
4230                        .all(|selector| selector_is_explicit(selector));
4231                let required_unless = !(unless_any
4232                    || unless_all
4233                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4234                if required_if || required_if_eq || required_if_eq_all || required_unless {
4235                    errors.push(UsageErr::MissingClauseArg {
4236                        clause: clause.name.clone(),
4237                        instance: instance_index + 1,
4238                        arg: arg.name.clone(),
4239                    });
4240                }
4241                continue;
4242            }
4243            for other in &arg.conflicts {
4244                if selector_is_explicit(other) {
4245                    errors.push(UsageErr::InvalidFlag {
4246                        token: arg.name.clone(),
4247                        reason: format!("conflicts with {other}"),
4248                        span: (0, 0).into(),
4249                        input: format!("{} {other}", arg.name),
4250                    });
4251                }
4252            }
4253            for other in &arg.requires {
4254                if selector_is_satisfied(other) {
4255                    continue;
4256                }
4257                if other.starts_with('-') {
4258                    errors.push(UsageErr::MissingFlag(selector_name(other)));
4259                } else {
4260                    errors.push(UsageErr::MissingClauseArg {
4261                        clause: clause.name.clone(),
4262                        instance: instance_index + 1,
4263                        arg: other.clone(),
4264                    });
4265                }
4266            }
4267        }
4268    }
4269    out.errors.extend(errors);
4270}
4271
4272fn selector_explicit_has_value(
4273    selector: &str,
4274    expected: &str,
4275    out: &ParseOutput,
4276    overridden_flags: &HashSet<String>,
4277    custom_env: Option<&HashMap<String, String>>,
4278) -> bool {
4279    if let Some(flag) = selected_clause_flag(out, selector) {
4280        return clause_flag_has_value(out, flag, expected);
4281    }
4282    if let Some(flag) = out
4283        .available_flags
4284        .values()
4285        .chain(out.flags.keys())
4286        .filter(|flag| !is_clause_scoped_flag(out, flag))
4287        .find(|flag| flag_matches_selector(flag, selector))
4288    {
4289        return !overridden_flags.contains(&flag.name)
4290            && explicit_flag_has_value(flag, expected, out, custom_env);
4291    }
4292    let Some(arg) = selector_arg(selector, out) else {
4293        return false;
4294    };
4295    let parsed = out
4296        .args
4297        .iter()
4298        .find(|(given, _)| given.name == arg.name)
4299        .map(|(_, value)| value)
4300        .or_else(|| {
4301            out.clauses.values().flatten().find_map(|instance| {
4302                instance
4303                    .iter()
4304                    .find(|(given, _)| given.name == arg.name)
4305                    .map(|(_, value)| value)
4306            })
4307        });
4308    if let Some(value) = parsed {
4309        return match value {
4310            ParseValue::String(value) => value == expected,
4311            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4312            ParseValue::Bool(value) => value.to_string() == expected,
4313            ParseValue::MultiBool(values) => {
4314                values.iter().any(|value| value.to_string() == expected)
4315            }
4316        };
4317    }
4318    let value = arg.env_names().find_map(|env| match custom_env {
4319        Some(values) => values.get(env).cloned(),
4320        None => std::env::var(env).ok(),
4321    });
4322    value.is_some_and(|value| match arg.delimiter {
4323        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4324        None => value == expected,
4325    })
4326}
4327
4328fn selector_is_explicit(
4329    selector: &str,
4330    out: &ParseOutput,
4331    overridden_flags: &HashSet<String>,
4332    custom_env: Option<&HashMap<String, String>>,
4333) -> bool {
4334    let scoped_flag_is_explicit =
4335        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4336    let flag_is_explicit = out
4337        .available_flags
4338        .values()
4339        .chain(out.flags.keys())
4340        .filter(|flag| !is_clause_scoped_flag(out, flag))
4341        .any(|flag| {
4342            flag_matches_selector(flag, selector)
4343                && !overridden_flags.contains(&flag.name)
4344                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4345        });
4346    scoped_flag_is_explicit
4347        || flag_is_explicit
4348        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4349}
4350
4351/// The name of the flag a selector points at, for an error that has to name it.
4352///
4353/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
4354/// about a flag that is *missing* has to say which one, and the selector may be a short
4355/// form or an alias rather than the name.
4356fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4357    if let Some(flag) = selected_clause_flag(out, selector) {
4358        return Some(flag.name.clone());
4359    }
4360    out.available_flags
4361        .values()
4362        .chain(out.flags.keys())
4363        .filter(|flag| !is_clause_scoped_flag(out, flag))
4364        .find(|flag| flag_matches_selector(flag, selector))
4365        .map(|flag| flag.name.clone())
4366        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4367}
4368
4369/// Whether a selector's flag ended up with a value, however it got one.
4370///
4371/// The rule for a *positive* relationship, and the difference from
4372/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4373/// exclusivity — has to count only what was given, or a flag with a default would
4374/// conflict with everything and no command line would parse. A positive one asks whether
4375/// the flag it names has a value, and a default is a value: that is already how plain
4376/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4377/// otherwise would have made the same flag missing here and present ten lines below.
4378fn selector_is_satisfied(
4379    selector: &str,
4380    out: &ParseOutput,
4381    overridden_flags: &HashSet<String>,
4382    custom_env: Option<&HashMap<String, String>>,
4383) -> bool {
4384    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4385        return true;
4386    }
4387    let flag_is_satisfied = out
4388        .available_flags
4389        .values()
4390        .chain(out.flags.keys())
4391        .filter(|flag| !is_clause_scoped_flag(out, flag))
4392        .filter(|flag| flag_matches_selector(flag, selector))
4393        .any(|flag| {
4394            !overridden_flags.contains(&flag.name)
4395                && (!flag.default.is_empty()
4396                    || flag.arg.iter().any(|a| !a.default.is_empty())
4397                    || flag.default_if.iter().any(|condition| {
4398                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4399                    }))
4400        });
4401    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4402}
4403
4404fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4405    // Bare words are positional selectors. Keep accepting a flag's internal name above
4406    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4407    if selector.starts_with('-') {
4408        return None;
4409    }
4410    out.cmds
4411        .iter()
4412        .flat_map(active_args)
4413        .find(|arg| arg.name == selector)
4414}
4415
4416fn arg_is_explicit(
4417    arg: &SpecArg,
4418    out: &ParseOutput,
4419    custom_env: Option<&HashMap<String, String>>,
4420) -> bool {
4421    out.args.keys().any(|given| given.name == arg.name)
4422        || out
4423            .clauses
4424            .values()
4425            .flatten()
4426            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4427        || arg
4428            .env
4429            .as_ref()
4430            .is_some_and(|env| env_contains(custom_env, env))
4431}
4432
4433fn apply_flag_overrides(
4434    flag: &Arc<SpecFlag>,
4435    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4436    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4437    pending_flags: &mut Vec<Arc<SpecFlag>>,
4438    overridden_flags: &mut HashSet<String>,
4439    // The reportable half of the same fact: which flag did the overriding. The set above
4440    // only stops a default or an environment value restoring what was overridden, and
4441    // "`--quiet` is unset despite its default" has no answer without the name.
4442    attributed: &mut BTreeMap<String, String>,
4443) {
4444    let overridden_names: HashSet<String> = available_flags
4445        .values()
4446        .chain(parsed_flags.keys())
4447        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4448        .map(|other| other.name.clone())
4449        .collect();
4450
4451    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4452    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4453    for name in &overridden_names {
4454        attributed.insert(name.clone(), flag.name.clone());
4455    }
4456    overridden_flags.extend(overridden_names);
4457    // An explicit occurrence always restores this flag, including self-overrides.
4458    overridden_flags.remove(&flag.name);
4459    attributed.remove(&flag.name);
4460}
4461
4462/// The help a parse answers `--help` with. A parse is a program running, so its page lists
4463/// what `choices run=` prints, run against the environment the parse was given.
4464#[cfg(feature = "cli-help")]
4465fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool, env: Env<'_>) -> UsageErr {
4466    UsageErr::Help(docs::cli::render_runtime_help(
4467        spec,
4468        cmd,
4469        long,
4470        docs::cli::Style::PLAIN,
4471        env,
4472    ))
4473}
4474
4475#[cfg(feature = "cli-help")]
4476fn render_help_all_err(spec: &Spec, cmd: &SpecCommand, env: Env<'_>) -> UsageErr {
4477    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand, env: Env<'_>) {
4478        if !out.is_empty() {
4479            out.push('\n');
4480        }
4481        out.push_str(&docs::cli::render_runtime_help(
4482            spec,
4483            cmd,
4484            true,
4485            docs::cli::Style::PLAIN,
4486            env,
4487        ));
4488        let mut children: Vec<_> = cmd
4489            .subcommands
4490            .values()
4491            .filter(|child| !child.hide)
4492            .collect();
4493        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4494        for child in children {
4495            append(out, spec, child, env);
4496        }
4497    }
4498
4499    let mut out = String::new();
4500    append(&mut out, spec, cmd, env);
4501    UsageErr::Help(out)
4502}
4503
4504#[cfg(not(feature = "cli-help"))]
4505fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool, _env: Env<'_>) -> UsageErr {
4506    UsageErr::Help("help".to_string())
4507}
4508
4509#[cfg(not(feature = "cli-help"))]
4510fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand, _env: Env<'_>) -> UsageErr {
4511    UsageErr::Help("help".to_string())
4512}
4513
4514/// The version to answer with. `--version` prefers the long text and `-V` the concise
4515/// one, each falling back to the other when only one is declared.
4516fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4517    let value = if long {
4518        spec.long_version.as_ref().or(spec.version.as_ref())
4519    } else {
4520        spec.version.as_ref().or(spec.long_version.as_ref())
4521    };
4522    UsageErr::Version(value.cloned().unwrap_or_default())
4523}
4524
4525/// The environment a parse was given, which is also what a `choices run=` command runs with.
4526type Env<'a> = Option<&'a HashMap<String, String>>;
4527
4528fn render_action_err(
4529    spec: &Spec,
4530    cmd: &SpecCommand,
4531    flag: &SpecFlag,
4532    spelling: &str,
4533    env: Env<'_>,
4534) -> UsageErr {
4535    use crate::SpecFlagAction;
4536    match flag.action {
4537        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--"), env),
4538        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false, env),
4539        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true, env),
4540        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd, env),
4541        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4542        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4543    }
4544}
4545
4546/// Report a required flag value that was displaced by a later option.
4547fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4548    let token = flag
4549        .long
4550        .first()
4551        .map(|long| format!("--{long}"))
4552        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4553        .unwrap_or_else(|| flag.name.clone());
4554    UsageErr::InvalidFlag {
4555        token: token.clone(),
4556        reason: "requires an argument".to_string(),
4557        span: (0, 0).into(),
4558        input: format!("{token} {following}"),
4559    }
4560}
4561
4562#[derive(Copy, Clone)]
4563struct ChoiceTarget<'a> {
4564    kind: &'a str,
4565    name: &'a str,
4566}
4567
4568impl<'a> ChoiceTarget<'a> {
4569    fn arg(arg: &'a SpecArg) -> Self {
4570        Self {
4571            kind: "arg",
4572            name: &arg.name,
4573        }
4574    }
4575
4576    fn option(flag: &'a SpecFlag) -> Self {
4577        Self {
4578            kind: "option",
4579            name: &flag.name,
4580        }
4581    }
4582}
4583
4584/// Whether every letter of a short token names a flag in scope.
4585///
4586/// Scanning stops at the first letter whose flag takes a value, because everything
4587/// after it is that value rather than more letters.
4588fn short_bundle_is_known(
4589    spec: &Spec,
4590    cmds: &[SpecCommand],
4591    available: &BTreeMap<String, Arc<SpecFlag>>,
4592    token: &str,
4593) -> bool {
4594    for c in token.chars().skip(1) {
4595        match available.get(&format!("-{c}")) {
4596            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4597            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4598            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4599            // clap all answer it with help.
4600            None if supplies_short(spec, cmds, c) => {}
4601            None => return false,
4602            Some(f) if f.arg.is_some() => return true,
4603            Some(_) => {}
4604        }
4605    }
4606    true
4607}
4608
4609/// The response `-h` or `-V` produces where nothing declares that letter.
4610///
4611/// The letter form of the flags the parser supplies rather than a spec declaring them,
4612/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4613/// about here one letter at a time, because a bundle is read one letter at a time.
4614///
4615/// Always the short response: `-h` is short help however many letters share its token,
4616/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4617/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4618fn supplied_short(
4619    spec: &Spec,
4620    cmds: &[SpecCommand],
4621    letter: char,
4622    env: Env<'_>,
4623) -> Option<UsageErr> {
4624    let cmd = cmds.last()?;
4625    match letter {
4626        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false, env)),
4627        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4628        _ => None,
4629    }
4630}
4631
4632/// Whether [`supplied_short`] would answer for `letter`, without drawing the page it would
4633/// answer with — which can mean running `choices run=` commands.
4634fn supplies_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> bool {
4635    cmds.last().is_some_and(|cmd| match letter {
4636        'h' => is_help_arg(spec, cmd, "-h"),
4637        'V' => is_version_arg(spec, cmds, "-V"),
4638        _ => false,
4639    })
4640}
4641
4642/// Refuse a flag-like token that named nothing, if this command asked for that.
4643///
4644/// Called from the flag branches, where the lookup has just failed and nothing from
4645/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4646/// than after setting `-a`.
4647fn reject_unknown_flag_if_asked(
4648    spec: &Spec,
4649    path: &[SpecCommand],
4650    token: &str,
4651) -> Result<(), UsageErr> {
4652    // A lone `-` is a value by convention. A negative number reaches this only
4653    // when no pending value opted into the narrower exception.
4654    if !is_flag_like(token) {
4655        return Ok(());
4656    }
4657    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4658        return Ok(());
4659    }
4660    Err(UsageErr::InvalidFlag {
4661        token: token.to_string(),
4662        reason: "no such flag".to_string(),
4663        span: (0, 0).into(),
4664        input: token.to_string(),
4665    })
4666}
4667
4668/// Whether a flag-like token that matches nothing is a value or an error, here.
4669///
4670/// The nearest enclosing command that stated a preference wins, then the spec,
4671/// then the default. Inherited, unlike `effect`: it describes how a command line
4672/// is read, and a CLI that forwards options tends to forward them at every level.
4673fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4674    path.iter()
4675        .rev()
4676        .find_map(|cmd| cmd.unknown_flags)
4677        .or(spec.unknown_flags)
4678        .unwrap_or_default()
4679}
4680
4681/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4682/// ones.
4683///
4684/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4685/// a field may make the narrower negative-number exception.
4686fn is_flag_like(token: &str) -> bool {
4687    match token.strip_prefix('-') {
4688        None | Some("") => false,
4689        Some(_) => true,
4690    }
4691}
4692
4693fn is_negative_number(token: &str) -> bool {
4694    token.strip_prefix('-').is_some_and(is_number)
4695}
4696
4697/// Whether a flag may claim its following token as a detached value.
4698///
4699/// A required value keeps the historical negative-number exception. A value
4700/// that may be omitted needs an explicit opt-in to distinguish a negative value
4701/// from the flag's bare form.
4702fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4703    !flag.require_equals
4704        && (!is_flag_like(token)
4705            || flag.allow_hyphen_values()
4706            || (is_negative_number(token)
4707                && (flag
4708                    .arg
4709                    .as_ref()
4710                    .is_some_and(|arg| arg.allow_negative_numbers)
4711                    || (flag.default_missing.is_none() && !flag.value_optional))))
4712}
4713
4714fn record_scalar_flag_occurrence(
4715    cmds: &[SpecCommand],
4716    flag: &Arc<SpecFlag>,
4717    command_level: usize,
4718    bool_value: Option<bool>,
4719    occurrences: &mut HashMap<(usize, usize), u8>,
4720    errors: &mut Vec<UsageErr>,
4721) {
4722    let strict = cmds.get(command_level).is_some_and(|cmd| {
4723        !cmd.args_override_self
4724            || cmd
4725                .clause
4726                .as_ref()
4727                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4728    });
4729    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4730    if !strict || flag.count || collects_values {
4731        return;
4732    }
4733
4734    let bit = match bool_value {
4735        Some(false) if flag.negate.is_some() => 0b10,
4736        _ => 0b01,
4737    };
4738    let key = (Arc::as_ptr(flag) as usize, command_level);
4739    let seen = occurrences.entry(key).or_default();
4740    if *seen & bit != 0 {
4741        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4742    }
4743    *seen |= bit;
4744}
4745
4746/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4747/// external command.
4748///
4749/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4750/// so it was never a candidate to *select* anything either. usage-argv uses the
4751/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4752/// treated every token that `starts_with('-')` as a flag.
4753fn is_command_word(token: &str) -> bool {
4754    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4755}
4756
4757/// Whether an otherwise numeric-looking token is an exact declared short flag.
4758///
4759/// This check belongs in both parse phases: phase 1 must skip the flag while it
4760/// searches for a later subcommand, and phase 2 must bind it instead of offering
4761/// it to an `allow_negative_numbers` positional.
4762fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4763    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4764}
4765
4766/// Whether an unmatched word belongs to the root's default command.
4767///
4768/// Ordinary words always do. A negative number only does when the default command's
4769/// first positional explicitly accepts one; otherwise it stays at the root, matching
4770/// usage-argv and generated Go.
4771fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4772    !is_negative_number(token)
4773        || cmd
4774            .find_subcommand(default_name)
4775            .and_then(|default| default.args.first())
4776            .is_some_and(|arg| arg.allow_negative_numbers)
4777}
4778
4779/// Digits, at most one `.`, and an optional exponent.
4780///
4781/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4782/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4783/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4784/// two disagreed about `-1e5` when one used a float parse and the other did not.
4785fn is_number(rest: &str) -> bool {
4786    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4787        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4788        None => (rest, None),
4789    };
4790
4791    let mut seen_digit = false;
4792    let mut seen_dot = false;
4793    for c in mantissa.chars() {
4794        match c {
4795            '0'..='9' => seen_digit = true,
4796            '.' if !seen_dot => seen_dot = true,
4797            _ => return false,
4798        }
4799    }
4800    if !seen_digit {
4801        return false;
4802    }
4803
4804    match exponent {
4805        None => true,
4806        Some(exp) => {
4807            let digits = exp
4808                .strip_prefix('+')
4809                .or_else(|| exp.strip_prefix('-'))
4810                .unwrap_or(exp);
4811            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4812        }
4813    }
4814}
4815
4816/// Bind one value to the flag waiting for it, and let a variadic argument go on
4817/// collecting from the words that follow.
4818///
4819/// Every route to a flag's value comes through here — the following word, the text
4820/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4821/// like — so that all three agree on how many values the flag ends up with.
4822#[allow(clippy::too_many_arguments)]
4823fn bind_pending_flag_value(
4824    spec: &Spec,
4825    cmd: &SpecCommand,
4826    errors: &mut Vec<UsageErr>,
4827    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4828    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4829    word: &mut String,
4830    input: &mut VecDeque<Token>,
4831    custom_env: Option<&HashMap<String, String>>,
4832    trace: &mut Trace,
4833    // Which token supplied `word`, and whether it rode along on the flag's own token
4834    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4835    // their own positions and are recorded where they are read.
4836    argv: usize,
4837    attached: bool,
4838) -> miette::Result<bool> {
4839    // Held before the drain pops it, along with what the flag is already carrying: a
4840    // `var_max` bounds the values this occurrence takes, not the list they are appended
4841    // to, so a second `--include` starts counting again.
4842    let collecting = flag_awaiting_value
4843        .last()
4844        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4845        .cloned()
4846        .map(|flag| {
4847            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4848            (flag, carried)
4849        });
4850    let mut bound = vec![];
4851    let refused = drain_pending_flag_values(
4852        spec,
4853        cmd,
4854        errors,
4855        flags,
4856        flag_awaiting_value,
4857        word,
4858        custom_env,
4859        &mut bound,
4860    )?;
4861    for (flag, values) in bound {
4862        trace.record(
4863            argv,
4864            TokenRole::Value {
4865                flag,
4866                values,
4867                attached,
4868            },
4869        );
4870    }
4871    if refused {
4872        return Ok(true);
4873    }
4874    let Some((flag, carried)) = collecting else {
4875        return Ok(false);
4876    };
4877    collect_variadic_flag_values(
4878        spec,
4879        cmd,
4880        errors,
4881        flags,
4882        flag_awaiting_value,
4883        &flag,
4884        carried,
4885        input,
4886        custom_env,
4887        trace,
4888    )
4889}
4890
4891/// Keep feeding a flag whose argument is variadic from the words that follow it.
4892///
4893/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4894/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4895/// This is greedy by design — a command declaring both such a flag and positionals will
4896/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4897///
4898/// `carried` is what the flag already held when this occurrence began, so the bound
4899/// counts this run rather than everything the flag has collected across the command
4900/// line. Each value goes through the same drain as the first, so choices are checked
4901/// and the value lands in the same list rather than by a second route that could
4902/// disagree.
4903#[allow(clippy::too_many_arguments)]
4904fn collect_variadic_flag_values(
4905    spec: &Spec,
4906    cmd: &SpecCommand,
4907    errors: &mut Vec<UsageErr>,
4908    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4909    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4910    flag: &Arc<SpecFlag>,
4911    carried: usize,
4912    input: &mut VecDeque<Token>,
4913    custom_env: Option<&HashMap<String, String>>,
4914    trace: &mut Trace,
4915) -> miette::Result<bool> {
4916    let max = flag
4917        .arg
4918        .as_ref()
4919        .and_then(|arg| arg.var_max)
4920        .unwrap_or(usize::MAX);
4921    while flags
4922        .get(flag)
4923        .map(value_count)
4924        .unwrap_or(0)
4925        .saturating_sub(carried)
4926        < max
4927    {
4928        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4929            break;
4930        };
4931        if flag
4932            .arg
4933            .as_ref()
4934            .and_then(|arg| arg.value_terminator.as_deref())
4935            == Some(next)
4936        {
4937            let terminator = input.pop_front().unwrap();
4938            trace.record(
4939                terminator.argv,
4940                TokenRole::ValueTerminator {
4941                    ends: flag.name.clone(),
4942                },
4943            );
4944            break;
4945        }
4946        // The separator is left where it is: stopping here hands it to the arm that
4947        // knows what it means, rather than reading it as one more value.
4948        if next == "--"
4949            || (is_flag_like(next)
4950                && !(flag
4951                    .arg
4952                    .as_ref()
4953                    .is_some_and(|arg| arg.allow_negative_numbers)
4954                    && is_negative_number(next)))
4955        {
4956            break;
4957        }
4958        let taken = input.pop_front().unwrap();
4959        let argv = taken.argv;
4960        let mut word = taken.word;
4961        flag_awaiting_value.push(Arc::clone(flag));
4962        let mut bound = vec![];
4963        let refused = drain_pending_flag_values(
4964            spec,
4965            cmd,
4966            errors,
4967            flags,
4968            flag_awaiting_value,
4969            &mut word,
4970            custom_env,
4971            &mut bound,
4972        )?;
4973        for (flag, values) in bound {
4974            // A later word of the same occurrence is its own token, and never attached:
4975            // only the first value can ride along on the flag.
4976            trace.record(
4977                argv,
4978                TokenRole::Value {
4979                    flag,
4980                    values,
4981                    attached: false,
4982                },
4983            );
4984        }
4985        if refused {
4986            return Ok(true);
4987        }
4988    }
4989    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4990    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4991    // several values, so the run can end up *past* the bound rather than on it, and stopping
4992    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4993    // case — three values out of the one word the loop was entitled to take.
4994    //
4995    // Counted against `carried` like the loop itself, so this stays a statement about the
4996    // occurrence rather than about the list the occurrences build up.
4997    let taken = flags
4998        .get(flag)
4999        .map(value_count)
5000        .unwrap_or(0)
5001        .saturating_sub(carried);
5002    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min)
5003        && taken < min
5004    {
5005        errors.push(UsageErr::VarFlagTooFew {
5006            name: flag.name.clone(),
5007            min,
5008            got: taken,
5009        });
5010    }
5011    if taken > max {
5012        errors.push(UsageErr::VarFlagTooMany {
5013            name: flag.name.clone(),
5014            max,
5015            got: taken,
5016        });
5017    }
5018    Ok(false)
5019}
5020
5021/// How many values a flag is holding, for a bound that counts them.
5022fn value_count(value: &ParseValue) -> usize {
5023    match value {
5024        ParseValue::MultiString(values) => values.len(),
5025        ParseValue::MultiBool(values) => values.len(),
5026        _ => 1,
5027    }
5028}
5029
5030/// Finish a value-optional flag that was given with no value.
5031///
5032/// Returns whether anything was bound. Completions keep the flag waiting — a
5033/// half-typed `--color ` is a question about the value — so this is asked only
5034/// once a full parse has decided the value is not coming, or once the next token
5035/// has made that decision.
5036///
5037/// The missing string is a real value: if the flag names `choices`, it has to
5038/// be one of them, the same way an env var or a `default` is checked. Binding
5039/// first and failing later would leave the flag set to a value the spec forbids.
5040fn try_bind_default_missing(
5041    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5042    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5043    custom_env: Option<&HashMap<String, String>>,
5044    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
5045) -> miette::Result<bool> {
5046    let Some(flag) = flag_awaiting_value.last() else {
5047        return Ok(false);
5048    };
5049    let value = match flag.default_missing.clone() {
5050        Some(value) => value,
5051        None if flag.value_optional => {
5052            let flag = flag_awaiting_value.pop().unwrap();
5053            // Presence in the map distinguishes this from an absent flag; an
5054            // empty collection distinguishes it from an explicitly empty
5055            // `--flag=` string without inventing a sentinel value.
5056            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
5057            origins
5058                .entry(Arc::clone(&flag))
5059                .or_default()
5060                .push(ValueOrigin::DefaultMissing);
5061            if flag.var {
5062                // A repeated bare occurrence is still an occurrence. The string collection
5063                // uses an empty value for it, just as the concrete `default_missing` path
5064                // pushes one value per occurrence; otherwise bounds and consumers silently
5065                // lose every bare repeat after the first.
5066                flags
5067                    .entry(flag)
5068                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
5069                    .try_as_multi_string_mut()
5070                    .unwrap()
5071                    .push(String::new());
5072            } else if variadic_value {
5073                // A variadic occurrence stays pending after each value. Reaching the next
5074                // flag (or EOF) closes that same occurrence; it must not erase what it took.
5075                flags
5076                    .entry(flag)
5077                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
5078            } else {
5079                // A scalar pending here is a new bare occurrence. The normal permissive
5080                // repeat policy makes the later occurrence a correction, including a
5081                // correction from an explicit value back to the bare tri-state.
5082                flags.insert(flag, ParseValue::MultiString(Vec::new()));
5083            }
5084            return Ok(true);
5085        }
5086        None => return Ok(false),
5087    };
5088    if let Some(arg) = flag.arg.as_ref() {
5089        validate_choice_value(
5090            ChoiceTarget::option(flag),
5091            &value,
5092            arg.choices.as_ref(),
5093            custom_env,
5094        )?;
5095    }
5096    let flag = flag_awaiting_value.pop().unwrap();
5097    origins
5098        .entry(Arc::clone(&flag))
5099        .or_default()
5100        .push(ValueOrigin::DefaultMissing);
5101    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5102    if collecting {
5103        let arr = flags
5104            .entry(flag)
5105            .or_insert_with(|| ParseValue::MultiString(vec![]))
5106            .try_as_multi_string_mut()
5107            .unwrap();
5108        arr.push(value);
5109    } else {
5110        flags.insert(flag, ParseValue::String(value));
5111    }
5112    Ok(true)
5113}
5114
5115/// `bound` collects what each drained flag took, in the order it took it. The values are
5116/// the word after any `delimiter` split, which is the only place that split is known: by the
5117/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
5118/// second occurrence has appended to the same list.
5119#[allow(clippy::too_many_arguments)]
5120fn drain_pending_flag_values(
5121    spec: &Spec,
5122    cmd: &SpecCommand,
5123    errors: &mut Vec<UsageErr>,
5124    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5125    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5126    word: &mut String,
5127    custom_env: Option<&HashMap<String, String>>,
5128    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5129) -> miette::Result<bool> {
5130    while let Some(flag) = flag_awaiting_value.pop() {
5131        let arg = flag.arg.as_ref().unwrap();
5132        // Split before anything judges the word, because after the split it is no longer
5133        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
5134        // Judging first would reject the whole word against a list neither half is on.
5135        let parts: Vec<String> = match arg.delimiter {
5136            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5137            None => vec![std::mem::take(word)],
5138        };
5139        for part in &parts {
5140            if validate_choices(
5141                spec,
5142                cmd,
5143                errors,
5144                ChoiceTarget::option(&flag),
5145                part,
5146                arg.choices.as_ref(),
5147                custom_env,
5148            )? {
5149                return Ok(true);
5150            }
5151        }
5152        word.clear();
5153        bound.push((Arc::clone(&flag), parts.clone()));
5154        // Two ways to hold several values, and both record a list: a `var` flag
5155        // collects one per occurrence, a variadic argument collects several from one.
5156        if flag.var || arg.var {
5157            let arr = flags
5158                .entry(flag)
5159                .or_insert_with(|| ParseValue::MultiString(vec![]))
5160                .try_as_multi_string_mut()
5161                .unwrap();
5162            arr.extend(parts);
5163        } else {
5164            // Nowhere for a second value to go, so the word stands as it was typed. A
5165            // delimiter on a flag that takes one value is refused where it is written.
5166            flags.insert(
5167                flag,
5168                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5169            );
5170        }
5171    }
5172    Ok(false)
5173}
5174
5175fn choice_error(
5176    target: ChoiceTarget<'_>,
5177    value: &str,
5178    choices: Option<&SpecChoices>,
5179    custom_env: Option<&HashMap<String, String>>,
5180) -> Option<String> {
5181    let choices = choices?;
5182    if !choices.strict {
5183        return None;
5184    }
5185    // The declared values and `env=` first, so a value they accept never costs a command.
5186    match choices.matches_resolved(value, custom_env) {
5187        Ok(true) => return None,
5188        Ok(false) => {}
5189        // Only a command could say, and this parse may not run one: let the value through
5190        // rather than report an error the real run might not have.
5191        Err(_) if choices.run().is_some() && crate::spec::choices::runs_disabled() => return None,
5192        Err(err) => {
5193            return Some(format!(
5194                "Could not check {} {}: {value} against its choices: {err}",
5195                target.kind, target.name,
5196            ));
5197        }
5198    }
5199    // Already run by the check above, so this reads what it printed rather than running it
5200    // again.
5201    let values = choices
5202        .resolved_values(custom_env)
5203        .unwrap_or_else(|_| choices.values_with_env(custom_env));
5204    if let Some(run) = choices.run()
5205        && values.is_empty()
5206    {
5207        return Some(format!(
5208            "Invalid choice for {} {}: {value}, `{run}` printed no choices",
5209            target.kind, target.name,
5210        ));
5211    }
5212    if let Some(env) = choices.env()
5213        && values.is_empty()
5214    {
5215        return Some(format!(
5216            "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5217            target.kind, target.name,
5218        ));
5219    }
5220    Some(format!(
5221        "Invalid choice for {} {}: {value}, expected one of {}",
5222        target.kind,
5223        target.name,
5224        values.join(", ")
5225    ))
5226}
5227
5228fn validate_choices(
5229    spec: &Spec,
5230    cmd: &SpecCommand,
5231    errors: &mut Vec<UsageErr>,
5232    target: ChoiceTarget<'_>,
5233    value: &str,
5234    choices: Option<&SpecChoices>,
5235    custom_env: Option<&HashMap<String, String>>,
5236) -> miette::Result<bool> {
5237    // A choice named like a help flag is a value, whether it was declared or printed by
5238    // `run=`. A command that cannot be run leaves the word to mean help, which is what a
5239    // user typing it most likely wanted.
5240    if is_help_arg(spec, cmd, value)
5241        && choices.is_some_and(|choices| {
5242            choices.strict && !choices.matches_resolved(value, custom_env).unwrap_or(false)
5243        })
5244    {
5245        errors.push(render_help_err(spec, cmd, value.len() > 2, custom_env));
5246        return Ok(true);
5247    }
5248
5249    if let Some(err) = choice_error(target, value, choices, custom_env) {
5250        bail!("{err}");
5251    }
5252    Ok(false)
5253}
5254
5255fn validate_choice_value(
5256    target: ChoiceTarget<'_>,
5257    value: &str,
5258    choices: Option<&SpecChoices>,
5259    custom_env: Option<&HashMap<String, String>>,
5260) -> miette::Result<()> {
5261    if let Some(err) = choice_error(target, value, choices, custom_env) {
5262        bail!("{err}");
5263    }
5264    Ok(())
5265}
5266
5267fn validate_choice_values(
5268    target: ChoiceTarget<'_>,
5269    values: &[String],
5270    choices: Option<&SpecChoices>,
5271    custom_env: Option<&HashMap<String, String>>,
5272) -> miette::Result<()> {
5273    for value in values {
5274        validate_choice_value(target, value, choices, custom_env)?;
5275    }
5276    Ok(())
5277}
5278
5279/// Everything a parse records about where it stopped: the positional cursor, so callers that
5280/// do not re-run the parse — completions, above all — agree with it, and the token trace.
5281///
5282/// Every exit from the binding phase comes through here, which is what makes it the right
5283/// place to close the trace: whatever is still queued was never read, and saying so is more
5284/// useful than leaving those words out of the report entirely.
5285fn record_stop(
5286    out: &mut ParseOutput,
5287    next_arg_idx: usize,
5288    seen_double_dash: bool,
5289    trace: &mut Trace,
5290    unread: &VecDeque<Token>,
5291) {
5292    out.next_arg = active_args(&out.cmd)
5293        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5294        .cloned()
5295        .map(Arc::new);
5296    out.double_dash_seen = seen_double_dash;
5297    finalize_current_clause(out);
5298    trace.close(unread);
5299    out.tokens = std::mem::take(&mut trace.tokens);
5300}
5301
5302fn finalize_current_clause(out: &mut ParseOutput) {
5303    let Some(clause) = out.cmd.clause.as_ref() else {
5304        return;
5305    };
5306    let has_scoped_flags = clause
5307        .flags
5308        .iter()
5309        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5310    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5311        return;
5312    }
5313    out.clauses
5314        .entry(clause.name.clone())
5315        .or_default()
5316        .push(std::mem::take(&mut out.args));
5317    let mut flags = IndexMap::new();
5318    for clause_flag in &clause.flags {
5319        let key = Arc::new(clause_flag.clone());
5320        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5321            flags.insert(flag, value);
5322        }
5323    }
5324    out.clause_flags
5325        .entry(clause.name.clone())
5326        .or_default()
5327        .push(flags);
5328}
5329
5330fn restore_current_clause(out: &mut ParseOutput) {
5331    let Some(clause) = out.cmd.clause.as_ref() else {
5332        return;
5333    };
5334    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5335        out.args = current;
5336    }
5337    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5338        out.flags.extend(current);
5339    }
5340}
5341
5342fn reset_clause_scalar_occurrences(
5343    out: &ParseOutput,
5344    occurrences: &mut HashMap<(usize, usize), u8>,
5345) {
5346    let Some(clause) = out.cmd.clause.as_ref() else {
5347        return;
5348    };
5349    let names = clause
5350        .flags
5351        .iter()
5352        .map(|flag| flag.name.as_str())
5353        .collect::<HashSet<_>>();
5354    let pointers = unique_flags(out.available_flags.values())
5355        .filter(|flag| names.contains(flag.name.as_str()))
5356        .map(|flag| Arc::as_ptr(flag) as usize)
5357        .collect::<HashSet<_>>();
5358    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5359}
5360
5361fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5362    while active_args(cmd)
5363        .get(idx)
5364        .is_some_and(|arg| arg.sigil.is_some())
5365    {
5366        idx += 1;
5367    }
5368    idx
5369}
5370
5371fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5372    cmd.clause
5373        .as_ref()
5374        .map(|clause| clause.args.as_slice())
5375        .unwrap_or(cmd.args.as_slice())
5376}
5377
5378fn match_sigil_arg<'a>(
5379    cmd: &'a SpecCommand,
5380    word: &'a str,
5381) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5382    cmd.args
5383        .iter()
5384        .filter_map(|arg| {
5385            let sigil = arg.sigil.as_deref()?;
5386            let value = word.strip_prefix(sigil)?;
5387            Some((arg, sigil, value))
5388        })
5389        .max_by_key(|(_, sigil, _)| sigil.len())
5390}
5391
5392fn match_sigil_arg_chain<'a>(
5393    cmds: &'a [SpecCommand],
5394    word: &'a str,
5395) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5396    cmds.iter()
5397        .filter_map(|cmd| match_sigil_arg(cmd, word))
5398        .max_by_key(|(_, sigil, _)| sigil.len())
5399}
5400
5401/// Record that `arg` was handed a word before the `--` it requires.
5402///
5403/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
5404/// message is emitted only the first time each arg is seen. The set is also what suppresses the
5405/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
5406/// end of the parse.
5407fn report_double_dash_violation(
5408    arg: &SpecArg,
5409    errors: &mut Vec<UsageErr>,
5410    violations: &mut HashSet<String>,
5411) {
5412    if violations.insert(arg.name.clone()) {
5413        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5414    }
5415}
5416
5417/// `--version` and `-V`, which the parser supplies where the spec declares a version.
5418///
5419/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
5420/// command tables — both of which accepted these spellings while this parser called them
5421/// unknown words. The help page has always listed `-V, --version` under the same
5422/// condition, and said in as many words that it did so "only where a version is
5423/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
5424/// page advertising a flag the parse refused.
5425///
5426/// The root only, because that is where the page lists it: `version` is a property of
5427/// the program, and a subcommand answering with the program's version is a claim no spec
5428/// made. A declared flag wins by arriving first — every scan consults this only after
5429/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
5430/// keeps `--version` supplied beside it.
5431fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5432    (spec.version.is_some() || spec.long_version.is_some())
5433        && cmds.len() == 1
5434        && !spec.cmd.disable_version_flag
5435        && (w == "--version" || w == "-V")
5436}
5437
5438fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5439    spec.disable_help != Some(true)
5440        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5441            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5442}
5443
5444impl ParseOutput {
5445    pub fn as_env(&self) -> BTreeMap<String, String> {
5446        let mut env = BTreeMap::new();
5447        for (flag, val) in &self.flags {
5448            let key = format!("usage_{}", crate::case::snake(&flag.name));
5449            let val = match val {
5450                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5451                ParseValue::String(s) => s.clone(),
5452                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5453                ParseValue::MultiString(s) => crate::shell_words::join(s),
5454            };
5455            env.insert(key, val);
5456        }
5457        for (arg, val) in &self.args {
5458            let key = format!("usage_{}", crate::case::snake(&arg.name));
5459            env.insert(key, val.to_string());
5460        }
5461        // The subcommand path, canonical names joined by spaces (`db migrate`), so a script can
5462        // dispatch on it without re-reading argv. Unset at the root, where there is nothing to
5463        // dispatch on. `cmd` is also an ordinary argument name, so a flag or arg the spec
5464        // declares under it anywhere on the path, clauses included, keeps the variable: a
5465        // script reading `$usage_cmd` for its own `<cmd>` must never get the path instead,
5466        // even when the arg was left out.
5467        let path = self.cmds.iter().skip(1).map(|c| c.name.as_str()).join(" ");
5468        let declared = |c: &SpecCommand| {
5469            let clause = c.clause.as_ref();
5470            c.args
5471                .iter()
5472                .chain(clause.into_iter().flat_map(|cl| &cl.args))
5473                .any(|a| crate::case::snake(&a.name) == "cmd")
5474                || c.flags
5475                    .iter()
5476                    .chain(clause.into_iter().flat_map(|cl| &cl.flags))
5477                    .any(|f| crate::case::snake(&f.name) == "cmd")
5478        };
5479        if !path.is_empty() && !self.cmds.iter().any(declared) {
5480            env.entry("usage_cmd".to_string()).or_insert(path);
5481        }
5482        env
5483    }
5484}
5485
5486impl Display for ParseValue {
5487    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5488        match self {
5489            ParseValue::Bool(b) => write!(f, "{b}"),
5490            ParseValue::String(s) => write!(f, "{s}"),
5491            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5492            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5493        }
5494    }
5495}
5496
5497/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5498fn render_token(token: &TokenBinding) -> String {
5499    let roles = token.roles.iter().map(render_role).join(", ");
5500    let synthesized = if token.synthesized { " (read as)" } else { "" };
5501    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5502}
5503
5504fn render_role(role: &TokenRole) -> String {
5505    match role {
5506        TokenRole::Program => "program".to_string(),
5507        TokenRole::Command { name } => format!("subcommand {name}"),
5508        TokenRole::Flag {
5509            flag,
5510            spelling,
5511            negated,
5512        } => {
5513            let negated = if *negated { ", negated" } else { "" };
5514            format!("flag {} as {spelling}{negated}", flag.name)
5515        }
5516        TokenRole::Value {
5517            flag,
5518            values,
5519            attached,
5520        } => {
5521            let attached = if *attached { ", attached" } else { "" };
5522            format!("value of {} = {values:?}{attached}", flag.name)
5523        }
5524        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5525        TokenRole::Sigil { arg, sigil, values } => {
5526            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5527        }
5528        TokenRole::Separator => "separator".to_string(),
5529        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5530        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5531        TokenRole::Restart => "restart".to_string(),
5532        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5533        TokenRole::UnknownFlag { bound_as } => match bound_as {
5534            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5535            None => "unknown flag".to_string(),
5536        },
5537        TokenRole::Refused { reason } => format!("refused: {reason}"),
5538        TokenRole::External => "external".to_string(),
5539        TokenRole::Unread => "unread".to_string(),
5540    }
5541}
5542
5543impl Debug for ParseOutput {
5544    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5545        f.debug_struct("ParseOutput")
5546            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5547            .field(
5548                "args",
5549                &self
5550                    .args
5551                    .iter()
5552                    .map(|(a, w)| format!("{}: {w}", a.name))
5553                    .collect_vec(),
5554            )
5555            .field("clauses", &self.clauses)
5556            .field(
5557                "available_flags",
5558                &self
5559                    .available_flags
5560                    .iter()
5561                    .map(|(f, w)| format!("{f}: {w}"))
5562                    .collect_vec(),
5563            )
5564            .field(
5565                "flags",
5566                &self
5567                    .flags
5568                    .iter()
5569                    .map(|(f, w)| format!("{}: {w}", f.name))
5570                    .collect_vec(),
5571            )
5572            .field("flag_awaiting_value", &self.flag_awaiting_value)
5573            .field("errors", &self.errors)
5574            .field("external", &self.external)
5575            // Provenance, one line per token and one per fallback. This is the parser's
5576            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5577            // first — `usage explain` renders the same facts for a reader.
5578            .field(
5579                "tokens",
5580                &self.tokens.iter().map(render_token).collect_vec(),
5581            )
5582            .field(
5583                "origins",
5584                &self
5585                    .flag_origins
5586                    .iter()
5587                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5588                    .chain(
5589                        self.arg_origins
5590                            .iter()
5591                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5592                    )
5593                    .collect_vec(),
5594            )
5595            .field("overridden_flags", &self.overridden_flags)
5596            .finish()
5597    }
5598}
5599
5600#[cfg(test)]
5601mod tests {
5602    use super::*;
5603    use crate::SpecFlagAction;
5604
5605    fn input(words: &[&str]) -> Vec<String> {
5606        words.iter().map(|word| (*word).to_string()).collect()
5607    }
5608
5609    #[test]
5610    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5611        // The page has always listed `-V, --version` wherever a `version` is declared,
5612        // and usage-argv and usage-go have always accepted both. This parser called them
5613        // unknown words, so the one implementation the corpus measures the others against
5614        // was the one that disagreed.
5615        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5616            .parse()
5617            .unwrap();
5618
5619        for spelling in ["--version", "-V"] {
5620            let err = parse(&spec, &input(&["ex", spelling]))
5621                .expect_err("answering with a version ends the parse");
5622            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5623        }
5624    }
5625
5626    #[test]
5627    fn the_supplied_version_flag_is_the_roots_alone() {
5628        // `version` describes the program, and the page lists the entry on the program's
5629        // own page only. A subcommand answering with it would be a claim no spec made.
5630        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5631            .parse()
5632            .unwrap();
5633
5634        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5635        assert_eq!(err.to_string(), "unexpected word: --version");
5636    }
5637
5638    #[test]
5639    fn no_declared_version_supplies_nothing() {
5640        // A `--version` answering with nothing is worse than one that is not there, which
5641        // is why the entry is conditional on the page and the spelling on the parse.
5642        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5643
5644        for spelling in ["--version", "-V"] {
5645            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5646            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5647        }
5648    }
5649
5650    #[test]
5651    fn disable_version_flag_removes_the_supplied_spellings() {
5652        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5653            .parse()
5654            .unwrap();
5655
5656        for spelling in ["--version", "-V"] {
5657            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5658            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5659        }
5660    }
5661
5662    #[test]
5663    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5664        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5665        // parse agrees without being told, because a declared flag is matched first.
5666        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5667            .parse()
5668            .unwrap();
5669
5670        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5671        assert_eq!(out.flags.len(), 1);
5672
5673        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5674        assert_eq!(err.to_string(), "1.2.3");
5675    }
5676
5677    #[test]
5678    fn the_supplied_spellings_split_the_two_version_texts() {
5679        // The same split `render_action_err` gives a declared version flag: the long
5680        // spelling prefers `long_version`, the short prefers the concise one.
5681        let spec: Spec =
5682            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5683                .parse()
5684                .unwrap();
5685
5686        assert_eq!(
5687            parse(&spec, &input(&["ex", "--version"]))
5688                .unwrap_err()
5689                .to_string(),
5690            "1.2.3 (abcdef)"
5691        );
5692        assert_eq!(
5693            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5694            "1.2.3"
5695        );
5696    }
5697
5698    #[test]
5699    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5700        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5701        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5702        // naming nothing: usage-argv and usage-go both resolve the letter through the
5703        // same lookup that finds a declared short, and clap prints help for it too.
5704        let spec: Spec =
5705            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5706                .parse()
5707                .unwrap();
5708
5709        for token in ["-vh", "-hv"] {
5710            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5711            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5712        }
5713        for token in ["-vV", "-Vv"] {
5714            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5715            assert_eq!(err.to_string(), "1.2.3", "{token}");
5716        }
5717    }
5718
5719    #[test]
5720    fn a_bundled_help_letter_asks_for_the_short_page() {
5721        // Whatever else shares the token: `-h` is the short spelling, and the letters
5722        // beside it say nothing about which page was asked for.
5723        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"
5724            .parse()
5725            .unwrap();
5726
5727        let short = parse(&spec, &input(&["ex", "-vh"]))
5728            .unwrap_err()
5729            .to_string();
5730        let long = parse(&spec, &input(&["ex", "--help"]))
5731            .unwrap_err()
5732            .to_string();
5733        assert!(short.contains("Be loud"), "{short}");
5734        assert!(!short.contains("at length"), "{short}");
5735        assert!(long.contains("at length"), "{long}");
5736    }
5737
5738    #[test]
5739    fn the_bundled_version_letter_is_the_roots_alone() {
5740        // The same rule the whole-token spelling follows, asked one letter at a time.
5741        let spec: Spec =
5742            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5743                .parse()
5744                .unwrap();
5745
5746        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5747        assert_eq!(err.to_string(), "unexpected word: -vV");
5748    }
5749
5750    #[test]
5751    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5752        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5753        // this spec's own `-h`, taking its value from the rest of the token.
5754        let spec: Spec =
5755            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5756                .parse()
5757                .unwrap();
5758
5759        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5760        assert_eq!(out.flags.len(), 2);
5761        assert!(
5762            out.flags
5763                .iter()
5764                .any(|(flag, value)| flag.name == "host" && value.to_string() == "local")
5765        );
5766    }
5767
5768    #[test]
5769    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5770        let spec: Spec =
5771            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5772                .parse()
5773                .unwrap();
5774
5775        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5776        assert_eq!(err.to_string(), "unexpected word: -vh");
5777    }
5778
5779    #[test]
5780    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5781        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5782        // on the way to discovering that `z` names nothing.
5783        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5784            .parse()
5785            .unwrap();
5786
5787        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5788        assert!(out.flags.is_empty(), "{:?}", out.flags);
5789        assert_eq!(out.args.len(), 1);
5790    }
5791
5792    fn spec_with_arg(arg: SpecArg) -> Spec {
5793        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5794        Spec {
5795            name: "test".to_string(),
5796            bin: "test".to_string(),
5797            cmd,
5798            ..Default::default()
5799        }
5800    }
5801
5802    fn spec_with_flag(flag: SpecFlag) -> Spec {
5803        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5804        Spec {
5805            name: "test".to_string(),
5806            bin: "test".to_string(),
5807            cmd,
5808            ..Default::default()
5809        }
5810    }
5811
5812    fn parse_with_env(
5813        spec: &Spec,
5814        words: &[&str],
5815        env: &[(&str, &str)],
5816    ) -> Result<ParseOutput, miette::Error> {
5817        let env = env
5818            .iter()
5819            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5820            .collect();
5821        Parser::new(spec).with_env(env).parse(&input(words))
5822    }
5823
5824    fn first_string_value(parsed: &ParseOutput) -> &str {
5825        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5826            return value;
5827        }
5828        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5829            return value;
5830        }
5831        panic!("expected first parsed value to be ParseValue::String");
5832    }
5833
5834    #[test]
5835    fn custom_environment_parser_dispatches_executable_views() {
5836        let spec: Spec = r#"
5837bin "ex"
5838view "runner" root="run"
5839cmd "run" {
5840    flag "--token <token>" env="TOKEN"
5841}
5842        "#
5843        .parse()
5844        .unwrap();
5845        let parsed = Parser::new(&spec)
5846            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5847            .parse(&input(&["runner"]))
5848            .unwrap();
5849
5850        assert_eq!(parsed.cmd.name, "runner");
5851        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5852            && matches!(value, ParseValue::String(value) if value == "secret")));
5853    }
5854
5855    #[test]
5856    fn an_executable_view_keeps_the_hosts_version_action() {
5857        let spec: Spec = r#"
5858bin "ex"
5859version "1.2.3"
5860flag "-V --version" action="version"
5861flag "--verbose" global=#true
5862view "runner" root="run" globals=#true
5863cmd "run"
5864        "#
5865        .parse()
5866        .unwrap();
5867
5868        let error = Parser::new(&spec)
5869            .parse(&input(&["runner", "--version"]))
5870            .expect_err("the host version action should answer before view projection");
5871        assert_eq!(error.to_string(), "1.2.3");
5872
5873        let error = Parser::new(&spec)
5874            .parse(&input(&["runner", "--verbose", "--version"]))
5875            .expect_err("the host version action should remain after a carried global");
5876        assert_eq!(error.to_string(), "1.2.3");
5877    }
5878
5879    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5880        let flag = parsed
5881            .flags
5882            .keys()
5883            .find(|flag| flag.name == name)
5884            .unwrap_or_else(|| panic!("expected flag {name}"));
5885        let value = parsed
5886            .flags
5887            .get(flag)
5888            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5889        match value {
5890            ParseValue::String(value) => value,
5891            _ => panic!("expected flag {name} to be ParseValue::String"),
5892        }
5893    }
5894
5895    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5896        let err = result.expect_err("expected parser error");
5897        assert_eq!(format!("{err}"), expected);
5898    }
5899
5900    #[test]
5901    fn a_short_version_action_falls_back_to_the_long_version() {
5902        let flag = SpecFlag::builder()
5903            .short('R')
5904            .action(SpecFlagAction::Version)
5905            .build();
5906        let spec = Spec {
5907            name: "test".to_string(),
5908            bin: "test".to_string(),
5909            long_version: Some("1.2.3\ncommit abc123".to_string()),
5910            ..Default::default()
5911        };
5912        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R", None)
5913        else {
5914            panic!("expected version action")
5915        };
5916        assert_eq!(version, "1.2.3\ncommit abc123");
5917    }
5918
5919    #[cfg(feature = "unstable_choices_env")]
5920    fn spec_arg_choices_env(key: &str) -> Spec {
5921        spec_with_arg(
5922            SpecArg::builder()
5923                .name("env")
5924                .choices_env(key)
5925                .required(false)
5926                .build(),
5927        )
5928    }
5929
5930    #[cfg(feature = "unstable_choices_env")]
5931    fn spec_flag_choices_env(key: &str) -> Spec {
5932        spec_with_flag(
5933            SpecFlag::builder()
5934                .long("env")
5935                .arg(SpecArg::builder().name("env").choices_env(key).build())
5936                .build(),
5937        )
5938    }
5939
5940    #[test]
5941    fn test_parse() {
5942        let cmd = SpecCommand::builder()
5943            .name("test")
5944            .arg(SpecArg::builder().name("arg").build())
5945            .flag(SpecFlag::builder().long("flag").build())
5946            .build();
5947        let spec = Spec {
5948            name: "test".to_string(),
5949            bin: "test".to_string(),
5950            cmd,
5951            ..Default::default()
5952        };
5953        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5954        let parsed = parse(&spec, &input).unwrap();
5955        assert_eq!(parsed.cmds.len(), 1);
5956        assert_eq!(parsed.cmds[0].name, "test");
5957        assert_eq!(parsed.args.len(), 1);
5958        assert_eq!(parsed.flags.len(), 1);
5959        assert_eq!(parsed.available_flags.len(), 1);
5960    }
5961
5962    #[test]
5963    fn test_flag_overrides_last_occurrence_wins() {
5964        let spec: Spec = r#"
5965flag "--stdin" default=#true
5966flag "--file <file>" overrides="--stdin"
5967        "#
5968        .parse()
5969        .unwrap();
5970
5971        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5972        assert_eq!(file_wins.flags.len(), 1);
5973        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5974        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5975
5976        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5977        assert_eq!(stdin_wins.flags.len(), 1);
5978        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5979        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5980    }
5981
5982    #[test]
5983    fn test_flag_override_clears_pending_value() {
5984        let spec: Spec = r#"
5985flag "--file <file>" overrides="--stdin"
5986flag "--stdin"
5987arg "[input]"
5988        "#
5989        .parse()
5990        .unwrap();
5991
5992        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5993        assert_eq!(parsed.flags.len(), 1);
5994        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5995        assert_eq!(first_string_value(&parsed), "input.txt");
5996    }
5997
5998    #[cfg(unix)]
5999    #[test]
6000    fn a_mount_on_the_root_discovers_subcommands() {
6001        // The root is a command like any other, so it can find its own subcommands
6002        // by running something. Uses `echo` rather than a fixture because resolving
6003        // a mount is what is being tested.
6004        let spec: Spec = r#"
6005name "ex"
6006bin "ex"
6007cmd "declared"
6008mount run="echo 'cmd \"discovered\"'"
6009"#
6010        .parse()
6011        .unwrap();
6012
6013        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6014        assert_eq!(out.cmd.name, "discovered");
6015    }
6016
6017    #[test]
6018    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
6019        let spec: Spec = r#"
6020name "ex"
6021bin "ex"
6022mount run="this command must never run"
6023cmd "declared"
6024"#
6025        .parse()
6026        .unwrap();
6027
6028        Parser::new(&spec)
6029            .with_mount_outputs(HashMap::new())
6030            .parse(&input(&["ex", "declared"]))
6031            .expect("a declared command does not resolve the mount");
6032
6033        let error = Parser::new(&spec)
6034            .with_mount_outputs(HashMap::new())
6035            .parse(&input(&["ex", "discovered"]))
6036            .unwrap_err();
6037        assert!(
6038            error
6039                .to_string()
6040                .contains("No injected output was provided for mount command"),
6041            "{error}"
6042        );
6043    }
6044
6045    #[cfg(unix)]
6046    #[test]
6047    fn completion_sees_root_mounted_commands_with_nothing_typed() {
6048        // The case a root mount exists for. `mycli <tab>` has no word to trigger
6049        // discovery with, so a completion has to resolve up front or the mounted
6050        // commands are never offered.
6051        let spec: Spec = r#"
6052name "ex"
6053bin "ex"
6054cmd "declared"
6055mount run="echo 'cmd \"discovered\"'"
6056"#
6057        .parse()
6058        .unwrap();
6059
6060        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6061        assert!(
6062            out.cmd.subcommands.contains_key("discovered"),
6063            "a completion should see mounted commands; got {:?}",
6064            out.cmd.subcommands.keys().collect::<Vec<_>>()
6065        );
6066    }
6067
6068    #[cfg(unix)]
6069    #[test]
6070    fn completion_and_execution_agree_about_discovery() {
6071        // Offering a command that a real parse would hand to the default instead is
6072        // worse than not offering it, so the gate applies to both paths. The mount
6073        // fails if it runs, which is how both halves are checked at once.
6074        let spec: Spec = r#"
6075name "ex"
6076bin "ex"
6077default_subcommand "run"
6078cmd "run" {
6079  arg "<task>"
6080}
6081mount run="exit 1"
6082"#
6083        .parse()
6084        .unwrap();
6085
6086        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6087        assert!(
6088            !out.cmd.subcommands.contains_key("discovered"),
6089            "a completion must not offer what execution will not route"
6090        );
6091
6092        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6093        assert_eq!(out.cmd.name, "run");
6094    }
6095
6096    #[cfg(unix)]
6097    #[test]
6098    fn a_default_subcommand_outranks_discovery() {
6099        // The default already says what an unmatched word means, and says it for
6100        // free. The mount fails if it runs, so parsing proves discovery was skipped.
6101        let spec: Spec = r#"
6102name "ex"
6103bin "ex"
6104default_subcommand "run"
6105cmd "run" {
6106  arg "<task>"
6107}
6108mount run="exit 1"
6109"#
6110        .parse()
6111        .unwrap();
6112
6113        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6114        assert_eq!(out.cmd.name, "run");
6115    }
6116
6117    #[cfg(unix)]
6118    #[test]
6119    fn a_mount_may_ask_to_outrank_the_default() {
6120        // Opting in, and paying for it: discovery runs first, so a discovered
6121        // command wins over the fallback.
6122        let spec: Spec = r#"
6123name "ex"
6124bin "ex"
6125default_subcommand "run"
6126cmd "run" {
6127  arg "<task>"
6128}
6129mount run="echo 'cmd \"discovered\"'" overrides_default=#true
6130"#
6131        .parse()
6132        .unwrap();
6133
6134        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6135        assert_eq!(out.cmd.name, "discovered");
6136
6137        // A word it does not know still reaches the default.
6138        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6139        assert_eq!(out.cmd.name, "run");
6140    }
6141
6142    #[cfg(unix)]
6143    #[test]
6144    fn a_flag_does_not_run_the_mount() {
6145        // A flag matches no subcommand, which would have been enough to trigger
6146        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
6147        // so parsing at all is the proof that it did not.
6148        let spec: Spec = r#"
6149name "ex"
6150bin "ex"
6151flag "--verbose"
6152cmd "declared"
6153mount run="exit 1"
6154"#
6155        .parse()
6156        .unwrap();
6157
6158        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6159        assert_eq!(out.cmd.name, "ex");
6160    }
6161
6162    #[cfg(unix)]
6163    #[test]
6164    fn a_declared_subcommand_does_not_run_the_mount() {
6165        // The mount would fail if it ran, so this parsing at all is the proof that
6166        // discovery is skipped when the word is already known. Worth pinning: a root
6167        // mount that resolved eagerly would spawn a process on every invocation.
6168        let spec: Spec = r#"
6169name "ex"
6170bin "ex"
6171cmd "declared"
6172mount run="exit 1"
6173"#
6174        .parse()
6175        .unwrap();
6176
6177        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6178        assert_eq!(out.cmd.name, "declared");
6179    }
6180
6181    #[test]
6182    fn a_root_mount_survives_being_written_out() {
6183        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6184            .parse()
6185            .unwrap();
6186        assert_eq!(spec.cmd.mounts.len(), 1);
6187
6188        let reparsed: Spec = spec.to_string().parse().unwrap();
6189        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6190        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6191    }
6192
6193    #[test]
6194    fn test_mount_prefix_applies_flag_overrides() {
6195        let stdin = Arc::new(
6196            SpecFlag::builder()
6197                .name("stdin")
6198                .long("stdin")
6199                .global(true)
6200                .build(),
6201        );
6202        let file = Arc::new(
6203            SpecFlag::builder()
6204                .name("file")
6205                .long("file")
6206                .arg(SpecArg::builder().name("file").build())
6207                .global(true)
6208                .overrides_with(vec!["--stdin".to_string()])
6209                .build(),
6210        );
6211        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6212
6213        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6214        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6215
6216        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6217    }
6218
6219    #[test]
6220    fn test_flag_override_suppresses_env_value() {
6221        let spec: Spec = r#"
6222flag "--stdin" env="USE_STDIN"
6223flag "--file <file>" overrides="--stdin"
6224        "#
6225        .parse()
6226        .unwrap();
6227
6228        let parsed = parse_with_env(
6229            &spec,
6230            &["test", "--file", "input.txt"],
6231            &[("USE_STDIN", "true")],
6232        )
6233        .unwrap();
6234        assert_eq!(parsed.flags.len(), 1);
6235        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6236    }
6237
6238    #[test]
6239    fn test_flag_override_suppresses_required_check() {
6240        let spec: Spec = r#"
6241flag "--stdin" required=#true
6242flag "--file <file>" overrides="--stdin"
6243        "#
6244        .parse()
6245        .unwrap();
6246
6247        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6248        assert_eq!(parsed.flags.len(), 1);
6249        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6250    }
6251
6252    #[test]
6253    fn test_flag_required_if() {
6254        let spec: Spec = r#"
6255flag "--dir <dir>"
6256flag "--file <file>" required_if="--dir"
6257        "#
6258        .parse()
6259        .unwrap();
6260
6261        parse(&spec, &input(&["test"])).unwrap();
6262        assert_parse_err(
6263            parse(&spec, &input(&["test", "--dir", "src"])),
6264            "Missing required flag: --file <file>",
6265        );
6266        parse(
6267            &spec,
6268            &input(&["test", "--dir", "src", "--file", "input.txt"]),
6269        )
6270        .unwrap();
6271    }
6272
6273    #[test]
6274    fn test_flag_required_unless() {
6275        let spec: Spec = r#"
6276flag "--stdin"
6277flag "--file <file>" required_unless="--stdin"
6278        "#
6279        .parse()
6280        .unwrap();
6281
6282        assert_parse_err(
6283            parse(&spec, &input(&["test"])),
6284            "Missing required flag: --file <file>",
6285        );
6286        parse(&spec, &input(&["test", "--stdin"])).unwrap();
6287        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6288    }
6289
6290    #[test]
6291    fn complete_required_relationship_truth_tables() {
6292        let spec: Spec = r#"
6293name "ex"
6294bin "ex"
6295flag "--mode <mode>"
6296flag "--scope <scope>"
6297flag "--token <token>" {
6298    required_if_eq "--mode" "remote"
6299}
6300flag "--approval <approval>" {
6301    required_if_eq_all "--mode" "remote" "--scope" "global"
6302}
6303flag "--input <input>" {
6304    required_unless "--stdin" "--file"
6305}
6306flag "--checksum <checksum>" {
6307    required_unless_all "--stdin" "--file"
6308}
6309flag "--stdin"
6310flag "--file <file>"
6311arg "[request]" {
6312    requires "--mode" "--scope"
6313}
6314"#
6315        .parse()
6316        .unwrap();
6317        let parse_args = |args: &[&str]| {
6318            parse(
6319                &spec,
6320                &args
6321                    .iter()
6322                    .map(|arg| (*arg).to_string())
6323                    .collect::<Vec<_>>(),
6324            )
6325        };
6326
6327        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6328        assert!(
6329            parse_args(&[
6330                "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6331            ])
6332            .is_err()
6333        );
6334        parse_args(&[
6335            "ex",
6336            "--mode",
6337            "remote",
6338            "--token",
6339            "secret",
6340            "--scope",
6341            "global",
6342            "--approval",
6343            "yes",
6344            "--stdin",
6345            "--file",
6346            "in",
6347        ])
6348        .unwrap();
6349        parse_args(&[
6350            "ex",
6351            "--mode",
6352            "local",
6353            "--scope",
6354            "project",
6355            "--stdin",
6356            "--checksum",
6357            "sum",
6358            "request.json",
6359        ])
6360        .unwrap();
6361
6362        let reparsed: Spec = spec.to_string().parse().unwrap();
6363        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6364        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6365        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6366        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6367    }
6368
6369    #[test]
6370    fn test_conditional_requirements_treat_env_as_explicit() {
6371        let spec: Spec = r#"
6372flag "--dir <dir>" env="INPUT_DIR"
6373flag "--stdin" env="USE_STDIN"
6374flag "--file <file>" required_if="--dir" required_unless="--stdin"
6375        "#
6376        .parse()
6377        .unwrap();
6378
6379        assert_parse_err(
6380            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6381            "Missing required flag: --file <file>",
6382        );
6383        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6384    }
6385
6386    #[test]
6387    fn test_custom_env_does_not_fall_back_to_process_env() {
6388        assert!(std::env::var("PATH").is_ok());
6389        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6390
6391        assert_parse_err(
6392            parse_with_env(&spec, &["test"], &[]),
6393            "Missing required flag: --file <file>",
6394        );
6395    }
6396
6397    #[test]
6398    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6399        let spec: Spec = r#"
6400flag "--dir <dir>" default="src"
6401flag "--file <file>" required_if="--dir"
6402        "#
6403        .parse()
6404        .unwrap();
6405
6406        parse(&spec, &input(&["test"])).unwrap();
6407    }
6408
6409    #[test]
6410    fn test_conditional_requirements_see_overridden_flags_as_absent() {
6411        let spec: Spec = r#"
6412flag "--stdin"
6413flag "--dir <dir>" overrides="--stdin"
6414flag "--file <file>" required_unless="--stdin"
6415        "#
6416        .parse()
6417        .unwrap();
6418
6419        assert_parse_err(
6420            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6421            "Missing required flag: --file <file>",
6422        );
6423    }
6424
6425    #[test]
6426    fn short_flag_is_one_character_not_one_byte() {
6427        // A short is declared and read by character. Counting bytes instead either
6428        // refuses the declaration or slices the token inside the character, and clap
6429        // — which many specs are generated from — accepts shorts like this one.
6430        let spec = spec_with_flag(
6431            SpecFlag::builder()
6432                .short('磨')
6433                .long("polish")
6434                .arg(SpecArg::builder().name("opt").build())
6435                .build(),
6436        );
6437        let attached = Parser::new(&spec)
6438            .parse(&input(&["test", "-磨VALUE"]))
6439            .unwrap();
6440        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6441        let detached = Parser::new(&spec)
6442            .parse(&input(&["test", "-磨", "V"]))
6443            .unwrap();
6444        assert_eq!(flag_string_value(&detached, "polish"), "V");
6445    }
6446
6447    #[test]
6448    fn test_as_env() {
6449        let cmd = SpecCommand::builder()
6450            .name("test")
6451            .arg(SpecArg::builder().name("arg").build())
6452            .flag(SpecFlag::builder().long("flag").build())
6453            .flag(
6454                SpecFlag::builder()
6455                    .long("force")
6456                    .negate("--no-force")
6457                    .build(),
6458            )
6459            .build();
6460        let spec = Spec {
6461            name: "test".to_string(),
6462            bin: "test".to_string(),
6463            cmd,
6464            ..Default::default()
6465        };
6466        let input = vec![
6467            "test".to_string(),
6468            "--flag".to_string(),
6469            "--no-force".to_string(),
6470        ];
6471        let parsed = parse(&spec, &input).unwrap();
6472        let env = parsed.as_env();
6473        assert_eq!(env.len(), 2);
6474        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6475        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6476    }
6477
6478    #[test]
6479    fn as_env_names_the_subcommand_path() {
6480        let spec: Spec = r#"
6481            flag "-v --verbose"
6482            cmd "db" {
6483                cmd "migrate" {
6484                    alias "m"
6485                    arg "[target]"
6486                }
6487            }
6488            "#
6489        .parse()
6490        .unwrap();
6491        let env = |args: &[&str]| {
6492            let input: Vec<String> = std::iter::once("test")
6493                .chain(args.iter().copied())
6494                .map(String::from)
6495                .collect();
6496            parse(&spec, &input).unwrap().as_env()
6497        };
6498
6499        // The canonical name, not the alias that was typed.
6500        let nested = env(&["db", "m", "v2"]);
6501        assert_eq!(
6502            nested.get("usage_cmd").map(String::as_str),
6503            Some("db migrate")
6504        );
6505        assert_eq!(nested.get("usage_target").map(String::as_str), Some("v2"));
6506        assert_eq!(
6507            env(&["db"]).get("usage_cmd").map(String::as_str),
6508            Some("db")
6509        );
6510        assert!(!env(&["-v"]).contains_key("usage_cmd"));
6511    }
6512
6513    #[test]
6514    fn as_env_leaves_usage_cmd_to_a_declared_cmd() {
6515        // Declared on the chosen command, and supplied: the value is the arg's.
6516        let spec: Spec = r#"
6517            cmd "run" {
6518                arg "[cmd]"
6519            }
6520            "#
6521        .parse()
6522        .unwrap();
6523        let input = ["test", "run", "build"].map(String::from);
6524        let env = parse(&spec, &input).unwrap().as_env();
6525        assert_eq!(env.get("usage_cmd").map(String::as_str), Some("build"));
6526        // Declared but left out: still the arg's, so unset rather than the path.
6527        let input = ["test", "run"].map(String::from);
6528        assert!(
6529            !parse(&spec, &input)
6530                .unwrap()
6531                .as_env()
6532                .contains_key("usage_cmd")
6533        );
6534
6535        // Declared as a global flag on an ancestor.
6536        let spec: Spec = r#"
6537            flag "--cmd <cmd>" global=#true
6538            cmd "run"
6539            "#
6540        .parse()
6541        .unwrap();
6542        let input = ["test", "run"].map(String::from);
6543        assert!(
6544            !parse(&spec, &input)
6545                .unwrap()
6546                .as_env()
6547                .contains_key("usage_cmd")
6548        );
6549
6550        // Declared inside a clause, and the clause left out.
6551        let spec: Spec = r#"
6552            cmd "run" {
6553                clause "steps" separator=":::" {
6554                    arg "[cmd]"
6555                }
6556            }
6557            "#
6558        .parse()
6559        .unwrap();
6560        let input = ["test", "run"].map(String::from);
6561        assert!(
6562            !parse(&spec, &input)
6563                .unwrap()
6564                .as_env()
6565                .contains_key("usage_cmd")
6566        );
6567    }
6568
6569    #[test]
6570    fn test_arg_env_var() {
6571        let cmd = SpecCommand::builder()
6572            .name("test")
6573            .arg(
6574                SpecArg::builder()
6575                    .name("input")
6576                    .env("TEST_ARG_INPUT")
6577                    .required(true)
6578                    .build(),
6579            )
6580            .build();
6581        let spec = Spec {
6582            name: "test".to_string(),
6583            bin: "test".to_string(),
6584            cmd,
6585            ..Default::default()
6586        };
6587
6588        // Set env var
6589        // FIXME: Audit that the environment access only happens in single-threaded code.
6590        unsafe { std::env::set_var("TEST_ARG_INPUT", "test_file.txt") };
6591
6592        let input = vec!["test".to_string()];
6593        let parsed = parse(&spec, &input).unwrap();
6594
6595        assert_eq!(parsed.args.len(), 1);
6596        let arg = parsed.args.keys().next().unwrap();
6597        assert_eq!(arg.name, "input");
6598        let value = parsed.args.values().next().unwrap();
6599        assert_eq!(value.to_string(), "test_file.txt");
6600
6601        // Clean up
6602        // FIXME: Audit that the environment access only happens in single-threaded code.
6603        unsafe { std::env::remove_var("TEST_ARG_INPUT") };
6604    }
6605
6606    #[test]
6607    fn test_flag_env_var_with_arg() {
6608        let cmd = SpecCommand::builder()
6609            .name("test")
6610            .flag(
6611                SpecFlag::builder()
6612                    .long("output")
6613                    .env("TEST_FLAG_OUTPUT")
6614                    .arg(SpecArg::builder().name("file").build())
6615                    .build(),
6616            )
6617            .build();
6618        let spec = Spec {
6619            name: "test".to_string(),
6620            bin: "test".to_string(),
6621            cmd,
6622            ..Default::default()
6623        };
6624
6625        // Set env var
6626        // FIXME: Audit that the environment access only happens in single-threaded code.
6627        unsafe { std::env::set_var("TEST_FLAG_OUTPUT", "output.txt") };
6628
6629        let input = vec!["test".to_string()];
6630        let parsed = parse(&spec, &input).unwrap();
6631
6632        assert_eq!(parsed.flags.len(), 1);
6633        let flag = parsed.flags.keys().next().unwrap();
6634        assert_eq!(flag.name, "output");
6635        let value = parsed.flags.values().next().unwrap();
6636        assert_eq!(value.to_string(), "output.txt");
6637
6638        // Clean up
6639        // FIXME: Audit that the environment access only happens in single-threaded code.
6640        unsafe { std::env::remove_var("TEST_FLAG_OUTPUT") };
6641    }
6642
6643    #[test]
6644    fn test_flag_env_var_boolean() {
6645        let cmd = SpecCommand::builder()
6646            .name("test")
6647            .flag(
6648                SpecFlag::builder()
6649                    .long("verbose")
6650                    .env("TEST_FLAG_VERBOSE")
6651                    .build(),
6652            )
6653            .build();
6654        let spec = Spec {
6655            name: "test".to_string(),
6656            bin: "test".to_string(),
6657            cmd,
6658            ..Default::default()
6659        };
6660
6661        // Set env var to true
6662        // FIXME: Audit that the environment access only happens in single-threaded code.
6663        unsafe { std::env::set_var("TEST_FLAG_VERBOSE", "true") };
6664
6665        let input = vec!["test".to_string()];
6666        let parsed = parse(&spec, &input).unwrap();
6667
6668        assert_eq!(parsed.flags.len(), 1);
6669        let flag = parsed.flags.keys().next().unwrap();
6670        assert_eq!(flag.name, "verbose");
6671        let value = parsed.flags.values().next().unwrap();
6672        assert_eq!(value.to_string(), "true");
6673
6674        // Clean up
6675        // FIXME: Audit that the environment access only happens in single-threaded code.
6676        unsafe { std::env::remove_var("TEST_FLAG_VERBOSE") };
6677    }
6678
6679    #[test]
6680    fn test_env_var_precedence() {
6681        // CLI args should take precedence over env vars
6682        let cmd = SpecCommand::builder()
6683            .name("test")
6684            .arg(
6685                SpecArg::builder()
6686                    .name("input")
6687                    .env("TEST_PRECEDENCE_INPUT")
6688                    .required(true)
6689                    .build(),
6690            )
6691            .build();
6692        let spec = Spec {
6693            name: "test".to_string(),
6694            bin: "test".to_string(),
6695            cmd,
6696            ..Default::default()
6697        };
6698
6699        // Set env var
6700        // FIXME: Audit that the environment access only happens in single-threaded code.
6701        unsafe { std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt") };
6702
6703        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6704        let parsed = parse(&spec, &input).unwrap();
6705
6706        assert_eq!(parsed.args.len(), 1);
6707        let value = parsed.args.values().next().unwrap();
6708        // CLI arg should take precedence
6709        assert_eq!(value.to_string(), "cli_file.txt");
6710
6711        // Clean up
6712        // FIXME: Audit that the environment access only happens in single-threaded code.
6713        unsafe { std::env::remove_var("TEST_PRECEDENCE_INPUT") };
6714    }
6715
6716    #[test]
6717    fn test_flag_var_true_with_single_default() {
6718        // When var=true and default="bar", the default should be MultiString(["bar"])
6719        let cmd = SpecCommand::builder()
6720            .name("test")
6721            .flag(
6722                SpecFlag::builder()
6723                    .long("foo")
6724                    .var(true)
6725                    .arg(SpecArg::builder().name("foo").build())
6726                    .default_value("bar")
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        // User doesn't provide the flag
6738        let input = vec!["test".to_string()];
6739        let parsed = parse(&spec, &input).unwrap();
6740
6741        assert_eq!(parsed.flags.len(), 1);
6742        let flag = parsed.flags.keys().next().unwrap();
6743        assert_eq!(flag.name, "foo");
6744        let value = parsed.flags.values().next().unwrap();
6745        // Should be MultiString, not String
6746        match value {
6747            ParseValue::MultiString(v) => {
6748                assert_eq!(v.len(), 1);
6749                assert_eq!(v[0], "bar");
6750            }
6751            _ => panic!("Expected MultiString, got {:?}", value),
6752        }
6753    }
6754
6755    #[test]
6756    fn test_flag_var_true_with_multiple_defaults() {
6757        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6758        let cmd = SpecCommand::builder()
6759            .name("test")
6760            .flag(
6761                SpecFlag::builder()
6762                    .long("foo")
6763                    .var(true)
6764                    .arg(SpecArg::builder().name("foo").build())
6765                    .default_values(["xyz", "bar"])
6766                    .build(),
6767            )
6768            .build();
6769        let spec = Spec {
6770            name: "test".to_string(),
6771            bin: "test".to_string(),
6772            cmd,
6773            ..Default::default()
6774        };
6775
6776        // User doesn't provide the flag
6777        let input = vec!["test".to_string()];
6778        let parsed = parse(&spec, &input).unwrap();
6779
6780        assert_eq!(parsed.flags.len(), 1);
6781        let value = parsed.flags.values().next().unwrap();
6782        // Should be MultiString with both values
6783        match value {
6784            ParseValue::MultiString(v) => {
6785                assert_eq!(v.len(), 2);
6786                assert_eq!(v[0], "xyz");
6787                assert_eq!(v[1], "bar");
6788            }
6789            _ => panic!("Expected MultiString, got {:?}", value),
6790        }
6791    }
6792
6793    #[test]
6794    fn test_flag_var_false_with_default_remains_string() {
6795        // When var=false (default), the default should still be String("bar")
6796        let cmd = SpecCommand::builder()
6797            .name("test")
6798            .flag(
6799                SpecFlag::builder()
6800                    .long("foo")
6801                    .var(false) // Default behavior
6802                    .arg(SpecArg::builder().name("foo").build())
6803                    .default_value("bar")
6804                    .build(),
6805            )
6806            .build();
6807        let spec = Spec {
6808            name: "test".to_string(),
6809            bin: "test".to_string(),
6810            cmd,
6811            ..Default::default()
6812        };
6813
6814        // User doesn't provide the flag
6815        let input = vec!["test".to_string()];
6816        let parsed = parse(&spec, &input).unwrap();
6817
6818        assert_eq!(parsed.flags.len(), 1);
6819        let value = parsed.flags.values().next().unwrap();
6820        // Should be String, not MultiString
6821        match value {
6822            ParseValue::String(s) => {
6823                assert_eq!(s, "bar");
6824            }
6825            _ => panic!("Expected String, got {:?}", value),
6826        }
6827    }
6828
6829    #[test]
6830    fn test_arg_var_true_with_single_default() {
6831        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6832        let cmd = SpecCommand::builder()
6833            .name("test")
6834            .arg(
6835                SpecArg::builder()
6836                    .name("files")
6837                    .var(true)
6838                    .default_value("default.txt")
6839                    .required(false)
6840                    .build(),
6841            )
6842            .build();
6843        let spec = Spec {
6844            name: "test".to_string(),
6845            bin: "test".to_string(),
6846            cmd,
6847            ..Default::default()
6848        };
6849
6850        // User doesn't provide the arg
6851        let input = vec!["test".to_string()];
6852        let parsed = parse(&spec, &input).unwrap();
6853
6854        assert_eq!(parsed.args.len(), 1);
6855        let value = parsed.args.values().next().unwrap();
6856        // Should be MultiString, not String
6857        match value {
6858            ParseValue::MultiString(v) => {
6859                assert_eq!(v.len(), 1);
6860                assert_eq!(v[0], "default.txt");
6861            }
6862            _ => panic!("Expected MultiString, got {:?}", value),
6863        }
6864    }
6865
6866    #[test]
6867    fn test_arg_var_true_with_multiple_defaults() {
6868        // When arg has var=true and multiple defaults
6869        let cmd = SpecCommand::builder()
6870            .name("test")
6871            .arg(
6872                SpecArg::builder()
6873                    .name("files")
6874                    .var(true)
6875                    .default_values(["file1.txt", "file2.txt"])
6876                    .required(false)
6877                    .build(),
6878            )
6879            .build();
6880        let spec = Spec {
6881            name: "test".to_string(),
6882            bin: "test".to_string(),
6883            cmd,
6884            ..Default::default()
6885        };
6886
6887        // User doesn't provide the arg
6888        let input = vec!["test".to_string()];
6889        let parsed = parse(&spec, &input).unwrap();
6890
6891        assert_eq!(parsed.args.len(), 1);
6892        let value = parsed.args.values().next().unwrap();
6893        // Should be MultiString with both values
6894        match value {
6895            ParseValue::MultiString(v) => {
6896                assert_eq!(v.len(), 2);
6897                assert_eq!(v[0], "file1.txt");
6898                assert_eq!(v[1], "file2.txt");
6899            }
6900            _ => panic!("Expected MultiString, got {:?}", value),
6901        }
6902    }
6903
6904    #[test]
6905    fn test_arg_var_false_with_default_remains_string() {
6906        // When arg has var=false (default), the default should still be String
6907        let cmd = SpecCommand::builder()
6908            .name("test")
6909            .arg(
6910                SpecArg::builder()
6911                    .name("file")
6912                    .var(false)
6913                    .default_value("default.txt")
6914                    .required(false)
6915                    .build(),
6916            )
6917            .build();
6918        let spec = Spec {
6919            name: "test".to_string(),
6920            bin: "test".to_string(),
6921            cmd,
6922            ..Default::default()
6923        };
6924
6925        // User doesn't provide the arg
6926        let input = vec!["test".to_string()];
6927        let parsed = parse(&spec, &input).unwrap();
6928
6929        assert_eq!(parsed.args.len(), 1);
6930        let value = parsed.args.values().next().unwrap();
6931        // Should be String, not MultiString
6932        match value {
6933            ParseValue::String(s) => {
6934                assert_eq!(s, "default.txt");
6935            }
6936            _ => panic!("Expected String, got {:?}", value),
6937        }
6938    }
6939
6940    #[test]
6941    fn test_scalar_defaults_validate_only_first_default_choice() {
6942        let specs = [
6943            spec_with_arg(
6944                SpecArg::builder()
6945                    .name("env")
6946                    .var(false)
6947                    .default_values(["dev", "prod"])
6948                    .choices(["dev"])
6949                    .required(false)
6950                    .build(),
6951            ),
6952            spec_with_flag(
6953                SpecFlag::builder()
6954                    .long("env")
6955                    .arg(
6956                        SpecArg::builder()
6957                            .name("env")
6958                            .default_values(["dev", "prod"])
6959                            .choices(["dev"])
6960                            .build(),
6961                    )
6962                    .build(),
6963            ),
6964        ];
6965
6966        for spec in specs {
6967            let parsed = parse(&spec, &input(&["test"])).unwrap();
6968            assert_eq!(first_string_value(&parsed), "dev");
6969        }
6970    }
6971
6972    #[test]
6973    fn a_delimiter_turns_one_word_into_several_values() {
6974        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6975            .parse()
6976            .unwrap();
6977
6978        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6979        let multi = |value: &ParseValue| match value {
6980            ParseValue::MultiString(values) => values.clone(),
6981            other => panic!("expected several values, got {other:?}"),
6982        };
6983        let tags = parsed
6984            .flags
6985            .iter()
6986            .find(|(f, _)| f.name == "tags")
6987            .map(|(_, v)| v)
6988            .unwrap();
6989        assert_eq!(multi(tags), vec!["a", "b", "c"]);
6990        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6991    }
6992
6993    #[test]
6994    fn a_positional_splits_before_its_choices_are_asked() {
6995        // The flag path did this and the positional path did not, so a word whose parts
6996        // were all choices was rejected as one value, and a bad half was reported as the
6997        // whole word.
6998        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
6999            .parse()
7000            .unwrap();
7001
7002        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
7003
7004        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
7005        let message = err.to_string();
7006        assert!(message.contains("nowhere"), "{message}");
7007        assert!(
7008            !message.contains("src:nowhere"),
7009            "the bad half should be named, not the whole word: {message}"
7010        );
7011    }
7012
7013    #[test]
7014    fn a_split_value_is_counted_and_judged_as_values() {
7015        // Split during the parse rather than after it, so everything downstream sees the
7016        // values the user meant rather than the words they typed: `choices` judges each
7017        // one, and the bounds count them.
7018        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
7019            .parse()
7020            .unwrap();
7021
7022        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
7023        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
7024        assert!(err.to_string().contains("staging"), "{err}");
7025        assert!(
7026            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
7027            "three values should breach var_max=2"
7028        );
7029    }
7030
7031    #[test]
7032    fn a_split_bound_counts_one_occurrence_at_a_time() {
7033        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
7034        // delimiter the collection simply stops at it, so it could never be exceeded; a word
7035        // carrying several values can carry an occurrence past it in one step, and that is
7036        // the only way this bound is ever breached.
7037        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
7038            .parse()
7039            .unwrap();
7040
7041        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
7042        assert!(
7043            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
7044            "three values out of one word is still three values"
7045        );
7046        // The rule the corpus documents for plain words, on split ones: a second occurrence
7047        // starts counting again rather than adding to the first.
7048        parse(
7049            &spec,
7050            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
7051        )
7052        .expect("two per occurrence, twice, is within the bound");
7053    }
7054
7055    #[test]
7056    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
7057        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
7058            .parse()
7059            .unwrap();
7060
7061        parse(
7062            &spec,
7063            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
7064        )
7065        .expect("each occurrence satisfies the bound independently");
7066
7067        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
7068        assert!(
7069            error
7070                .to_string()
7071                .contains("requires at least 2 value(s), got 1"),
7072            "{error:?}"
7073        );
7074    }
7075
7076    #[test]
7077    fn an_exclusive_flag_has_to_be_alone() {
7078        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
7079            .parse()
7080            .unwrap();
7081
7082        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
7083
7084        // Any other flag.
7085        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
7086        assert!(err.to_string().contains("on its own"), "{err}");
7087
7088        // And a positional, which is what makes this more than a conflict with every
7089        // other flag.
7090        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
7091        assert!(err.to_string().contains("on its own"), "{err}");
7092
7093        // Not given, so it imposes nothing.
7094        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
7095    }
7096
7097    #[test]
7098    fn an_exclusive_flag_conflicts_with_clause_arguments() {
7099        let spec: Spec = r#"name "ex"
7100bin "ex"
7101flag "--dump" exclusive=#true
7102clause "tasks" separator=":::" {
7103  arg "<task>"
7104}
7105"#
7106        .parse()
7107        .unwrap();
7108
7109        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
7110        assert!(err.to_string().contains("on its own"), "{err}");
7111    }
7112
7113    #[test]
7114    fn a_partial_parse_reports_the_next_implicit_clause_argument() {
7115        let spec: Spec = r#"name "ex"
7116bin "ex"
7117clause "tools" {
7118  arg "<tool@version>"
7119}
7120"#
7121        .parse()
7122        .unwrap();
7123
7124        let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
7125
7126        assert_eq!(
7127            parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
7128            Some("tool@version")
7129        );
7130    }
7131
7132    #[test]
7133    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
7134        // Only what was supplied counts, as `conflicts` reads it. A default counting as
7135        // company would make an exclusive flag unusable on any command that has one.
7136        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
7137            .parse()
7138            .unwrap();
7139
7140        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
7141        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
7142    }
7143
7144    #[test]
7145    fn an_exclusive_flag_bypasses_required_siblings() {
7146        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
7147            .parse()
7148            .unwrap();
7149
7150        parse(&spec, &input(&["ex", "--dump"]))
7151            .expect("exclusive is the command's requiredness escape");
7152        assert!(
7153            parse(
7154                &spec,
7155                &input(&["ex", "--dump", "--out", "somewhere", "target"])
7156            )
7157            .is_err()
7158        );
7159    }
7160
7161    #[test]
7162    fn an_environment_value_counts_for_an_exclusive_flag() {
7163        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
7164            .parse()
7165            .unwrap();
7166
7167        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
7168        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
7169    }
7170
7171    #[test]
7172    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
7173        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
7174            .parse()
7175            .unwrap();
7176
7177        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
7178        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
7179    }
7180
7181    #[test]
7182    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
7183        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7184            .parse()
7185            .unwrap();
7186
7187        parse(&spec, &input(&["ex", "run", "--clean"]))
7188            .expect("the child flag is alone within the child command");
7189        assert!(
7190            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7191            "the parent flag still conflicts with selecting the child"
7192        );
7193    }
7194
7195    #[test]
7196    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
7197        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7198            .parse()
7199            .unwrap();
7200
7201        parse(&spec, &input(&["ex", "run", "--clean"]))
7202            .expect("the child-local exclusive flag is alone inside the child command");
7203        assert!(
7204            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7205            "the ancestor spelling still conflicts with selecting the child"
7206        );
7207    }
7208
7209    #[test]
7210    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
7211        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
7212            .parse()
7213            .unwrap();
7214
7215        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
7216        assert!(
7217            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
7218            "the distinct parent declaration is still company despite sharing a name"
7219        );
7220    }
7221
7222    #[test]
7223    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
7224        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7225            .parse()
7226            .unwrap();
7227
7228        parse(&spec, &input(&["ex", "run", "--clean"]))
7229            .expect("the child exclusive flag is valid alone");
7230        assert!(
7231            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7232            "merging with the inherited global must not discard child exclusivity"
7233        );
7234    }
7235
7236    #[test]
7237    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
7238        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7239            .parse()
7240            .unwrap();
7241
7242        parse(&spec, &input(&["ex", "run", "--clean"]))
7243            .expect("the typed long form belongs to the child declaration");
7244        assert!(
7245            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7246            "the inherited short form still belongs to the ancestor"
7247        );
7248        assert!(
7249            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
7250            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
7251        );
7252    }
7253
7254    #[test]
7255    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7256        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
7257            .parse()
7258            .unwrap();
7259
7260        parse(&spec, &input(&["ex", "run", "--clean"]))
7261            .expect("the child's spelling does not activate the orphan ancestor alias");
7262        assert!(
7263            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7264            "the inherited short alias still belongs to the ancestor exclusive flag"
7265        );
7266    }
7267
7268    #[test]
7269    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7270        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"
7271            .parse()
7272            .unwrap();
7273
7274        assert!(
7275            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7276            "the inherited negated alias still belongs to the ancestor exclusive flag"
7277        );
7278    }
7279
7280    #[test]
7281    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7282        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
7283        // an unrelated inherited global already owns. That collision is resolved in the other
7284        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
7285        // the `--clean` it declared, and its exclusivity holds.
7286        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"
7287            .parse()
7288            .unwrap();
7289
7290        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7291        assert!(
7292            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7293            "one unrelated alias collision cannot disown the child from its own flag"
7294        );
7295    }
7296
7297    #[test]
7298    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7299        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
7300        // subcommand word the flag can only be the ancestor's, because that is the only one in
7301        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
7302        // is set. The pair below differ in nothing else, which is what makes this one rule
7303        // rather than two behaviors.
7304        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7305            .parse()
7306            .unwrap();
7307        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7308            .expect("the ancestor owns this occurrence, and it is not exclusive");
7309        assert!(
7310            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7311            "after the subcommand word the child's declaration is in scope, and it is exclusive"
7312        );
7313
7314        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7315            .parse()
7316            .unwrap();
7317        assert!(
7318            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7319            "the same rule, with an exclusive ancestor: selecting the child is company for it"
7320        );
7321    }
7322
7323    #[test]
7324    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7325        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
7326        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
7327        // the ancestor ever declared, so the ancestor's answer still governs it.
7328        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
7329            .parse()
7330            .unwrap();
7331
7332        assert!(
7333            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7334            "the orphan ancestor alias is still the ancestor's exclusive flag"
7335        );
7336        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7337            .expect("the child's own spelling drops the exclusivity the child did not restate");
7338    }
7339
7340    #[test]
7341    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7342        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
7343        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
7344        // occurrence to the ancestor because `-c` appeared in it lost that.
7345        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7346            .parse()
7347            .unwrap();
7348
7349        assert!(
7350            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7351            "the child spelling is exclusive whatever it was typed beside"
7352        );
7353        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7354            .expect("the ancestor's own spelling was never exclusive");
7355    }
7356
7357    #[test]
7358    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7359        // An environment value has no spelling to attribute, so the declaration the selected
7360        // command has in scope answers — in both directions. Comparing whole alias sets asked
7361        // the ancestor instead, because the merged flag also carries its orphan `-c`.
7362        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"
7363            .parse()
7364            .unwrap();
7365
7366        assert!(
7367            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7368            "the child added exclusivity the environment value has to honor"
7369        );
7370
7371        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"
7372            .parse()
7373            .unwrap();
7374
7375        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7376            .expect("the child dropped the exclusivity, and the environment value follows it");
7377    }
7378
7379    #[test]
7380    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7381        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
7382        // the same reason the companion check does.
7383        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"
7384            .parse()
7385            .unwrap();
7386
7387        parse(&spec, &input(&["ex", "run", "--clean"]))
7388            .expect("a merged child exclusive flag is still the command's requiredness escape");
7389    }
7390
7391    #[test]
7392    fn a_group_allows_one_member_and_refuses_two() {
7393        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7394            .parse()
7395            .unwrap();
7396
7397        // One is fine, and so is none: a plain group says "at most one".
7398        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7399        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7400
7401        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7402        assert!(err.to_string().contains("group input"), "{err}");
7403    }
7404
7405    #[test]
7406    fn positional_selectors_work_in_conflicts_and_groups() {
7407        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7408            .parse()
7409            .unwrap();
7410        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7411            .expect("the flag alone is valid");
7412        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7413        assert!(
7414            parse(
7415                &conflicts,
7416                &input(&["ex", "--from-file", "vars.env", "literal"])
7417            )
7418            .is_err()
7419        );
7420
7421        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7422            .parse()
7423            .unwrap();
7424        assert!(
7425            parse(
7426                &positional_source,
7427                &input(&["ex", "--from-file", "vars.env", "literal"])
7428            )
7429            .is_err()
7430        );
7431
7432        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7433            .parse()
7434            .unwrap();
7435        assert!(parse(&group, &input(&["ex"])).is_err());
7436        parse(&group, &input(&["ex", "target-name"]))
7437            .expect("a positional satisfies a required group");
7438        assert!(
7439            parse(
7440                &group,
7441                &input(&["ex", "--file", "input.txt", "target-name"])
7442            )
7443            .is_err()
7444        );
7445    }
7446
7447    #[test]
7448    fn a_required_group_needs_one_of_its_members() {
7449        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7450            .parse()
7451            .unwrap();
7452
7453        let err = parse(&spec, &input(&["ex"])).unwrap_err();
7454        // The members, because that is what a user has to type; the name, because a
7455        // command with several groups would otherwise report the same sentence twice.
7456        assert!(err.to_string().contains("--file, --url"), "{err}");
7457        assert!(err.to_string().contains("input"), "{err}");
7458
7459        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7460    }
7461
7462    #[test]
7463    fn a_multiple_group_only_polices_requiredness() {
7464        // `multiple` with `required` is "at least one of these", so two is fine and
7465        // none is not.
7466        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7467            .parse()
7468            .unwrap();
7469
7470        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7471        assert!(parse(&spec, &input(&["ex"])).is_err());
7472    }
7473
7474    #[test]
7475    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7476        // The two halves of a group are two kinds of rule, and they read a default
7477        // differently on purpose. Requiredness asks whether a member has a value, and a
7478        // default is a value — the rule `requires` follows. Exclusivity asks what the
7479        // user supplied, because a defaulted member counted as supplied would collide
7480        // with the sibling they actually typed and refuse a correct command line.
7481        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7482            .parse()
7483            .unwrap();
7484
7485        parse(&spec, &input(&["ex"])).expect("the default fills the group");
7486        parse(&spec, &input(&["ex", "--url", "u"]))
7487            .expect("the default must not conflict with the flag the user typed");
7488    }
7489
7490    #[test]
7491    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7492        // `-f` and `--file` are one flag. Counted by selector, giving it once would
7493        // report it as conflicting with itself.
7494        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7495            .parse()
7496            .unwrap();
7497
7498        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7499        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7500
7501        // A genuine collision is still one.
7502        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7503        assert!(err.to_string().contains("group input"), "{err}");
7504    }
7505
7506    #[test]
7507    fn a_group_reads_the_environment_as_given() {
7508        // The environment does count, which is the same asymmetry `conflicts` has: an
7509        // env var is somebody saying something, a default is nobody saying anything.
7510        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7511            .parse()
7512            .unwrap();
7513
7514        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7515    }
7516
7517    #[test]
7518    fn a_requirement_names_the_flag_that_is_missing() {
7519        // Reported as the missing flag rather than as something wrong with `--out`,
7520        // which is what clap says for an unmet `requires` and what a user can act on.
7521        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7522            .parse()
7523            .unwrap();
7524
7525        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7526        assert!(
7527            err.to_string().contains("format"),
7528            "the missing flag should be named: {err}"
7529        );
7530
7531        // Satisfied, in either order.
7532        for words in [
7533            &["ex", "--out", "a.txt", "--format", "json"][..],
7534            &["ex", "--format", "json", "--out", "a.txt"][..],
7535        ] {
7536            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7537        }
7538
7539        // Nothing happens when the flag that imposes the rule is absent: a requirement
7540        // is a consequence of using the flag, not a rule about the command line.
7541        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7542    }
7543
7544    #[test]
7545    fn a_value_activates_only_its_conditional_requirement() {
7546        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"
7547            .parse()
7548            .unwrap();
7549
7550        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7551            .expect("an unrelated value requires nothing");
7552
7553        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7554        assert!(key.to_string().contains("key"), "{key}");
7555        parse(
7556            &spec,
7557            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7558        )
7559        .expect("the matching requirement is satisfied");
7560
7561        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7562        assert!(token.to_string().contains("token"), "{token}");
7563    }
7564
7565    #[test]
7566    fn conditional_requirements_read_explicit_env_but_not_defaults() {
7567        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"
7568            .parse()
7569            .unwrap();
7570        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7571        assert!(err.to_string().contains("key"), "{err}");
7572
7573        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"
7574            .parse()
7575            .unwrap();
7576        parse(&from_default, &input(&["ex"]))
7577            .expect("a default is not an explicit conditional value");
7578    }
7579
7580    #[test]
7581    fn command_line_values_override_env_for_conditional_requirements() {
7582        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7583            .parse()
7584            .unwrap();
7585
7586        parse_with_env(
7587            &spec,
7588            &["ex", "--config", "ordinary.toml"],
7589            &[("EX_CONFIG", "special.toml")],
7590        )
7591        .expect("the command-line value takes precedence over the environment");
7592    }
7593
7594    #[test]
7595    fn conditional_requirements_normalize_boolean_env_values() {
7596        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7597            .parse()
7598            .unwrap();
7599
7600        for value in ["1", "true", "True", "TRUE"] {
7601            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7602            assert!(err.to_string().contains("key"), "{value}: {err}");
7603        }
7604        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7605            .expect("a false environment value does not activate a true condition");
7606    }
7607
7608    #[test]
7609    fn a_default_satisfies_a_requirement() {
7610        // The flag it names has a value, which is the question a requirement asks. Read
7611        // any other way, `--format` would be missing here and present ten lines further
7612        // down, where plain required-ness reads the same default as filling it.
7613        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7614            .parse()
7615            .unwrap();
7616
7617        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7618            .expect("a defaulted flag is not a missing one");
7619    }
7620
7621    #[test]
7622    fn a_present_flag_binds_a_conditional_default() {
7623        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7624            .parse()
7625            .unwrap();
7626
7627        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7628        assert_eq!(
7629            with.as_env().get("usage_bin_names").map(String::as_str),
7630            Some("true")
7631        );
7632
7633        let without = parse(&spec, &input(&["ex"])).unwrap();
7634        assert!(
7635            !without.as_env().contains_key("usage_bin_names"),
7636            "IsPresent does nothing when the selector is absent"
7637        );
7638    }
7639
7640    #[test]
7641    fn an_equals_condition_binds_a_conditional_default() {
7642        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7643            .parse()
7644            .unwrap();
7645
7646        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7647        assert_eq!(
7648            json.as_env().get("usage_style").map(String::as_str),
7649            Some("pretty")
7650        );
7651        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7652        assert!(!yaml.as_env().contains_key("usage_style"));
7653    }
7654
7655    #[test]
7656    fn an_equals_condition_reads_a_negated_flag() {
7657        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7658            .parse()
7659            .unwrap();
7660
7661        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7662        assert_eq!(
7663            off.as_env().get("usage_pretty").map(String::as_str),
7664            Some("true")
7665        );
7666        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7667        assert!(
7668            !on.as_env().contains_key("usage_pretty"),
7669            "--json is true, so when=false should miss"
7670        );
7671    }
7672
7673    #[test]
7674    fn the_first_matching_conditional_default_wins() {
7675        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"
7676            .parse()
7677            .unwrap();
7678
7679        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7680        assert_eq!(
7681            out.as_env().get("usage_style").map(String::as_str),
7682            Some("compact")
7683        );
7684    }
7685
7686    #[test]
7687    fn argv_and_env_suppress_a_conditional_default() {
7688        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7689            .parse()
7690            .unwrap();
7691
7692        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7693        assert_eq!(
7694            from_env.as_env().get("usage_bin_names").map(String::as_str),
7695            Some("false"),
7696            "the target's environment wins over default_if"
7697        );
7698    }
7699
7700    #[test]
7701    fn a_sibling_env_activates_a_conditional_default() {
7702        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7703            .parse()
7704            .unwrap();
7705
7706        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7707        assert_eq!(
7708            out.as_env().get("usage_bin_names").map(String::as_str),
7709            Some("true")
7710        );
7711    }
7712
7713    #[test]
7714    fn a_default_does_not_activate_a_conditional_default() {
7715        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7716        // bind would put json's default into `out.flags` and then treat it as
7717        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7718        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7719            .parse()
7720            .unwrap();
7721
7722        let out = parse(&spec, &input(&["ex"])).unwrap();
7723        assert_eq!(
7724            out.as_env().get("usage_json").map(String::as_str),
7725            Some("true")
7726        );
7727        assert!(
7728            !out.as_env().contains_key("usage_pretty"),
7729            "a default is not an explicit value for default_if"
7730        );
7731    }
7732
7733    #[test]
7734    fn a_conditional_default_does_not_activate_requires_if() {
7735        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"
7736            .parse()
7737            .unwrap();
7738
7739        parse(&spec, &input(&["ex", "--json"]))
7740            .expect("a default_if value is not explicit for requires_if");
7741        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7742    }
7743
7744    #[test]
7745    fn a_conditional_default_satisfies_a_requirement() {
7746        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7747            .parse()
7748            .unwrap();
7749
7750        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7751            .expect("default_if fills the required flag");
7752        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7753    }
7754
7755    #[test]
7756    fn an_environment_value_satisfies_a_requirement() {
7757        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7758            .parse()
7759            .unwrap();
7760
7761        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7762        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7763            .expect("the environment supplies it");
7764    }
7765
7766    #[test]
7767    fn a_requirement_is_satisfied_by_a_short_form() {
7768        // The selector may spell the other flag any way it answers to, so the check
7769        // resolves it the way every other selector is resolved rather than matching
7770        // text. The error names the flag, not the selector.
7771        let spec: Spec =
7772            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7773                .parse()
7774                .unwrap();
7775
7776        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7777
7778        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7779        assert!(err.to_string().contains("key"), "{err}");
7780    }
7781
7782    #[test]
7783    fn conflicting_flags_are_rejected_in_either_order() {
7784        // Declared once, on `--file`, which is all clap exposes — so the check has to
7785        // be order-independent by looking at every flag that was given rather than at
7786        // the one that declared the conflict.
7787        let spec: Spec =
7788            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7789                .parse()
7790                .unwrap();
7791
7792        for words in [
7793            &["ex", "--file", "a.txt", "--stdin"][..],
7794            &["ex", "--stdin", "--file", "a.txt"][..],
7795        ] {
7796            let err = parse(&spec, &input(words)).unwrap_err();
7797            assert!(
7798                err.to_string().contains("conflicts with --stdin"),
7799                "{words:?} should be refused: {err}"
7800            );
7801        }
7802
7803        // Either one alone is fine.
7804        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7805        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7806    }
7807
7808    #[test]
7809    fn unknown_flags_are_values_by_default() {
7810        // The default, and the reason it is the default: a spec often parses a
7811        // command line whose flags belong to something else.
7812        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7813            .parse()
7814            .unwrap();
7815        let out = parse(
7816            &spec,
7817            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7818        )
7819        .unwrap();
7820        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7821        assert_eq!(out.args[rest].to_string(), "--wat x");
7822    }
7823
7824    #[test]
7825    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7826        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7827            .parse()
7828            .unwrap();
7829        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7830            .expect("a repeat is a correction by default");
7831        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7832        assert_eq!(out.flags[jobs].to_string(), "2");
7833        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7834            .expect("switches use the same default");
7835
7836        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7837            .parse()
7838            .unwrap();
7839        for words in [
7840            &["ex", "--jobs", "1", "--jobs", "2"][..],
7841            &["ex", "--verbose", "--verbose"][..],
7842        ] {
7843            let err = parse(&strict, &input(words)).unwrap_err();
7844            assert!(
7845                err.to_string().contains("cannot be used multiple times"),
7846                "{err}"
7847            );
7848        }
7849
7850        let reparsed: Spec = strict.to_string().parse().unwrap();
7851        assert!(!reparsed.cmd.args_override_self);
7852    }
7853
7854    #[test]
7855    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7856        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7857            .parse()
7858            .unwrap();
7859
7860        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7861            .expect("opposite forms override each other");
7862        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7863        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7864
7865        for words in [
7866            &["ex", "--color", "--color"][..],
7867            &["ex", "--no-color", "--no-color"][..],
7868        ] {
7869            let err = parse(&spec, &input(words)).unwrap_err();
7870            assert!(err.to_string().contains("cannot be used multiple times"));
7871        }
7872    }
7873
7874    #[test]
7875    fn strict_global_flags_may_repeat_across_command_levels() {
7876        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"
7877            .parse()
7878            .unwrap();
7879
7880        let out = parse(
7881            &spec,
7882            &input(&[
7883                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7884            ]),
7885        )
7886        .expect("an inherited global is allowed once at each command level");
7887        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7888        assert_eq!(out.flags[jobs].to_string(), "2");
7889
7890        for words in [
7891            &["ex", "--color", "--color", "run", "--no-color"][..],
7892            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7893        ] {
7894            let err = parse(&spec, &input(words)).unwrap_err();
7895            assert!(err.to_string().contains("cannot be used multiple times"));
7896        }
7897    }
7898
7899    #[test]
7900    fn a_subcommand_can_negate_only_its_parents_requirements() {
7901        let base = r#"name "ex"
7902bin "ex"
7903flag "--config" required=#true
7904flag "--mode" requires="--config"
7905flag "--other"
7906arg "<input>"
7907group "source" "--config" "--other" required=#true
7908cmd "run" { flag "--child" required=#true }
7909"#;
7910        let strict: Spec = base.parse().unwrap();
7911        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7912        let message = err.to_string();
7913        assert!(
7914            message.contains("input") || message.contains("config"),
7915            "{message}"
7916        );
7917
7918        let negated: Spec = base
7919            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7920            .parse()
7921            .unwrap();
7922        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7923        assert!(
7924            err.to_string().contains("child"),
7925            "the selected command keeps its own requirements: {err}"
7926        );
7927
7928        let mut child_optional = negated.clone();
7929        child_optional.cmd.subcommands["run"].flags[0].required = false;
7930        parse(&child_optional, &input(&["ex", "run"]))
7931            .expect("the child selection satisfies all parent requirements");
7932        parse(&child_optional, &input(&["ex", "--mode", "run"]))
7933            .expect("parent requires relationships are negated too");
7934    }
7935
7936    #[test]
7937    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7938        let spec: Spec = r#"name "ex"
7939bin "ex"
7940args_conflicts_with_subcommands #true
7941flag "--verbose"
7942cmd "run"
7943"#
7944        .parse()
7945        .unwrap();
7946
7947        parse(&spec, &input(&["ex", "run"]))
7948            .expect("the subcommand is valid without a parent argument");
7949        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7950        assert!(
7951            err.to_string().contains("cannot be used with arguments"),
7952            "{err}"
7953        );
7954    }
7955
7956    #[test]
7957    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7958        let base = r#"name "ex"
7959bin "ex"
7960flag "--values <value>..."
7961cmd "run"
7962"#;
7963        let plain: Spec = base.parse().unwrap();
7964        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7965        assert_eq!(out.cmd.name, "ex");
7966
7967        let precedence: Spec = base
7968            .replacen(
7969                "bin \"ex\"",
7970                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7971                1,
7972            )
7973            .parse()
7974            .unwrap();
7975        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7976        assert_eq!(out.cmd.name, "run");
7977    }
7978
7979    #[test]
7980    fn a_required_positional_can_follow_an_unfilled_optional_one() {
7981        let base = r#"name "ex"
7982bin "ex"
7983arg "[optional]"
7984arg "<required>"
7985"#;
7986        let plain: Spec = base.parse().unwrap();
7987        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7988        assert!(err.to_string().contains("required"), "{err}");
7989
7990        let enabled: Spec = base
7991            .replacen(
7992                "bin \"ex\"",
7993                "bin \"ex\"\nallow_missing_positional #true",
7994                1,
7995            )
7996            .parse()
7997            .unwrap();
7998        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7999        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8000        let value = &out
8001            .args
8002            .iter()
8003            .find(|(arg, _)| arg.name == "required")
8004            .unwrap()
8005            .1;
8006        assert!(matches!(value, ParseValue::String(value) if value == "value"));
8007    }
8008
8009    #[test]
8010    fn sigil_args_do_not_block_optional_positional_skipping() {
8011        let spec: Spec = r#"name "ex"
8012bin "ex"
8013allow_missing_positional #true
8014arg "[optional]"
8015arg "[tool]" sigil="@"
8016arg "<required>"
8017"#
8018        .parse()
8019        .unwrap();
8020
8021        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
8022        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8023        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
8024        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
8025        assert_eq!(out.args[tool].to_string(), "node");
8026        assert_eq!(out.args[required].to_string(), "value");
8027    }
8028
8029    #[test]
8030    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
8031        let spec: Spec =
8032            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
8033                .parse()
8034                .unwrap();
8035        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
8036        assert!(
8037            err.to_string().contains("--wat"),
8038            "the message should name the token: {err}"
8039        );
8040
8041        // The positional opts into the narrower negative-number carve-out without
8042        // accepting arbitrary unknown flags.
8043        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
8044        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8045        assert_eq!(out.args[rest].to_string(), "-1");
8046
8047        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
8048        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8049        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8050    }
8051
8052    #[test]
8053    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
8054        let spec: Spec = r#"
8055name "ex"
8056bin "ex"
8057unknown_flags "error"
8058flag "-0 --print0" global=#true
8059cmd "run" {
8060  flag "--force"
8061}
8062"#
8063        .parse()
8064        .unwrap();
8065        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
8066        assert_eq!(out.cmd.name, "run");
8067        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8068        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
8069        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8070        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
8071    }
8072
8073    #[test]
8074    fn a_command_may_override_the_cli_wide_setting() {
8075        // Strict overall, lenient for the one command that forwards options.
8076        let spec: Spec = r#"
8077name "ex"
8078bin "ex"
8079unknown_flags "error"
8080cmd "exec" unknown_flags="value" {
8081  arg "[rest]..."
8082}
8083cmd "build" {
8084  arg "[rest]..."
8085}
8086"#
8087        .parse()
8088        .unwrap();
8089
8090        let out = parse(
8091            &spec,
8092            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
8093        )
8094        .unwrap();
8095        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8096        assert_eq!(out.args[rest].to_string(), "--wat");
8097
8098        assert!(
8099            parse(
8100                &spec,
8101                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
8102            )
8103            .is_err(),
8104            "a command that says nothing inherits the CLI's choice"
8105        );
8106    }
8107
8108    #[test]
8109    fn the_setting_survives_a_round_trip() {
8110        let spec: Spec =
8111            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
8112                .parse()
8113                .unwrap();
8114        let reparsed: Spec = spec.to_string().parse().unwrap();
8115        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
8116        assert_eq!(
8117            reparsed.cmd.subcommands["x"].unknown_flags,
8118            Some(UnknownFlags::Value)
8119        );
8120    }
8121
8122    #[test]
8123    fn test_default_subcommand() {
8124        // Test that default_subcommand routes to the specified subcommand
8125        let run_cmd = SpecCommand::builder()
8126            .name("run")
8127            .arg(SpecArg::builder().name("task").build())
8128            .build();
8129        let mut cmd = SpecCommand::builder().name("test").build();
8130        cmd.subcommands.insert("run".to_string(), run_cmd);
8131
8132        let spec = Spec {
8133            name: "test".to_string(),
8134            bin: "test".to_string(),
8135            cmd,
8136            default_subcommand: Some("run".to_string()),
8137            ..Default::default()
8138        };
8139
8140        // "test mytask" should be parsed as if it were "test run mytask"
8141        let input = vec!["test".to_string(), "mytask".to_string()];
8142        let parsed = parse(&spec, &input).unwrap();
8143
8144        // Should have two commands: root and "run"
8145        assert_eq!(parsed.cmds.len(), 2);
8146        assert_eq!(parsed.cmds[1].name, "run");
8147
8148        // Should have parsed the task argument
8149        assert_eq!(parsed.args.len(), 1);
8150        let arg = parsed.args.keys().next().unwrap();
8151        assert_eq!(arg.name, "task");
8152        let value = parsed.args.values().next().unwrap();
8153        assert_eq!(value.to_string(), "mytask");
8154    }
8155
8156    #[test]
8157    fn default_subcommand_outranks_root_sigil_arg() {
8158        let spec: Spec = r#"
8159name "test"
8160bin "test"
8161default_subcommand "run"
8162arg "[tools]..." sigil="+"
8163cmd "run" { arg "<task>" }
8164"#
8165        .parse()
8166        .unwrap();
8167
8168        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
8169        assert_eq!(parsed.cmd.name, "run");
8170        let value = |name| {
8171            parsed
8172                .args
8173                .iter()
8174                .find(|(arg, _)| arg.name == name)
8175                .map(|(_, value)| value.to_string())
8176                .unwrap()
8177        };
8178        assert_eq!(value("tools"), "node");
8179        assert_eq!(value("task"), "node");
8180    }
8181
8182    #[test]
8183    fn test_default_subcommand_explicit_still_works() {
8184        // Test that explicit subcommand takes precedence
8185        let run_cmd = SpecCommand::builder()
8186            .name("run")
8187            .arg(SpecArg::builder().name("task").build())
8188            .build();
8189        let other_cmd = SpecCommand::builder()
8190            .name("other")
8191            .arg(SpecArg::builder().name("other_arg").build())
8192            .build();
8193        let mut cmd = SpecCommand::builder().name("test").build();
8194        cmd.subcommands.insert("run".to_string(), run_cmd);
8195        cmd.subcommands.insert("other".to_string(), other_cmd);
8196
8197        let spec = Spec {
8198            name: "test".to_string(),
8199            bin: "test".to_string(),
8200            cmd,
8201            default_subcommand: Some("run".to_string()),
8202            ..Default::default()
8203        };
8204
8205        // "test other foo" should use "other" subcommand, not default
8206        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
8207        let parsed = parse(&spec, &input).unwrap();
8208
8209        // Should have used "other" subcommand
8210        assert_eq!(parsed.cmds.len(), 2);
8211        assert_eq!(parsed.cmds[1].name, "other");
8212    }
8213
8214    #[test]
8215    fn test_default_subcommand_applies_only_at_the_root() {
8216        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
8217        // was being looked up wherever the parser happened to be standing, so a command with
8218        // an unrelated subcommand of the same name acquired a default of its own: with
8219        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
8220        // `zzz` there. Nothing declared that, and nothing could have.
8221        let mut config_ls = SpecCommand::builder().name("ls").build();
8222        config_ls.args.push(SpecArg::builder().name("what").build());
8223        let mut config_cmd = SpecCommand::builder().name("config").build();
8224        config_cmd.subcommands.insert("ls".to_string(), config_ls);
8225
8226        // The root's own `ls`, which is what its default points at. It takes an argument so
8227        // that a routed word has somewhere to land.
8228        let mut root_ls = SpecCommand::builder().name("ls").build();
8229        root_ls.args.push(SpecArg::builder().name("what").build());
8230        let mut cmd = SpecCommand::builder().name("ex").build();
8231        cmd.subcommands.insert("ls".to_string(), root_ls);
8232        cmd.subcommands.insert("config".to_string(), config_cmd);
8233
8234        let spec = Spec {
8235            name: "ex".to_string(),
8236            bin: "ex".to_string(),
8237            cmd,
8238            default_subcommand: Some("ls".to_string()),
8239            ..Default::default()
8240        };
8241
8242        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
8243        // own business — and `config` takes no argument, so this is an error rather than a
8244        // silent descent.
8245        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
8246        assert!(
8247            parse(&spec, &input).is_err(),
8248            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
8249        );
8250
8251        // At the root, where it is declared, it still applies.
8252        let input = vec!["ex".to_string(), "zzz".to_string()];
8253        let parsed = parse(&spec, &input).expect("the root's default applies");
8254        assert_eq!(
8255            parsed
8256                .cmds
8257                .iter()
8258                .map(|c| c.name.as_str())
8259                .collect::<Vec<_>>(),
8260            ["ex", "ls"]
8261        );
8262        assert_eq!(
8263            parsed.args.values().next().map(|v| v.to_string()),
8264            Some("zzz".to_string()),
8265            "and the word binds inside the command it reached"
8266        );
8267    }
8268
8269    #[test]
8270    fn test_default_subcommand_with_nested_subcommands() {
8271        // Test that default_subcommand works when the default subcommand has nested subcommands.
8272        // This is the mise use case: "mise say" should be parsed as "mise run say"
8273        // where "say" is a subcommand of "run" (a task).
8274        let say_cmd = SpecCommand::builder()
8275            .name("say")
8276            .arg(SpecArg::builder().name("name").build())
8277            .build();
8278        let mut run_cmd = SpecCommand::builder().name("run").build();
8279        run_cmd.subcommands.insert("say".to_string(), say_cmd);
8280
8281        let mut cmd = SpecCommand::builder().name("test").build();
8282        cmd.subcommands.insert("run".to_string(), run_cmd);
8283
8284        let spec = Spec {
8285            name: "test".to_string(),
8286            bin: "test".to_string(),
8287            cmd,
8288            default_subcommand: Some("run".to_string()),
8289            ..Default::default()
8290        };
8291
8292        // "test say hello" should be parsed as "test run say hello"
8293        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8294        let parsed = parse(&spec, &input).unwrap();
8295
8296        // Should have three commands: root, "run", and "say"
8297        assert_eq!(parsed.cmds.len(), 3);
8298        assert_eq!(parsed.cmds[0].name, "test");
8299        assert_eq!(parsed.cmds[1].name, "run");
8300        assert_eq!(parsed.cmds[2].name, "say");
8301
8302        // Should have parsed the "name" argument
8303        assert_eq!(parsed.args.len(), 1);
8304        let arg = parsed.args.keys().next().unwrap();
8305        assert_eq!(arg.name, "name");
8306        let value = parsed.args.values().next().unwrap();
8307        assert_eq!(value.to_string(), "hello");
8308    }
8309
8310    /// Build a spec equivalent to the post-mount structure produced by mise's
8311    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
8312    /// subcommand that re-declares the same flag as NON-global, and a mounted task
8313    /// (`sample:run`) carrying a positional arg with `choices`.
8314    ///
8315    /// We construct the merged structure directly instead of executing a real mount so the
8316    /// test stays hermetic and cross-platform while still exercising the parser defect.
8317    fn mounted_global_flag_spec() -> Spec {
8318        let task_cmd = SpecCommand::builder()
8319            .name("sample:run")
8320            .arg(
8321                SpecArg::builder()
8322                    .name("profile")
8323                    .choices(["alpha", "beta", "gamma"])
8324                    .build(),
8325            )
8326            .build();
8327        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
8328        let mut run_cmd = SpecCommand::builder()
8329            .name("run")
8330            .flag(
8331                SpecFlag::builder()
8332                    .name("cd")
8333                    .short('C')
8334                    .long("cd")
8335                    .arg(SpecArg::builder().name("dir").build())
8336                    .global(false)
8337                    .build(),
8338            )
8339            .build();
8340        run_cmd
8341            .subcommands
8342            .insert("sample:run".to_string(), task_cmd);
8343
8344        let mut cmd = SpecCommand::builder()
8345            .name("test")
8346            .flag(
8347                SpecFlag::builder()
8348                    .name("cd")
8349                    .short('C')
8350                    .long("cd")
8351                    .arg(SpecArg::builder().name("dir").build())
8352                    .global(true)
8353                    .build(),
8354            )
8355            .build();
8356        cmd.subcommands.insert("run".to_string(), run_cmd);
8357
8358        Spec {
8359            name: "test".to_string(),
8360            bin: "test".to_string(),
8361            cmd,
8362            ..Default::default()
8363        }
8364    }
8365
8366    #[test]
8367    fn test_prefix_global_flag_does_not_pollute_choices() {
8368        // Regression for the parser-side root cause referenced by jdx/mise#10069.
8369        //
8370        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
8371        // then into the mounted `sample:run`) used to drop the inherited global flag from
8372        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
8373        // mis-validated against the task's `choices` positional arg.
8374        let spec = mounted_global_flag_spec();
8375
8376        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
8377        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
8378        for words in [
8379            &["test", "-C", "/tmp", "run", "sample:run"][..],
8380            // Embedded-value form must behave identically.
8381            &["test", "--cd=/tmp", "run", "sample:run"][..],
8382        ] {
8383            let parsed = parse_partial(&spec, &input(words)).unwrap();
8384            assert_eq!(
8385                parsed
8386                    .cmds
8387                    .iter()
8388                    .map(|c| c.name.as_str())
8389                    .collect::<Vec<_>>(),
8390                vec!["test", "run", "sample:run"],
8391            );
8392            // No positional arg should have been consumed by the leftover global-flag tokens.
8393            assert!(
8394                parsed.args.is_empty(),
8395                "args should be empty, got {:?}",
8396                parsed.args
8397            );
8398
8399            // Fix (B): the inherited global flag survives the descent even though `run`
8400            // re-declares `-C/--cd` as non-global.
8401            let cd = parsed
8402                .available_flags
8403                .get("--cd")
8404                .expect("--cd should remain available after descending into the subcommand");
8405            assert!(cd.global, "--cd must stay global after descent");
8406            assert!(
8407                parsed.available_flags.get("-C").is_some_and(|f| f.global),
8408                "-C must stay global after descent",
8409            );
8410
8411            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
8412            // for normal execution and for the env passed to mount scripts. (Removing the
8413            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
8414            assert_eq!(
8415                parsed.as_env().get("usage_cd").map(String::as_str),
8416                Some("/tmp"),
8417                "global flag value must survive in as_env(), got {:?}",
8418                parsed.as_env(),
8419            );
8420        }
8421
8422        // A real, valid choice still parses through the global flag prefix.
8423        let parsed = parse_partial(
8424            &spec,
8425            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8426        )
8427        .unwrap();
8428        assert_eq!(parsed.args.len(), 1);
8429        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8430
8431        // And genuinely invalid choices are still rejected (we didn't disable validation).
8432        assert_parse_err(
8433            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8434            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8435        );
8436    }
8437
8438    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
8439    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
8440    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
8441    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
8442    fn mounted_orphan_short_spec() -> Spec {
8443        let task_cmd = SpecCommand::builder()
8444            .name("sample:run")
8445            .arg(
8446                SpecArg::builder()
8447                    .name("profile")
8448                    .choices(["alpha", "beta", "gamma"])
8449                    .build(),
8450            )
8451            .build();
8452        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
8453        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
8454        let mut run_cmd = SpecCommand::builder()
8455            .name("run")
8456            .flag(
8457                SpecFlag::builder()
8458                    .name("raw")
8459                    .short('r')
8460                    .long("raw")
8461                    .global(false)
8462                    .build(),
8463            )
8464            .flag(
8465                SpecFlag::builder()
8466                    .name("force")
8467                    .short('f')
8468                    .long("force")
8469                    .global(false)
8470                    .build(),
8471            )
8472            .build();
8473        run_cmd
8474            .subcommands
8475            .insert("sample:run".to_string(), task_cmd);
8476
8477        // Root global is LONG-ONLY: `--raw` with no short.
8478        let mut cmd = SpecCommand::builder()
8479            .name("test")
8480            .flag(
8481                SpecFlag::builder()
8482                    .name("raw")
8483                    .long("raw")
8484                    .global(true)
8485                    .build(),
8486            )
8487            .build();
8488        cmd.subcommands.insert("run".to_string(), run_cmd);
8489
8490        Spec {
8491            name: "test".to_string(),
8492            bin: "test".to_string(),
8493            cmd,
8494            ..Default::default()
8495        }
8496    }
8497
8498    #[test]
8499    fn test_orphan_short_alias_survives_merge() {
8500        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
8501        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
8502        // added short `-r` must be unioned onto the surviving inherited global flag instead of
8503        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
8504        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
8505        let spec = mounted_orphan_short_spec();
8506
8507        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).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
8517        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
8518        // and the original long `--raw` is still global too.
8519        assert!(
8520            parsed.available_flags.get("-r").is_some_and(|f| f.global),
8521            "-r must be merged onto the inherited global flag and stay global after descent",
8522        );
8523        assert!(
8524            parsed
8525                .available_flags
8526                .get("--raw")
8527                .is_some_and(|f| f.global),
8528            "--raw must stay global after descent",
8529        );
8530
8531        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
8532        assert!(
8533            parsed.args.is_empty(),
8534            "args should be empty, got {:?}",
8535            parsed.args
8536        );
8537
8538        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
8539        assert_eq!(
8540            parsed.as_env().get("usage_raw").map(String::as_str),
8541            Some("true"),
8542            "merged short's value must survive in as_env(), got {:?}",
8543            parsed.as_env(),
8544        );
8545
8546        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
8547        // promoted/merged — it is correctly dropped when descending into the mount.
8548        assert!(
8549            !parsed.available_flags.contains_key("-f"),
8550            "purely-local -f must not be promoted onto a global",
8551        );
8552        assert!(
8553            !parsed.available_flags.contains_key("--force"),
8554            "purely-local --force must not be promoted onto a global",
8555        );
8556
8557        // A real, valid choice still parses through the merged short prefix.
8558        let parsed =
8559            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8560        assert_eq!(parsed.args.len(), 1);
8561        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8562
8563        // And genuinely invalid choices are still rejected.
8564        assert_parse_err(
8565            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8566            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8567        );
8568    }
8569
8570    #[test]
8571    fn test_orphan_short_does_not_clobber_unrelated_global() {
8572        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
8573        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
8574        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
8575        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
8576        let run_cmd = SpecCommand::builder()
8577            .name("run")
8578            .flag(
8579                SpecFlag::builder()
8580                    .name("raw")
8581                    .short('r')
8582                    .long("raw")
8583                    .global(false)
8584                    .build(),
8585            )
8586            .build();
8587        let mut cmd = SpecCommand::builder()
8588            .name("test")
8589            .flag(
8590                SpecFlag::builder()
8591                    .name("raw")
8592                    .long("raw")
8593                    .global(true)
8594                    .build(),
8595            )
8596            .flag(
8597                SpecFlag::builder()
8598                    .name("restrict")
8599                    .short('r')
8600                    .long("restrict")
8601                    .global(true)
8602                    .build(),
8603            )
8604            .build();
8605        cmd.subcommands.insert("run".to_string(), run_cmd);
8606        let spec = Spec {
8607            name: "test".to_string(),
8608            bin: "test".to_string(),
8609            cmd,
8610            ..Default::default()
8611        };
8612
8613        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8614        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8615        assert_eq!(
8616            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8617            Some("restrict"),
8618            "-r must remain owned by the unrelated global it already belonged to",
8619        );
8620        // Both globals are still recognized and global after the descent.
8621        assert!(
8622            parsed
8623                .available_flags
8624                .get("--raw")
8625                .is_some_and(|f| f.global)
8626        );
8627        assert!(
8628            parsed
8629                .available_flags
8630                .get("--restrict")
8631                .is_some_and(|f| f.global)
8632        );
8633    }
8634
8635    #[test]
8636    fn test_redeclared_global_aliases_share_one_flag() {
8637        // A global declared with BOTH a short and a long, re-declared non-globally by a
8638        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8639        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8640        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8641        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8642        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8643        let spec = r#"
8644flag "-y --yes" global=#true effect="write"
8645cmd "run" {
8646    flag "-y --yes --assume-yes"
8647}
8648"#
8649        .parse::<Spec>()
8650        .unwrap();
8651
8652        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8653
8654        for key in ["-y", "--yes", "--assume-yes"] {
8655            let flag = parsed
8656                .available_flags
8657                .get(key)
8658                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8659            assert!(flag.global, "{key} must stay global after the descent");
8660            assert_eq!(
8661                flag.long,
8662                vec!["yes".to_string(), "assume-yes".to_string()],
8663                "{key} must resolve to the flag carrying every alias",
8664            );
8665            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8666        }
8667
8668        // One logical flag means one object: all three keys share a single `Arc`.
8669        assert_eq!(
8670            unique_flags(parsed.available_flags.values()).count(),
8671            1,
8672            "all aliases must point at one flag object, got {:?}",
8673            parsed.available_flags,
8674        );
8675
8676        // The global's effect survives the merge, so `-y` still marks the command as writing.
8677        assert_eq!(
8678            parsed.available_flags["-y"].effect,
8679            Some(crate::SpecCommandEffect::Write),
8680        );
8681    }
8682
8683    #[test]
8684    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8685        let spec = r#"
8686flag "--yes" global=#true {
8687    alias "-q" "--quietly" hide=#true
8688}
8689cmd "run" {
8690    flag "--yes --assume-yes" {
8691        alias "-s" "--secret" hide=#true
8692    }
8693}
8694"#
8695        .parse::<Spec>()
8696        .unwrap();
8697
8698        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8699        let merged = &parsed.available_flags["--yes"];
8700        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8701        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8702        for key in ["-q", "-s", "--quietly", "--secret"] {
8703            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8704        }
8705    }
8706
8707    #[test]
8708    fn test_redeclared_global_can_promote_hidden_aliases() {
8709        let spec = r#"
8710flag "--yes" global=#true {
8711    alias "-q" "--quietly" hide=#true
8712}
8713cmd "run" {
8714    flag "-q --yes --quietly"
8715}
8716"#
8717        .parse::<Spec>()
8718        .unwrap();
8719
8720        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8721        let merged = &parsed.available_flags["--yes"];
8722        assert!(merged.hidden_short_aliases.is_empty());
8723        assert!(merged.hidden_aliases.is_empty());
8724        for key in ["-q", "--quietly"] {
8725            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8726        }
8727    }
8728
8729    #[test]
8730    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8731        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8732        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8733        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8734        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8735        // pre-merge global and miss the added `assume-yes`.
8736        let spec = r#"
8737flag "-y --yes --confirm" global=#true
8738cmd "run" {
8739    flag "--yes --assume-yes"
8740}
8741"#
8742        .parse::<Spec>()
8743        .unwrap();
8744
8745        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8746
8747        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8748            let flag = parsed
8749                .available_flags
8750                .get(key)
8751                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8752            assert!(flag.global, "{key} must stay global after the descent");
8753            assert_eq!(
8754                flag.long,
8755                vec![
8756                    "yes".to_string(),
8757                    "confirm".to_string(),
8758                    "assume-yes".to_string()
8759                ],
8760                "{key} must resolve to the flag carrying every alias",
8761            );
8762        }
8763
8764        assert_eq!(
8765            unique_flags(parsed.available_flags.values()).count(),
8766            1,
8767            "all aliases must point at one flag object, got {:?}",
8768            parsed.available_flags,
8769        );
8770    }
8771
8772    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8773    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8774    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8775    ///
8776    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8777    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8778    fn mounted_task_flag_spec() -> Spec {
8779        let mut task_cmd = SpecCommand::builder()
8780            .name("mytask")
8781            .flag(
8782                SpecFlag::builder()
8783                    .name("env")
8784                    .long("env")
8785                    .arg(
8786                        SpecArg::builder()
8787                            .name("name")
8788                            .choices(["dev", "stage", "prod"])
8789                            .build(),
8790                    )
8791                    .global(false)
8792                    .build(),
8793            )
8794            .flag(
8795                SpecFlag::builder()
8796                    .name("bump")
8797                    .long("bump")
8798                    .arg(
8799                        SpecArg::builder()
8800                            .name("type")
8801                            .choices(["auto", "major"])
8802                            .build(),
8803                    )
8804                    .global(false)
8805                    .build(),
8806            )
8807            .build();
8808        task_cmd.mounted = true;
8809
8810        let mut run_cmd = SpecCommand::builder()
8811            .name("run")
8812            .flag(
8813                SpecFlag::builder()
8814                    .name("force")
8815                    .short('f')
8816                    .long("force")
8817                    .global(false)
8818                    .build(),
8819            )
8820            .build();
8821        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8822
8823        let mut cmd = SpecCommand::builder()
8824            .name("test")
8825            .flag(
8826                SpecFlag::builder()
8827                    .name("env")
8828                    .short('E')
8829                    .long("env")
8830                    .arg(SpecArg::builder().name("ENV").build())
8831                    .global(true)
8832                    .build(),
8833            )
8834            .flag(
8835                SpecFlag::builder()
8836                    .name("silent")
8837                    .long("silent")
8838                    .global(true)
8839                    .build(),
8840            )
8841            .build();
8842        cmd.subcommands.insert("run".to_string(), run_cmd);
8843
8844        Spec {
8845            name: "test".to_string(),
8846            bin: "test".to_string(),
8847            cmd,
8848            ..Default::default()
8849        }
8850    }
8851
8852    #[test]
8853    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8854        // The mounted program's own commands are ordinary commands relative to each other, so
8855        // descending *within* the mounted tree must follow the normal rules — including keeping
8856        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8857        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8858        // global, which the next descent's `retain(global)` then dropped entirely.
8859        let deep = SpecCommand::builder().name("deep").build();
8860        let mut sub = SpecCommand::builder()
8861            .name("sub")
8862            // Re-declares the mounted program's own global as non-global.
8863            .flag(
8864                SpecFlag::builder()
8865                    .name("cd")
8866                    .short('C')
8867                    .long("cd")
8868                    .arg(SpecArg::builder().name("dir").build())
8869                    .global(false)
8870                    .build(),
8871            )
8872            .build();
8873        sub.subcommands.insert("deep".to_string(), deep);
8874        let mut task = SpecCommand::builder()
8875            .name("task")
8876            .flag(
8877                SpecFlag::builder()
8878                    .name("cd")
8879                    .short('C')
8880                    .long("cd")
8881                    .arg(SpecArg::builder().name("dir").build())
8882                    .global(true)
8883                    .build(),
8884            )
8885            .build();
8886        task.subcommands.insert("sub".to_string(), sub);
8887        task.mark_mounted();
8888
8889        let mut run_cmd = SpecCommand::builder().name("run").build();
8890        run_cmd.subcommands.insert("task".to_string(), task);
8891        let mut cmd = SpecCommand::builder().name("test").build();
8892        cmd.subcommands.insert("run".to_string(), run_cmd);
8893        let spec = Spec {
8894            name: "test".to_string(),
8895            bin: "test".to_string(),
8896            cmd,
8897            ..Default::default()
8898        };
8899
8900        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8901        assert!(
8902            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8903            "the mounted program's own global must survive descents inside the mounted tree",
8904        );
8905        assert!(
8906            parsed.completion_flags().contains_key("--cd"),
8907            "and must still be offered there: it belongs to the mounted program",
8908        );
8909        assert!(
8910            parsed.completion_flags().contains_key("-C"),
8911            "including the short the nested command re-declared",
8912        );
8913    }
8914
8915    #[test]
8916    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8917        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
8918        // into the command the mount sits on. They belong to the mounted program, so they must
8919        // be offered inside the mounted commands rather than filtered out with the mounting
8920        // CLI's own flags.
8921        let mut task = SpecCommand::builder()
8922            .name("task")
8923            .flag(
8924                SpecFlag::builder()
8925                    .name("bump")
8926                    .long("bump")
8927                    .global(false)
8928                    .build(),
8929            )
8930            .build();
8931        task.mark_mounted();
8932
8933        let mut run_cmd = SpecCommand::builder().name("run").build();
8934        run_cmd.subcommands.insert("task".to_string(), task);
8935        // What `mount()` leaves behind when the mounted spec's root declares flags.
8936        run_cmd.flags = vec![
8937            SpecFlag::builder()
8938                .name("tglobal")
8939                .long("tglobal")
8940                .global(true)
8941                .build(),
8942            SpecFlag::builder()
8943                .name("tlocal")
8944                .long("tlocal")
8945                .global(false)
8946                .build(),
8947        ];
8948        run_cmd.flags_from_mount = true;
8949
8950        let mut cmd = SpecCommand::builder()
8951            .name("test")
8952            .flag(
8953                SpecFlag::builder()
8954                    .name("silent")
8955                    .long("silent")
8956                    .global(true)
8957                    .build(),
8958            )
8959            .build();
8960        cmd.subcommands.insert("run".to_string(), run_cmd);
8961        let spec = Spec {
8962            name: "test".to_string(),
8963            bin: "test".to_string(),
8964            cmd,
8965            ..Default::default()
8966        };
8967
8968        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8969        assert_eq!(
8970            parsed.completion_flags().keys().collect::<Vec<_>>(),
8971            vec!["--bump", "--tglobal"],
8972            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8973             `--silent` does not, and the mount's non-global root flag is not inherited",
8974        );
8975    }
8976
8977    #[test]
8978    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8979        // Regression for jdx/mise#11282. A mounted command describes another program, which
8980        // does not accept the mounting CLI's globals (mise forwards everything after a task
8981        // name to the task). They must stay recognized — they may appear before the mounted
8982        // command — but must not be offered in completions there.
8983        let spec = mounted_task_flag_spec();
8984        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8985
8986        // Still recognized for parsing...
8987        assert!(parsed.available_flags.contains_key("--silent"));
8988        assert!(parsed.available_flags.contains_key("-E"));
8989        // ...but belonging to a command above the mount, so not offered.
8990        assert_eq!(
8991            parsed.completion_flags().keys().collect::<Vec<_>>(),
8992            vec!["--bump", "--env"],
8993            "only the mounted command's own flags may be offered",
8994        );
8995
8996        // `run`'s own non-global flag is dropped on descent, as it always was.
8997        assert!(!parsed.available_flags.contains_key("--force"));
8998    }
8999
9000    #[test]
9001    fn test_mounted_cmd_flag_wins_over_inherited_global() {
9002        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
9003        // by the root's `--env` global, so completing its value fell back to file completion.
9004        let spec = mounted_task_flag_spec();
9005        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
9006
9007        let awaiting = parsed
9008            .flag_awaiting_value
9009            .first()
9010            .expect("--env should await a value");
9011        assert_eq!(
9012            awaiting
9013                .arg
9014                .as_ref()
9015                .and_then(|a| a.choices.as_ref())
9016                .map(|c| c.choices.clone()),
9017            Some(vec![
9018                "dev".to_string(),
9019                "stage".to_string(),
9020                "prod".to_string()
9021            ]),
9022            "the mounted command's own --env must win over the inherited global",
9023        );
9024
9025        // The global's short is not declared by the mounted command, so it keeps pointing at
9026        // the global and a value passed before the mounted command still parses.
9027        let parsed =
9028            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
9029        assert!(
9030            parsed.args.is_empty(),
9031            "prefix global tokens must not be consumed as positionals, got {:?}",
9032            parsed.args
9033        );
9034        assert_eq!(
9035            parsed.as_env().get("usage_env").map(String::as_str),
9036            Some("anything"),
9037        );
9038    }
9039
9040    #[test]
9041    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
9042        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
9043        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
9044        // global's value would be validated against the mounted flag's choices and a legitimate
9045        // value would be rejected.
9046        let spec = mounted_task_flag_spec();
9047        let parsed = parse_partial(
9048            &spec,
9049            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
9050        )
9051        .unwrap();
9052        assert!(
9053            parsed.errors.is_empty(),
9054            "prefix global value must not be validated against the mounted flag: {:?}",
9055            parsed
9056                .errors
9057                .iter()
9058                .map(|e| e.to_string())
9059                .collect::<Vec<_>>(),
9060        );
9061        assert_eq!(
9062            parsed.as_env().get("usage_env").map(String::as_str),
9063            Some("not-a-task-choice"),
9064        );
9065
9066        // The embedded-value form binds the same way.
9067        let parsed = parse_partial(
9068            &spec,
9069            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
9070        )
9071        .unwrap();
9072        assert!(parsed.errors.is_empty());
9073        assert_eq!(
9074            parsed.as_env().get("usage_env").map(String::as_str),
9075            Some("not-a-task-choice"),
9076        );
9077
9078        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
9079        // the same name was already used before it.
9080        let parsed = parse_partial(
9081            &spec,
9082            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
9083        )
9084        .unwrap();
9085        let awaiting = parsed
9086            .flag_awaiting_value
9087            .first()
9088            .expect("--env should await a value");
9089        assert_eq!(
9090            awaiting
9091                .arg
9092                .as_ref()
9093                .and_then(|a| a.choices.as_ref())
9094                .map(|c| c.choices.clone()),
9095            Some(vec![
9096                "dev".to_string(),
9097                "stage".to_string(),
9098                "prod".to_string()
9099            ]),
9100            "the mounted command's --env must own the name after the mounted command",
9101        );
9102    }
9103
9104    #[test]
9105    fn test_non_global_flag_does_not_hide_subcommand() {
9106        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
9107        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
9108        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
9109        let spec = mounted_task_flag_spec();
9110
9111        for words in [
9112            // `run` declares `-f/--force` as non-global.
9113            &["test", "run", "--force", "mytask"][..],
9114            &["test", "run", "-f", "mytask"][..],
9115            // Mixed with a global before the subcommand.
9116            &["test", "-E", "prod", "run", "--force", "mytask"][..],
9117        ] {
9118            let parsed = parse_partial(&spec, &input(words)).unwrap();
9119            assert_eq!(
9120                parsed
9121                    .cmds
9122                    .iter()
9123                    .map(|c| c.name.as_str())
9124                    .collect::<Vec<_>>(),
9125                vec!["test", "run", "mytask"],
9126                "{words:?} should descend into the mounted command",
9127            );
9128            assert!(
9129                parsed.args.is_empty(),
9130                "{words:?} should not consume a positional, got {:?}",
9131                parsed.args,
9132            );
9133            assert_eq!(
9134                parsed.as_env().get("usage_force").map(String::as_str),
9135                Some("true"),
9136                "the non-global flag must still be recorded for {words:?}",
9137            );
9138        }
9139
9140        // A non-global flag that takes a value consumes it, rather than reading the value as the
9141        // subcommand.
9142        let mut run_cmd = SpecCommand::builder()
9143            .name("run")
9144            .flag(
9145                SpecFlag::builder()
9146                    .name("output")
9147                    .short('o')
9148                    .long("output")
9149                    .arg(SpecArg::builder().name("mode").build())
9150                    .global(false)
9151                    .build(),
9152            )
9153            .build();
9154        run_cmd.subcommands.insert(
9155            "task".to_string(),
9156            SpecCommand::builder().name("task").build(),
9157        );
9158        let mut cmd = SpecCommand::builder().name("test").build();
9159        cmd.subcommands.insert("run".to_string(), run_cmd);
9160        let spec = Spec {
9161            name: "test".to_string(),
9162            bin: "test".to_string(),
9163            cmd,
9164            ..Default::default()
9165        };
9166
9167        let parsed =
9168            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
9169        assert_eq!(
9170            parsed
9171                .cmds
9172                .iter()
9173                .map(|c| c.name.as_str())
9174                .collect::<Vec<_>>(),
9175            vec!["test", "run", "task"],
9176        );
9177        assert_eq!(
9178            parsed.as_env().get("usage_output").map(String::as_str),
9179            Some("quiet"),
9180        );
9181
9182        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
9183        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
9184        assert_parse_err(
9185            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
9186            "unexpected word: --nope",
9187        );
9188    }
9189
9190    #[test]
9191    fn test_non_mounted_subcommand_offers_inherited_globals() {
9192        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
9193        // still both recognized and offered.
9194        let mut run_cmd = SpecCommand::builder().name("run").build();
9195        run_cmd.subcommands.insert(
9196            "nested".to_string(),
9197            SpecCommand::builder().name("nested").build(),
9198        );
9199        let mut cmd = SpecCommand::builder()
9200            .name("test")
9201            .flag(
9202                SpecFlag::builder()
9203                    .name("silent")
9204                    .long("silent")
9205                    .global(true)
9206                    .build(),
9207            )
9208            .build();
9209        cmd.subcommands.insert("run".to_string(), run_cmd);
9210        let spec = Spec {
9211            name: "test".to_string(),
9212            bin: "test".to_string(),
9213            cmd,
9214            ..Default::default()
9215        };
9216
9217        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
9218        assert_eq!(
9219            parsed.completion_flags().keys().collect::<Vec<_>>(),
9220            parsed.available_flags.keys().collect::<Vec<_>>(),
9221        );
9222        assert!(parsed.completion_flags().contains_key("--silent"));
9223    }
9224
9225    #[test]
9226    fn test_subcommand_alias_collision_keeps_last_owner() {
9227        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
9228        // share an alias are resolved. Historically the flattened flag map gave the shared
9229        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
9230        let run_cmd = SpecCommand::builder()
9231            .name("run")
9232            .flag(
9233                SpecFlag::builder()
9234                    .name("alpha")
9235                    .short('x')
9236                    .long("alpha")
9237                    .global(false)
9238                    .build(),
9239            )
9240            .flag(
9241                SpecFlag::builder()
9242                    .name("beta")
9243                    .short('x')
9244                    .long("beta")
9245                    .global(false)
9246                    .build(),
9247            )
9248            .build();
9249        let mut cmd = SpecCommand::builder().name("test").build();
9250        cmd.subcommands.insert("run".to_string(), run_cmd);
9251        let spec = Spec {
9252            name: "test".to_string(),
9253            bin: "test".to_string(),
9254            cmd,
9255            ..Default::default()
9256        };
9257
9258        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
9259        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
9260        assert_eq!(
9261            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
9262            Some("beta"),
9263            "the last-declared flag must keep a shared short alias",
9264        );
9265        // Both distinct long aliases remain recognized and point to their own flag.
9266        assert_eq!(
9267            parsed
9268                .available_flags
9269                .get("--alpha")
9270                .map(|f| f.name.as_str()),
9271            Some("alpha"),
9272        );
9273        assert_eq!(
9274            parsed
9275                .available_flags
9276                .get("--beta")
9277                .map(|f| f.name.as_str()),
9278            Some("beta"),
9279        );
9280    }
9281
9282    #[test]
9283    fn test_default_subcommand_same_name_child() {
9284        // Test that default_subcommand doesn't cause issues when the default subcommand
9285        // has a child with the same name (e.g., "run" has a task named "run").
9286        // This verifies we don't switch multiple times or get stuck in a loop.
9287        let run_task = SpecCommand::builder()
9288            .name("run")
9289            .arg(SpecArg::builder().name("args").build())
9290            .build();
9291        let mut run_cmd = SpecCommand::builder().name("run").build();
9292        run_cmd.subcommands.insert("run".to_string(), run_task);
9293
9294        let mut cmd = SpecCommand::builder().name("test").build();
9295        cmd.subcommands.insert("run".to_string(), run_cmd);
9296
9297        let spec = Spec {
9298            name: "test".to_string(),
9299            bin: "test".to_string(),
9300            cmd,
9301            default_subcommand: Some("run".to_string()),
9302            ..Default::default()
9303        };
9304
9305        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
9306        let input = vec!["test".to_string(), "run".to_string()];
9307        let parsed = parse(&spec, &input).unwrap();
9308
9309        // Should have two commands: root and "run"
9310        assert_eq!(parsed.cmds.len(), 2);
9311        assert_eq!(parsed.cmds[0].name, "test");
9312        assert_eq!(parsed.cmds[1].name, "run");
9313
9314        // "test run run" should descend into the "run" task (child of "run" subcommand)
9315        let input = vec![
9316            "test".to_string(),
9317            "run".to_string(),
9318            "run".to_string(),
9319            "hello".to_string(),
9320        ];
9321        let parsed = parse(&spec, &input).unwrap();
9322
9323        assert_eq!(parsed.cmds.len(), 3);
9324        assert_eq!(parsed.cmds[0].name, "test");
9325        assert_eq!(parsed.cmds[1].name, "run");
9326        assert_eq!(parsed.cmds[2].name, "run");
9327        assert_eq!(parsed.args.len(), 1);
9328        let value = parsed.args.values().next().unwrap();
9329        assert_eq!(value.to_string(), "hello");
9330
9331        // Key test case: "test other" should switch to default subcommand "run"
9332        // and treat "other" as a positional arg (not try to switch again because
9333        // "run" also has a "run" child).
9334        let mut run_cmd = SpecCommand::builder()
9335            .name("run")
9336            .arg(SpecArg::builder().name("task").build())
9337            .build();
9338        let run_task = SpecCommand::builder().name("run").build();
9339        run_cmd.subcommands.insert("run".to_string(), run_task);
9340
9341        let mut cmd = SpecCommand::builder().name("test").build();
9342        cmd.subcommands.insert("run".to_string(), run_cmd);
9343
9344        let spec = Spec {
9345            name: "test".to_string(),
9346            bin: "test".to_string(),
9347            cmd,
9348            default_subcommand: Some("run".to_string()),
9349            ..Default::default()
9350        };
9351
9352        let input = vec!["test".to_string(), "other".to_string()];
9353        let parsed = parse(&spec, &input).unwrap();
9354
9355        // Should have two commands: root and "run" (the default)
9356        // We should NOT have switched again to the "run" task child
9357        assert_eq!(parsed.cmds.len(), 2);
9358        assert_eq!(parsed.cmds[0].name, "test");
9359        assert_eq!(parsed.cmds[1].name, "run");
9360
9361        // "other" should be parsed as a positional arg
9362        assert_eq!(parsed.args.len(), 1);
9363        let value = parsed.args.values().next().unwrap();
9364        assert_eq!(value.to_string(), "other");
9365    }
9366
9367    #[test]
9368    fn test_restart_token() {
9369        // Test that restart_token resets argument parsing
9370        let run_cmd = SpecCommand::builder()
9371            .name("run")
9372            .arg(SpecArg::builder().name("task").build())
9373            .restart_token(":::".to_string())
9374            .build();
9375        let mut cmd = SpecCommand::builder().name("test").build();
9376        cmd.subcommands.insert("run".to_string(), run_cmd);
9377
9378        let spec = Spec {
9379            name: "test".to_string(),
9380            bin: "test".to_string(),
9381            cmd,
9382            ..Default::default()
9383        };
9384
9385        // "test run task1 ::: task2" - should end up with task2 as the arg
9386        let input = vec![
9387            "test".to_string(),
9388            "run".to_string(),
9389            "task1".to_string(),
9390            ":::".to_string(),
9391            "task2".to_string(),
9392        ];
9393        let parsed = parse(&spec, &input).unwrap();
9394
9395        // After restart, args were cleared and task2 was parsed
9396        assert_eq!(parsed.args.len(), 1);
9397        let value = parsed.args.values().next().unwrap();
9398        assert_eq!(value.to_string(), "task2");
9399    }
9400
9401    #[test]
9402    fn test_restart_token_multiple() {
9403        // Test multiple restart tokens
9404        let run_cmd = SpecCommand::builder()
9405            .name("run")
9406            .arg(SpecArg::builder().name("task").build())
9407            .restart_token(":::".to_string())
9408            .build();
9409        let mut cmd = SpecCommand::builder().name("test").build();
9410        cmd.subcommands.insert("run".to_string(), run_cmd);
9411
9412        let spec = Spec {
9413            name: "test".to_string(),
9414            bin: "test".to_string(),
9415            cmd,
9416            ..Default::default()
9417        };
9418
9419        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
9420        let input = vec![
9421            "test".to_string(),
9422            "run".to_string(),
9423            "task1".to_string(),
9424            ":::".to_string(),
9425            "task2".to_string(),
9426            ":::".to_string(),
9427            "task3".to_string(),
9428        ];
9429        let parsed = parse(&spec, &input).unwrap();
9430
9431        // After multiple restarts, args were cleared and task3 was parsed
9432        assert_eq!(parsed.args.len(), 1);
9433        let value = parsed.args.values().next().unwrap();
9434        assert_eq!(value.to_string(), "task3");
9435    }
9436
9437    #[test]
9438    fn test_restart_token_clears_flag_awaiting_value() {
9439        // Test that restart_token clears pending flag values
9440        let run_cmd = SpecCommand::builder()
9441            .name("run")
9442            .arg(SpecArg::builder().name("task").build())
9443            .flag(
9444                SpecFlag::builder()
9445                    .name("jobs")
9446                    .long("jobs")
9447                    .arg(SpecArg::builder().name("count").build())
9448                    .build(),
9449            )
9450            .restart_token(":::".to_string())
9451            .build();
9452        let mut cmd = SpecCommand::builder().name("test").build();
9453        cmd.subcommands.insert("run".to_string(), run_cmd);
9454
9455        let spec = Spec {
9456            name: "test".to_string(),
9457            bin: "test".to_string(),
9458            cmd,
9459            ..Default::default()
9460        };
9461
9462        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
9463        let input = vec![
9464            "test".to_string(),
9465            "run".to_string(),
9466            "task1".to_string(),
9467            "--jobs".to_string(),
9468            ":::".to_string(),
9469            "task2".to_string(),
9470        ];
9471        let parsed = parse(&spec, &input).unwrap();
9472
9473        // task2 should be parsed as the task arg, not as --jobs value
9474        assert_eq!(parsed.args.len(), 1);
9475        let value = parsed.args.values().next().unwrap();
9476        assert_eq!(value.to_string(), "task2");
9477        // --jobs should not have a value
9478        assert!(parsed.flag_awaiting_value.is_empty());
9479    }
9480
9481    #[test]
9482    fn test_restart_token_resets_double_dash() {
9483        // Test that restart_token resets the -- separator effect
9484        let run_cmd = SpecCommand::builder()
9485            .name("run")
9486            .arg(SpecArg::builder().name("task").build())
9487            .arg(SpecArg::builder().name("extra_args").var(true).build())
9488            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9489            .restart_token(":::".to_string())
9490            .build();
9491        let mut cmd = SpecCommand::builder().name("test").build();
9492        cmd.subcommands.insert("run".to_string(), run_cmd);
9493
9494        let spec = Spec {
9495            name: "test".to_string(),
9496            bin: "test".to_string(),
9497            cmd,
9498            ..Default::default()
9499        };
9500
9501        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
9502        let input = vec![
9503            "test".to_string(),
9504            "run".to_string(),
9505            "task1".to_string(),
9506            "--".to_string(),
9507            "extra".to_string(),
9508            ":::".to_string(),
9509            "--verbose".to_string(),
9510            "task2".to_string(),
9511        ];
9512        let parsed = parse(&spec, &input).unwrap();
9513
9514        // --verbose should be parsed as a flag (not an arg) after the restart
9515        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9516        // task2 should be the arg after restart
9517        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9518        let value = parsed.args.get(task_arg).unwrap();
9519        assert_eq!(value.to_string(), "task2");
9520    }
9521
9522    #[test]
9523    fn test_double_dashes_without_preserve() {
9524        // Only the first `--` is a separator; a later one is a value, because flag
9525        // parsing has already stopped and there is nothing left for it to do.
9526        // `preserve` is about the *first* one — see the test below, where none is
9527        // consumed at all.
9528        let run_cmd = SpecCommand::builder()
9529            .name("run")
9530            .arg(SpecArg::builder().name("args").var(true).build())
9531            .build();
9532        let mut cmd = SpecCommand::builder().name("test").build();
9533        cmd.subcommands.insert("run".to_string(), run_cmd);
9534
9535        let spec = Spec {
9536            name: "test".to_string(),
9537            bin: "test".to_string(),
9538            cmd,
9539            ..Default::default()
9540        };
9541
9542        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
9543        let input = vec![
9544            "test".to_string(),
9545            "run".to_string(),
9546            "arg1".to_string(),
9547            "--".to_string(),
9548            "arg2".to_string(),
9549            "--".to_string(),
9550            "arg3".to_string(),
9551        ];
9552        let parsed = parse(&spec, &input).unwrap();
9553
9554        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9555        let value = parsed.args.get(args_arg).unwrap();
9556        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9557    }
9558
9559    #[test]
9560    fn test_double_dashes_with_preserve() {
9561        // Test that variadic args WITH `preserve` keep all double dashes
9562        let run_cmd = SpecCommand::builder()
9563            .name("run")
9564            .arg(
9565                SpecArg::builder()
9566                    .name("args")
9567                    .var(true)
9568                    .double_dash(SpecDoubleDashChoices::Preserve)
9569                    .build(),
9570            )
9571            .build();
9572        let mut cmd = SpecCommand::builder().name("test").build();
9573        cmd.subcommands.insert("run".to_string(), run_cmd);
9574
9575        let spec = Spec {
9576            name: "test".to_string(),
9577            bin: "test".to_string(),
9578            cmd,
9579            ..Default::default()
9580        };
9581
9582        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
9583        let input = vec![
9584            "test".to_string(),
9585            "run".to_string(),
9586            "arg1".to_string(),
9587            "--".to_string(),
9588            "arg2".to_string(),
9589            "--".to_string(),
9590            "arg3".to_string(),
9591        ];
9592        let parsed = parse(&spec, &input).unwrap();
9593
9594        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9595        let value = parsed.args.get(args_arg).unwrap();
9596        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9597    }
9598
9599    #[test]
9600    fn test_double_dashes_with_preserve_only_dashes() {
9601        // Test that variadic args WITH `preserve` keep all double dashes even
9602        // if the values are just double dashes
9603        let run_cmd = SpecCommand::builder()
9604            .name("run")
9605            .arg(
9606                SpecArg::builder()
9607                    .name("args")
9608                    .var(true)
9609                    .double_dash(SpecDoubleDashChoices::Preserve)
9610                    .build(),
9611            )
9612            .build();
9613        let mut cmd = SpecCommand::builder().name("test").build();
9614        cmd.subcommands.insert("run".to_string(), run_cmd);
9615
9616        let spec = Spec {
9617            name: "test".to_string(),
9618            bin: "test".to_string(),
9619            cmd,
9620            ..Default::default()
9621        };
9622
9623        // "test run -- --" - all double dashes should be preserved
9624        let input = vec![
9625            "test".to_string(),
9626            "run".to_string(),
9627            "--".to_string(),
9628            "--".to_string(),
9629        ];
9630        let parsed = parse(&spec, &input).unwrap();
9631
9632        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9633        let value = parsed.args.get(args_arg).unwrap();
9634        assert_eq!(value.to_string(), "-- --");
9635    }
9636
9637    #[test]
9638    fn test_double_dashes_with_preserve_multiple_args() {
9639        // Test with multiple args where only the second has has `preserve`
9640        let run_cmd = SpecCommand::builder()
9641            .name("run")
9642            .arg(SpecArg::builder().name("task").build())
9643            .arg(
9644                SpecArg::builder()
9645                    .name("extra_args")
9646                    .var(true)
9647                    .double_dash(SpecDoubleDashChoices::Preserve)
9648                    .build(),
9649            )
9650            .build();
9651        let mut cmd = SpecCommand::builder().name("test").build();
9652        cmd.subcommands.insert("run".to_string(), run_cmd);
9653
9654        let spec = Spec {
9655            name: "test".to_string(),
9656            bin: "test".to_string(),
9657            cmd,
9658            ..Default::default()
9659        };
9660
9661        // The first arg "task1" is captured normally
9662        // Then extra_args with `preserve` captures everything, including the "--" tokens
9663        let input = vec![
9664            "test".to_string(),
9665            "run".to_string(),
9666            "task1".to_string(),
9667            "--".to_string(),
9668            "arg1".to_string(),
9669            "--".to_string(),
9670            "--foo".to_string(),
9671        ];
9672        let parsed = parse(&spec, &input).unwrap();
9673
9674        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9675        let task_value = parsed.args.get(task_arg).unwrap();
9676        assert_eq!(task_value.to_string(), "task1");
9677
9678        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9679        let extra_value = parsed.args.get(extra_arg).unwrap();
9680        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9681    }
9682
9683    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9684        let cmd = SpecCommand::builder().name("test").args(args).build();
9685        Spec {
9686            name: "test".to_string(),
9687            bin: "test".to_string(),
9688            cmd,
9689            ..Default::default()
9690        }
9691    }
9692
9693    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9694        let arg = parsed
9695            .args
9696            .keys()
9697            .find(|a| a.name == name)
9698            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9699        parsed.args.get(arg).unwrap().to_string()
9700    }
9701
9702    fn required_arg(name: &str) -> SpecArg {
9703        SpecArg::builder()
9704            .name(name)
9705            .var(true)
9706            .required(false)
9707            .double_dash(SpecDoubleDashChoices::Required)
9708            .build()
9709    }
9710
9711    #[test]
9712    fn test_double_dash_required_reports_error_once_for_variadic() {
9713        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9714        let spec = spec_with_args([required_arg("files")]);
9715
9716        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9717
9718        assert!(parsed.args.is_empty());
9719        assert_eq!(parsed.errors.len(), 1);
9720        assert!(
9721            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9722        );
9723    }
9724
9725    #[test]
9726    fn test_double_dash_required_suppresses_missing_arg() {
9727        // The arg is never filled, so the end-of-parse check would also call it missing.
9728        let spec = spec_with_args([SpecArg::builder()
9729            .name("file")
9730            .required(true)
9731            .double_dash(SpecDoubleDashChoices::Required)
9732            .build()]);
9733
9734        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9735
9736        assert_eq!(parsed.errors.len(), 1);
9737        assert!(matches!(
9738            &parsed.errors[0],
9739            UsageErr::ArgRequiresDoubleDash(_)
9740        ));
9741        // The cursor stays put, so a completion keeps offering the same arg.
9742        assert_eq!(
9743            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9744            Some("file")
9745        );
9746        assert!(!parsed.double_dash_seen);
9747    }
9748
9749    #[test]
9750    fn test_double_dash_routes_to_required_arg() {
9751        // Everything after `--` belongs to the arg that requires it, even though the greedy
9752        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9753        let spec = spec_with_args([
9754            SpecArg::builder()
9755                .name("tool")
9756                .var(true)
9757                .required(false)
9758                .build(),
9759            required_arg("command"),
9760        ]);
9761
9762        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9763
9764        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9765        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9766        assert!(parsed.double_dash_seen);
9767    }
9768
9769    #[test]
9770    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9771        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9772        // "is it filled?" check cannot be a count of how many args were filled.
9773        let spec = spec_with_args([
9774            SpecArg::builder()
9775                .name("tool")
9776                .var(true)
9777                .required(true)
9778                .build(),
9779            required_arg("command"),
9780        ]);
9781
9782        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9783
9784        assert_eq!(arg_value(&parsed, "command"), "ls");
9785        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9786        assert!(
9787            parsed
9788                .errors
9789                .iter()
9790                .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool"))
9791        );
9792    }
9793
9794    #[test]
9795    fn test_double_dash_gap_applies_defaults() {
9796        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9797        let spec = spec_with_args([
9798            SpecArg::builder()
9799                .name("tool")
9800                .var(true)
9801                .required(false)
9802                .default_value("node@20")
9803                .build(),
9804            required_arg("command"),
9805        ]);
9806
9807        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9808
9809        assert_eq!(arg_value(&parsed, "command"), "ls");
9810        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9811    }
9812
9813    fn spec_with_restart_token_and_required_arg() -> Spec {
9814        let run_cmd = SpecCommand::builder()
9815            .name("run")
9816            .arg(SpecArg::builder().name("task").build())
9817            .arg(required_arg("run_args"))
9818            .restart_token(":::".to_string())
9819            .build();
9820        let mut cmd = SpecCommand::builder().name("test").build();
9821        cmd.subcommands.insert("run".to_string(), run_cmd);
9822        Spec {
9823            name: "test".to_string(),
9824            bin: "test".to_string(),
9825            cmd,
9826            ..Default::default()
9827        }
9828    }
9829
9830    #[test]
9831    fn test_double_dash_required_restart_token_resets_separator() {
9832        // The `--` before `:::` belongs to the previous invocation only.
9833        let spec = spec_with_restart_token_and_required_arg();
9834
9835        let parsed = parse_partial(
9836            &spec,
9837            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9838        )
9839        .unwrap();
9840
9841        assert_eq!(arg_value(&parsed, "task"), "task2");
9842        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9843        // Reported once even though the arg was violated after already succeeding once.
9844        assert_eq!(
9845            parsed
9846                .errors
9847                .iter()
9848                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9849                .count(),
9850            1
9851        );
9852    }
9853
9854    #[test]
9855    fn test_double_dash_required_restart_token_accepts_new_separator() {
9856        let spec = spec_with_restart_token_and_required_arg();
9857
9858        let parsed = parse(
9859            &spec,
9860            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9861        )
9862        .unwrap();
9863
9864        assert_eq!(arg_value(&parsed, "task"), "task2");
9865        assert_eq!(arg_value(&parsed, "run_args"), "c");
9866    }
9867
9868    #[test]
9869    fn test_double_dash_preserve_is_not_a_separator() {
9870        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9871        // arg that requires a separator. Deliberate: one token cannot be both.
9872        let spec = spec_with_args([
9873            SpecArg::builder()
9874                .name("kept")
9875                .var(true)
9876                .var_max(1)
9877                .required(false)
9878                .double_dash(SpecDoubleDashChoices::Preserve)
9879                .build(),
9880            required_arg("rest"),
9881        ]);
9882
9883        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9884
9885        assert_eq!(arg_value(&parsed, "kept"), "--");
9886        assert!(parsed.args.keys().all(|a| a.name != "rest"));
9887        assert!(!parsed.double_dash_seen);
9888        assert_eq!(parsed.errors.len(), 1);
9889    }
9890
9891    #[test]
9892    fn test_double_dash_required_does_not_bail_in_parse_partial() {
9893        // Completions parse half-typed command lines; they must still get a result.
9894        let spec = spec_with_args([required_arg("file")]);
9895
9896        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9897        assert!(parse(&spec, &input(&["test", "x"])).is_err());
9898    }
9899
9900    #[test]
9901    fn test_double_dash_without_required_arg_does_not_move_cursor() {
9902        // Specs with no `double_dash="required"` arg are untouched by the jump.
9903        let spec = spec_with_args([
9904            SpecArg::builder().name("first").required(false).build(),
9905            SpecArg::builder().name("second").required(false).build(),
9906        ]);
9907
9908        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9909
9910        assert_eq!(arg_value(&parsed, "first"), "a");
9911        assert_eq!(arg_value(&parsed, "second"), "b");
9912        assert!(parsed.next_arg.is_none());
9913    }
9914
9915    #[test]
9916    fn test_parser_with_custom_env_for_required_arg() {
9917        let spec = spec_with_arg(
9918            SpecArg::builder()
9919                .name("name")
9920                .env("NAME")
9921                .required(true)
9922                .build(),
9923        );
9924        // FIXME: Audit that the environment access only happens in single-threaded code.
9925        unsafe { std::env::remove_var("NAME") };
9926
9927        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9928            .expect("parse should succeed with custom env");
9929        assert_eq!(parsed.args.len(), 1);
9930        assert_eq!(first_string_value(&parsed), "john");
9931    }
9932
9933    #[test]
9934    fn test_parser_with_custom_env_for_required_flag() {
9935        let spec = spec_with_flag(
9936            SpecFlag::builder()
9937                .long("name")
9938                .env("NAME")
9939                .required(true)
9940                .arg(SpecArg::builder().name("name").build())
9941                .build(),
9942        );
9943        // FIXME: Audit that the environment access only happens in single-threaded code.
9944        unsafe { std::env::remove_var("NAME") };
9945
9946        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9947            .expect("parse should succeed with custom env");
9948        assert_eq!(parsed.flags.len(), 1);
9949        assert_eq!(first_string_value(&parsed), "jane");
9950    }
9951
9952    #[test]
9953    fn test_flag_environment_fallbacks_preserve_declaration_order() {
9954        let spec = spec_with_flag(
9955            SpecFlag::builder()
9956                .long("name")
9957                .env("NAME")
9958                .env_fallback("OLD_NAME")
9959                .env_fallback("OLDER_NAME")
9960                .deprecated_env("DEPRECATED_NAME")
9961                .arg(SpecArg::builder().name("name").build())
9962                .build(),
9963        );
9964
9965        let parsed = parse_with_env(
9966            &spec,
9967            &["test"],
9968            &[
9969                ("NAME", "canonical"),
9970                ("OLD_NAME", "fallback"),
9971                ("DEPRECATED_NAME", "deprecated"),
9972            ],
9973        )
9974        .unwrap();
9975        assert_eq!(first_string_value(&parsed), "canonical");
9976
9977        let parsed = parse_with_env(
9978            &spec,
9979            &["test"],
9980            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9981        )
9982        .unwrap();
9983        assert_eq!(first_string_value(&parsed), "old");
9984
9985        let parsed =
9986            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9987        assert_eq!(first_string_value(&parsed), "deprecated");
9988    }
9989
9990    #[test]
9991    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9992        let spec = spec_with_flag(
9993            SpecFlag::builder()
9994                .long("name")
9995                .env("NAME")
9996                .deprecated_env("DEPRECATED_NAME")
9997                .arg(SpecArg::builder().name("name").build())
9998                .build(),
9999        );
10000
10001        // The current name is not a deprecated one, and says nothing.
10002        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
10003        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10004
10005        let parsed =
10006            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
10007        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10008        assert_eq!(
10009            parsed.warnings[0].kind,
10010            crate::warn::WarningKind::DeprecatedEnv
10011        );
10012        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
10013        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
10014        // Reported, not printed, and the value still arrives.
10015        assert_eq!(first_string_value(&parsed), "deprecated");
10016    }
10017
10018    #[test]
10019    fn a_deprecated_flag_reports_only_when_it_was_used() {
10020        let spec = spec_with_flag(
10021            SpecFlag::builder()
10022                .long("output")
10023                .deprecated("use --out")
10024                .deprecated_remove_at("3.0.0")
10025                .arg(SpecArg::builder().name("output").build())
10026                .build(),
10027        );
10028
10029        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
10030        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10031
10032        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10033        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10034        assert_eq!(
10035            parsed.warnings[0].kind,
10036            crate::warn::WarningKind::DeprecatedFlag
10037        );
10038        // Named the way it was typed, dashes and all.
10039        assert_eq!(parsed.warnings[0].name, "--output");
10040        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
10041        assert_eq!(
10042            parsed.warnings[0].render(),
10043            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
10044        );
10045    }
10046
10047    #[test]
10048    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
10049        let flag = SpecFlag::builder()
10050            .long("output")
10051            .deprecated("use --out")
10052            .deprecated_warn_at("9.0.0")
10053            .arg(SpecArg::builder().name("output").build())
10054            .build();
10055        let mut spec = spec_with_flag(flag);
10056        spec.version = Some("2.0.0".to_string());
10057
10058        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10059        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10060
10061        // And once the CLI is the release that was named, it speaks up.
10062        spec.version = Some("9.0.0".to_string());
10063        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10064        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10065    }
10066
10067    #[test]
10068    fn test_parser_with_custom_env_still_fails_when_missing() {
10069        let spec = spec_with_arg(
10070            SpecArg::builder()
10071                .name("name")
10072                .env("NAME")
10073                .required(true)
10074                .build(),
10075        );
10076        // FIXME: Audit that the environment access only happens in single-threaded code.
10077        unsafe { std::env::remove_var("NAME") };
10078        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
10079    }
10080
10081    #[test]
10082    fn test_parser_does_not_treat_env_choice_value_as_help() {
10083        let spec = spec_with_arg(
10084            SpecArg::builder()
10085                .name("env")
10086                .env("CURRENT_ENV")
10087                .choices(["dev", "staging"])
10088                .required(false)
10089                .build(),
10090        );
10091
10092        assert_parse_err(
10093            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
10094            "Invalid choice for arg env: --help, expected one of dev, staging",
10095        );
10096    }
10097
10098    #[test]
10099    fn test_parser_does_not_treat_default_choice_value_as_help() {
10100        let spec = spec_with_flag(
10101            SpecFlag::builder()
10102                .long("env")
10103                .arg(
10104                    SpecArg::builder()
10105                        .name("env")
10106                        .choices(["dev", "staging"])
10107                        .build(),
10108                )
10109                .default_value("--help")
10110                .build(),
10111        );
10112
10113        assert_parse_err(
10114            parse_with_env(&spec, &["test"], &[]),
10115            "Invalid choice for option env: --help, expected one of dev, staging",
10116        );
10117    }
10118
10119    /// argv as `parse` wants it, program name included.
10120    fn words(of: &[&str]) -> Vec<String> {
10121        of.iter().map(|s| s.to_string()).collect()
10122    }
10123
10124    #[test]
10125    fn a_command_that_needs_a_subcommand_says_so() {
10126        // The spec has carried `subcommand_required` since the derive needed it, and this parser
10127        // never read it — so `mise generate`, which declares it, parsed as a complete
10128        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
10129        let spec: Spec = r#"
10130name "ex"
10131bin "ex"
10132cmd "gen" subcommand_required=#true {
10133    cmd "two" {}
10134    cmd "one" {}
10135    cmd "secret" hide=#true {}
10136    alias "g"
10137}
10138cmd "open" {
10139    cmd "sub" {}
10140}
10141"#
10142        .parse()
10143        .unwrap();
10144
10145        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
10146        // Sorted, so the message does not depend on map order; hidden commands left out,
10147        // because a message telling someone to type a hidden name is worse than a vague one;
10148        // and the alias not listed beside the name it points at.
10149        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
10150
10151        // Reached through its alias, and still about the command rather than the spelling.
10152        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
10153        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
10154
10155        // Given one: fine.
10156        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
10157
10158        // And a command that has subcommands without declaring them required is untouched —
10159        // this is the half that keeps the check from being "any command with children".
10160        parse(&spec, &words(&["ex", "open"])).unwrap();
10161        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
10162    }
10163
10164    #[test]
10165    fn arg_required_else_help_observes_the_selected_commands_argv() {
10166        let spec: Spec = r#"
10167name "ex"
10168bin "ex"
10169flag "--verbose" global=#true
10170cmd "run" arg_required_else_help=#true {
10171    flag "--all"
10172}
10173"#
10174        .parse()
10175        .unwrap();
10176        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10177
10178        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
10179        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10180
10181        // A global before the command belongs to the ancestor. It selected `run`, but did not
10182        // give `run` an argument of its own.
10183        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
10184        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10185
10186        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
10187    }
10188
10189    #[test]
10190    fn empty_default_with_parent_flags_observes_child_argv() {
10191        let spec: Spec = r#"
10192name "ex"
10193bin "ex"
10194default_subcommand "run"
10195default_subcommand_on_empty #true
10196flag "--verbose" global=#true
10197cmd "run" arg_required_else_help=#true {}
10198"#
10199        .parse()
10200        .unwrap();
10201        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10202        let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
10203        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10204    }
10205
10206    #[test]
10207    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
10208        let spec: Spec = r#"
10209name "ex"
10210bin "ex"
10211unknown_flags "error"
10212external_subcommand #true
10213cmd "install"
10214flag "-v --verbose" global=#true
10215"#
10216        .parse()
10217        .unwrap();
10218
10219        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
10220        assert_eq!(
10221            parsed.external,
10222            Some(vec!["foo".into(), "--help".into(), "bar".into()])
10223        );
10224        assert!(parsed.flags.is_empty());
10225
10226        // Known subcommands still win.
10227        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
10228        assert_eq!(parsed.cmd.name, "install");
10229        assert!(parsed.external.is_none());
10230
10231        // A global flag before the unmatched word still binds on the parent.
10232        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
10233        assert_eq!(
10234            parsed.external,
10235            Some(vec!["foo".into(), "--verbose".into()])
10236        );
10237        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
10238
10239        // An unknown flag on the parent is still an error, which is what clap does.
10240        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
10241
10242        // A negative number is a value, not a flag, so it can be the unmatched word.
10243        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
10244        // token as a flag and never reach the catch-all.
10245        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
10246        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
10247    }
10248
10249    #[test]
10250    fn an_external_subcommand_satisfies_subcommand_required() {
10251        let mut spec: Spec = r#"
10252name "ex"
10253bin "ex"
10254external_subcommand #true
10255cmd "install"
10256"#
10257        .parse()
10258        .unwrap();
10259        spec.cmd.subcommand_required = true;
10260
10261        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
10262        assert!(parse(&spec, &input(&["ex"])).is_err());
10263    }
10264
10265    #[test]
10266    fn a_default_subcommand_outranks_an_external_one() {
10267        let spec: Spec = r#"
10268name "ex"
10269bin "ex"
10270default_subcommand "run"
10271external_subcommand #true
10272cmd "run" {
10273    arg "[task]"
10274}
10275"#
10276        .parse()
10277        .unwrap();
10278
10279        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10280        assert_eq!(parsed.cmd.name, "run");
10281        assert!(parsed.external.is_none());
10282        assert_eq!(first_string_value(&parsed), "build");
10283    }
10284
10285    #[test]
10286    fn multicall_basename_strips_a_path_and_exe() {
10287        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10288        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10289        assert_eq!(multicall_basename("LS.EXE"), "LS");
10290        assert_eq!(multicall_basename("busybox"), "busybox");
10291    }
10292
10293    #[test]
10294    fn a_multicall_applet_is_the_first_word() {
10295        let spec: Spec = r#"
10296name "busybox"
10297bin "busybox"
10298multicall #true
10299cmd "ls" {
10300    arg "[ARGS]" var=#true
10301}
10302cmd "cat"
10303"#
10304        .parse()
10305        .unwrap();
10306
10307        // A symlink: argv[0] is the applet.
10308        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10309        assert_eq!(parsed.cmd.name, "ls");
10310        match parsed.args.values().next() {
10311            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10312            other => panic!("expected ARGS to collect -l, got {other:?}"),
10313        }
10314
10315        // A dispatcher invocation still skips argv[0].
10316        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10317        assert_eq!(parsed.cmd.name, "ls");
10318
10319        // Configured dispatcher values receive the same path and extension normalization.
10320        let mut configured = spec.clone();
10321        configured.name = "BusyBox".to_string();
10322        configured.bin = "/opt/bin/busybox.exe".to_string();
10323        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10324        assert_eq!(parsed.cmd.name, "ls");
10325
10326        // `.exe` is stripped so Windows and Unix agree.
10327        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10328        assert_eq!(parsed.cmd.name, "ls");
10329
10330        // Without the property, argv[0] is discarded as usual.
10331        let mut plain = spec.clone();
10332        plain.multicall = false;
10333        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10334        assert_eq!(parsed.cmd.name, "ls");
10335    }
10336
10337    #[test]
10338    fn a_multicall_unknown_applet_can_be_external() {
10339        let spec: Spec = r#"
10340name "busybox"
10341bin "busybox"
10342multicall #true
10343unknown_flags "error"
10344external_subcommand #true
10345cmd "ls"
10346"#
10347        .parse()
10348        .unwrap();
10349
10350        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10351        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10352
10353        let mut closed = spec.clone();
10354        closed.cmd.external_subcommand = false;
10355        assert!(parse(&closed, &input(&["wat"])).is_err());
10356    }
10357
10358    #[cfg(feature = "unstable_choices_env")]
10359    #[test]
10360    fn test_parser_arg_choices_from_custom_env() {
10361        let spec = spec_arg_choices_env("DEPLOY_ENVS");
10362
10363        let parsed =
10364            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10365        assert_eq!(first_string_value(&parsed), "bar");
10366
10367        assert_parse_err(
10368            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10369            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10370        );
10371        assert_parse_err(
10372            parse_with_env(&spec, &["test", "prod"], &[]),
10373            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10374        );
10375    }
10376
10377    #[cfg(feature = "unstable_choices_env")]
10378    #[test]
10379    fn test_parser_validates_flag_choices_from_custom_env() {
10380        let spec = spec_flag_choices_env("DEPLOY_ENVS");
10381        let parsed = parse_with_env(
10382            &spec,
10383            &["test", "--env", "baz"],
10384            &[("DEPLOY_ENVS", "foo,bar baz")],
10385        )
10386        .unwrap();
10387        assert_eq!(first_string_value(&parsed), "baz");
10388    }
10389
10390    #[cfg(feature = "unstable_choices_env")]
10391    #[test]
10392    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10393        let arg_env_spec = spec_with_arg(
10394            SpecArg::builder()
10395                .name("env")
10396                .env("CURRENT_ENV")
10397                .choices_env("DEPLOY_ENVS")
10398                .build(),
10399        );
10400        assert_parse_err(
10401            parse_with_env(
10402                &arg_env_spec,
10403                &["test"],
10404                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10405            ),
10406            "Invalid choice for arg env: prod, expected one of dev, staging",
10407        );
10408
10409        let flag_default_spec = spec_with_flag(
10410            SpecFlag::builder()
10411                .long("env")
10412                .arg(
10413                    SpecArg::builder()
10414                        .name("env")
10415                        .choices_env("DEPLOY_ENVS")
10416                        .build(),
10417                )
10418                .default_value("prod")
10419                .build(),
10420        );
10421        assert_parse_err(
10422            parse_with_env(
10423                &flag_default_spec,
10424                &["test"],
10425                &[("DEPLOY_ENVS", "dev,staging")],
10426            ),
10427            "Invalid choice for option env: prod, expected one of dev, staging",
10428        );
10429    }
10430
10431    #[test]
10432    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10433        let spec: Spec = r#"
10434            flag "-v --verbose" var=#true
10435            arg "[database]" default="myapp_dev"
10436            arg "[args...]"
10437        "#
10438        .parse()
10439        .unwrap();
10440        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10441            .into_iter()
10442            .map(String::from)
10443            .collect();
10444        let parsed = parse(&spec, &input).unwrap();
10445        let env = parsed.as_env();
10446        assert_eq!(env.get("usage_database").unwrap(), "mydb");
10447        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10448    }
10449
10450    #[test]
10451    fn test_variadic_arg_captures_unknown_flags() {
10452        let cmd = SpecCommand::builder()
10453            .name("test")
10454            .flag(SpecFlag::builder().short('v').long("verbose").build())
10455            .arg(SpecArg::builder().name("database").required(false).build())
10456            .arg(
10457                SpecArg::builder()
10458                    .name("args")
10459                    .required(false)
10460                    .var(true)
10461                    .build(),
10462            )
10463            .build();
10464        let spec = Spec {
10465            name: "test".to_string(),
10466            bin: "test".to_string(),
10467            cmd,
10468            ..Default::default()
10469        };
10470
10471        // Unknown --host flag and its value should be captured by [args...]
10472        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10473            .into_iter()
10474            .map(String::from)
10475            .collect();
10476        let parsed = parse(&spec, &input).unwrap();
10477        assert_eq!(parsed.args.len(), 2);
10478        let args_val = parsed
10479            .args
10480            .iter()
10481            .find(|(a, _)| a.name == "args")
10482            .unwrap()
10483            .1;
10484        match args_val {
10485            ParseValue::MultiString(v) => {
10486                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10487            }
10488            _ => panic!("Expected MultiString, got {:?}", args_val),
10489        }
10490    }
10491
10492    #[test]
10493    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10494        let cmd = SpecCommand::builder()
10495            .name("test")
10496            .flag(SpecFlag::builder().short('v').long("verbose").build())
10497            .arg(SpecArg::builder().name("database").required(false).build())
10498            .arg(
10499                SpecArg::builder()
10500                    .name("args")
10501                    .required(false)
10502                    .var(true)
10503                    .build(),
10504            )
10505            .build();
10506        let spec = Spec {
10507            name: "test".to_string(),
10508            bin: "test".to_string(),
10509            cmd,
10510            ..Default::default()
10511        };
10512
10513        // With explicit -- separator
10514        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10515            .into_iter()
10516            .map(String::from)
10517            .collect();
10518        let parsed = parse(&spec, &input).unwrap();
10519        assert_eq!(parsed.args.len(), 2);
10520        let args_val = parsed
10521            .args
10522            .iter()
10523            .find(|(a, _)| a.name == "args")
10524            .unwrap()
10525            .1;
10526        match args_val {
10527            ParseValue::MultiString(v) => {
10528                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10529            }
10530            _ => panic!("Expected MultiString, got {:?}", args_val),
10531        }
10532    }
10533
10534    #[test]
10535    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10536        // Regression: --flag=value should be treated as a single positional token when
10537        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
10538        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10539
10540        // Single unknown --flag=value: must not produce a stray "3" positional.
10541        // as_env() shell-joins values, so "=" gets quoted.
10542        let input: Vec<String> = vec!["test", "--option=3"]
10543            .into_iter()
10544            .map(String::from)
10545            .collect();
10546        let parsed = parse(&spec, &input).unwrap();
10547        let env = parsed.as_env();
10548        assert_eq!(
10549            env.get("usage_other_args").map(String::as_str),
10550            Some("'--option=3'"),
10551            "expected a single --option=3 token, got {:?}",
10552            env.get("usage_other_args"),
10553        );
10554
10555        // Multiple unknown --flag=value args should each be kept intact
10556        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10557            .into_iter()
10558            .map(String::from)
10559            .collect();
10560        let parsed2 = parse(&spec, &input2).unwrap();
10561        let env2 = parsed2.as_env();
10562        assert_eq!(
10563            env2.get("usage_other_args").map(String::as_str),
10564            Some("'--foo=bar' '--baz=qux'"),
10565            "expected two intact tokens, got {:?}",
10566            env2.get("usage_other_args"),
10567        );
10568
10569        // Mix of plain positional args and unknown --flag=value tokens
10570        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10571            .into_iter()
10572            .map(String::from)
10573            .collect();
10574        let parsed3 = parse(&spec, &input3).unwrap();
10575        let env3 = parsed3.as_env();
10576        assert_eq!(
10577            env3.get("usage_other_args").map(String::as_str),
10578            Some("positional1 '--option=3' positional2"),
10579            "expected positional args and intact flag token, got {:?}",
10580            env3.get("usage_other_args"),
10581        );
10582    }
10583
10584    #[test]
10585    fn test_allow_hyphen_values_consumes_short_flag_collision() {
10586        let spec = r#"
10587flag "-d --working-dir <DIR>"
10588flag "-a --args <ARGS>" allow_hyphen_values=#true
10589"#
10590        .parse::<Spec>()
10591        .unwrap();
10592
10593        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10594
10595        assert_eq!(parsed.flags.len(), 1);
10596        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10597    }
10598
10599    #[test]
10600    fn test_allow_hyphen_values_consumes_embedded_long_value() {
10601        let spec = r#"
10602flag "-d --working-dir <DIR>"
10603flag "-a --args <ARGS>" allow_hyphen_values=#true
10604"#
10605        .parse::<Spec>()
10606        .unwrap();
10607
10608        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10609
10610        assert_eq!(parsed.flags.len(), 1);
10611        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10612    }
10613
10614    #[test]
10615    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10616        // The flag is declared to accept a token that looks like a flag, and `--` looks
10617        // like one, so it binds — which is what clap does with the same declaration.
10618        // Letting the separator arm run first consumed it and left the flag hungry, and
10619        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10620        let spec = r#"
10621flag "-a --args <ARGS>" allow_hyphen_values=#true
10622arg "[rest]..."
10623"#
10624        .parse::<Spec>()
10625        .unwrap();
10626
10627        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10628
10629        assert_eq!(flag_string_value(&parsed, "args"), "--");
10630        let rest = parsed
10631            .args
10632            .values()
10633            .next()
10634            .expect("expected the word after the separator to reach the argument");
10635        assert_eq!(rest.to_string(), "-x");
10636    }
10637
10638    #[test]
10639    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10640        // Which token supplied the first value says nothing about how many the argument
10641        // takes, so collection carries on from a hyphenated one exactly as from a plain
10642        // one. It still stops at the next flag-like token, which is what keeps a second
10643        // occurrence of the flag from being eaten as a value.
10644        let spec = r#"
10645flag "-a --args <ARGS>..." allow_hyphen_values=#true
10646"#
10647        .parse::<Spec>()
10648        .unwrap();
10649
10650        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10651
10652        let flag = parsed
10653            .flags
10654            .keys()
10655            .find(|flag| flag.name == "args")
10656            .expect("expected args flag");
10657        match parsed.flags.get(flag).expect("expected args value") {
10658            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10659            other => panic!("expected a list of values, got {other:?}"),
10660        }
10661    }
10662
10663    #[test]
10664    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10665        let spec = r#"
10666flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10667"#
10668        .parse::<Spec>()
10669        .unwrap();
10670
10671        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10672
10673        let flag = parsed
10674            .flags
10675            .keys()
10676            .find(|flag| flag.name == "args")
10677            .expect("expected args flag");
10678        let value = parsed.flags.get(flag).expect("expected args value");
10679        match value {
10680            ParseValue::MultiString(values) => {
10681                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10682            }
10683            _ => panic!("expected MultiString, got {value:?}"),
10684        }
10685    }
10686
10687    #[test]
10688    fn test_require_equals_accepts_attached_and_refuses_detached() {
10689        let spec = r#"
10690flag "--inspect <PORT>" require_equals=#true
10691"#
10692        .parse::<Spec>()
10693        .unwrap();
10694
10695        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10696        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10697
10698        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10699        let msg = format!("{err}");
10700        assert!(
10701            msg.contains("requires an argument") || msg.contains("inspect"),
10702            "detached value must be refused: {msg}"
10703        );
10704    }
10705
10706    #[test]
10707    fn boolean_flags_can_accept_attached_values_when_enabled() {
10708        let spec: Spec = r#"
10709name "ex"
10710bin "ex"
10711flag "--color" negate="--no-color" bool_value=#true
10712arg "[rest]"
10713"#
10714        .parse()
10715        .unwrap();
10716
10717        for (token, expected) in [
10718            ("--color", true),
10719            ("--color=true", true),
10720            ("--color=false", false),
10721            ("--no-color", false),
10722            ("--no-color=false", true),
10723        ] {
10724            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10725            assert!(
10726                matches!(
10727                    parsed.flags.get(&spec.cmd.flags[0]),
10728                    Some(ParseValue::Bool(value)) if *value == expected
10729                ),
10730                "{token}"
10731            );
10732        }
10733
10734        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10735        assert!(matches!(
10736            parsed.args.get(&spec.cmd.args[0]),
10737            Some(ParseValue::String(value)) if value == "word"
10738        ));
10739        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10740        assert!(err.to_string().contains("expected `true` or `false`"));
10741
10742        let strict: Spec = r#"
10743name "ex"
10744bin "ex"
10745args_override_self #false
10746flag "--color" negate="--no-color" bool_value=#true
10747"#
10748        .parse()
10749        .unwrap();
10750        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10751        let parsed = parse(
10752            &strict,
10753            &input(&["ex", "--color=false", "--no-color=false"]),
10754        )
10755        .unwrap();
10756        assert!(matches!(
10757            parsed.flags.get(&strict.cmd.flags[0]),
10758            Some(ParseValue::Bool(true))
10759        ));
10760    }
10761
10762    #[test]
10763    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10764        let spec = r#"
10765flag "-a --all"
10766flag "-i --inspect <PORT>" require_equals=#true
10767"#
10768        .parse::<Spec>()
10769        .unwrap();
10770
10771        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10772        let msg = format!("{err}");
10773        assert!(
10774            msg.contains("requires an argument") || msg.contains("inspect"),
10775            "bundled short must refuse the following word: {msg}"
10776        );
10777    }
10778
10779    #[test]
10780    fn test_default_missing_binds_when_the_value_is_left_off() {
10781        let spec = r#"
10782flag "-c --color <WHEN>" default_missing="always"
10783flag "-v --verbose"
10784"#
10785        .parse::<Spec>()
10786        .unwrap();
10787
10788        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10789        assert_eq!(flag_string_value(&parsed, "color"), "always");
10790
10791        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10792        assert_eq!(flag_string_value(&parsed, "color"), "never");
10793
10794        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10795        assert_eq!(flag_string_value(&parsed, "color"), "never");
10796
10797        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10798        assert_eq!(flag_string_value(&parsed, "color"), "always");
10799        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10800
10801        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10802        assert_eq!(flag_string_value(&parsed, "color"), "");
10803
10804        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10805        assert_eq!(flag_string_value(&parsed, "color"), "never");
10806
10807        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10808        assert_eq!(flag_string_value(&parsed, "color"), "always");
10809        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10810    }
10811
10812    #[test]
10813    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10814        let spec = r#"
10815flag "--apps <N>"
10816flag "--jobs <N>" default_missing="default missing"
10817flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10818"#
10819        .parse::<Spec>()
10820        .unwrap();
10821
10822        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10823        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10824
10825        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10826        assert!(
10827            err.to_string().contains("unexpected word: -1"),
10828            "default_missing must keep an unopted negative value separate: {err}"
10829        );
10830
10831        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10832        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10833
10834        let external_spec = r#"
10835external_subcommand #true
10836flag "--apps <N>"
10837"#
10838        .parse::<Spec>()
10839        .unwrap();
10840        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10841        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10842        assert!(parsed.external.is_none());
10843
10844        for words in [
10845            &["test", "--apps", "--jobs", "1"][..],
10846            &["test", "--apps", "--kids", "-1"][..],
10847        ] {
10848            let err = parse(&spec, &input(words)).unwrap_err();
10849            let message = err.to_string();
10850            assert!(
10851                message.contains("--apps") && message.contains("requires an argument"),
10852                "the earlier flag must report its missing value for {words:?}: {message}"
10853            );
10854        }
10855    }
10856
10857    #[test]
10858    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10859        let spec = r#"
10860flag "--bump [LEVEL]" value_optional=#true
10861flag "--verbose"
10862arg "[FILE]"
10863"#
10864        .parse::<Spec>()
10865        .unwrap();
10866
10867        let absent = parse(&spec, &input(&["test"])).unwrap();
10868        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10869
10870        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10871        let bump = bare
10872            .flags
10873            .iter()
10874            .find(|(flag, _)| flag.name == "bump")
10875            .map(|(_, value)| value)
10876            .unwrap();
10877        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10878        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10879        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10880
10881        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10882        assert_eq!(flag_string_value(&explicit, "bump"), "");
10883
10884        let corrected = parse(
10885            &spec,
10886            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10887        )
10888        .unwrap();
10889        let bump = corrected
10890            .flags
10891            .iter()
10892            .find(|(flag, _)| flag.name == "bump")
10893            .map(|(_, value)| value)
10894            .unwrap();
10895        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10896
10897        let collecting = r#"
10898flag "--tag [TAG]..." value_optional=#true
10899flag "--verbose"
10900"#
10901        .parse::<Spec>()
10902        .unwrap();
10903        let valued = parse(
10904            &collecting,
10905            &input(&["test", "--tag", "one", "two", "--verbose"]),
10906        )
10907        .unwrap();
10908        let tag = valued
10909            .flags
10910            .iter()
10911            .find(|(flag, _)| flag.name == "tag")
10912            .map(|(_, value)| value)
10913            .unwrap();
10914        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10915    }
10916
10917    #[test]
10918    fn test_repeatable_bare_optional_values_count_each_occurrence() {
10919        let spec = r#"
10920flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10921"#
10922        .parse::<Spec>()
10923        .unwrap();
10924
10925        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10926        let tag = parsed
10927            .flags
10928            .iter()
10929            .find(|(flag, _)| flag.name == "tag")
10930            .map(|(_, value)| value)
10931            .unwrap();
10932        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10933
10934        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10935        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10936    }
10937
10938    #[test]
10939    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10940        let spec = r#"
10941flag "--tag [TAG]..." var=#true value_optional=#true
10942flag "--verbose"
10943"#
10944        .parse::<Spec>()
10945        .unwrap();
10946
10947        for argv in [
10948            &["test", "--tag", "one", "two"][..],
10949            &["test", "--tag", "one", "two", "--verbose"][..],
10950            &["test", "--tag", "one", "--tag", "two"][..],
10951        ] {
10952            let parsed = parse(&spec, &input(argv)).unwrap();
10953            let tag = parsed
10954                .flags
10955                .iter()
10956                .find(|(flag, _)| flag.name == "tag")
10957                .map(|(_, value)| value)
10958                .unwrap();
10959            assert!(
10960                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10961                "argv={argv:?}: {tag:?}"
10962            );
10963        }
10964
10965        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10966        let tag = bare
10967            .flags
10968            .iter()
10969            .find(|(flag, _)| flag.name == "tag")
10970            .map(|(_, value)| value)
10971            .unwrap();
10972        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10973    }
10974
10975    #[test]
10976    fn test_default_missing_with_require_equals_refuses_the_following_word() {
10977        let spec = r#"
10978flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10979arg "[rest]"
10980"#
10981        .parse::<Spec>()
10982        .unwrap();
10983
10984        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10985        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10986
10987        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10988        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10989
10990        // The following word is not the value; the missing value is, and 80 is a positional.
10991        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10992        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10993        assert_eq!(
10994            parsed
10995                .args
10996                .values()
10997                .next()
10998                .map(|v| v.to_string())
10999                .as_deref(),
11000            Some("80")
11001        );
11002
11003        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
11004        assert_eq!(flag_string_value(&parsed, "inspect"), "");
11005    }
11006
11007    #[test]
11008    fn test_default_missing_must_be_a_choice() {
11009        let spec = r#"
11010flag "--color <WHEN>" default_missing="always" {
11011    choices "auto" "always" "never"
11012}
11013"#
11014        .parse::<Spec>()
11015        .unwrap();
11016
11017        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
11018        assert_eq!(flag_string_value(&parsed, "color"), "always");
11019
11020        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
11021        assert_eq!(flag_string_value(&parsed, "color"), "never");
11022
11023        let spec = r#"
11024flag "--color <WHEN>" default_missing="wat" {
11025    choices "auto" "always" "never"
11026}
11027"#
11028        .parse::<Spec>()
11029        .unwrap();
11030
11031        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
11032        let msg = format!("{err}");
11033        assert!(
11034            msg.contains("Invalid choice for option color: wat"),
11035            "missing default has to pass choices the same way a typed value does: {msg}"
11036        );
11037
11038        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
11039        let msg = format!("{err}");
11040        assert!(
11041            msg.contains("Invalid choice for option color: wat"),
11042            "an attached value that is not a choice is still refused: {msg}"
11043        );
11044
11045        let spec = r#"
11046flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
11047    choices "9229" "80"
11048}
11049arg "[rest]"
11050"#
11051        .parse::<Spec>()
11052        .unwrap();
11053
11054        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
11055        let msg = format!("{err}");
11056        assert!(
11057            msg.contains("Invalid choice for option inspect: wat"),
11058            "require_equals still binds the missing string, so the error is the choice: {msg}"
11059        );
11060    }
11061
11062    #[test]
11063    fn test_hyphen_values_still_start_short_flag_parsing() {
11064        let spec = r#"
11065flag "-d --working-dir <DIR>"
11066flag "-a --args <ARGS>"
11067"#
11068        .parse::<Spec>()
11069        .unwrap();
11070
11071        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
11072        let message = err.to_string();
11073        assert!(
11074            message.contains("--args") && message.contains("requires an argument"),
11075            "the recognized -d must leave the earlier -a missing: {message}"
11076        );
11077    }
11078
11079    /// `available_flags` has to agree with what an actual parse accepts, since
11080    /// its whole reason to exist is answering that question without one.
11081    mod available_flags {
11082        use super::*;
11083
11084        fn spec() -> Spec {
11085            r#"
11086bin "test"
11087flag "-v --verbose" global=#true
11088flag "--raw" global=#true effect="write"
11089flag "--local-only"
11090cmd "run" {
11091    flag "-r --raw"
11092    flag "-w --watch"
11093    cmd "once"
11094}
11095"#
11096            .parse::<Spec>()
11097            .unwrap()
11098        }
11099
11100        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
11101            let mut chain = vec![&spec.cmd];
11102            for segment in path {
11103                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
11104            }
11105            chain
11106        }
11107
11108        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
11109            let mut names: Vec<_> = available_flags(&chain(spec, path))
11110                .iter()
11111                .map(|f| f.name.clone())
11112                .collect();
11113            names.sort();
11114            names
11115        }
11116
11117        #[test]
11118        fn an_empty_chain_yields_nothing() {
11119            assert!(available_flags(&[]).is_empty());
11120        }
11121
11122        #[test]
11123        fn the_root_gets_its_own_flags() {
11124            let spec = spec();
11125            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
11126        }
11127
11128        #[test]
11129        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
11130            let spec = spec();
11131            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
11132        }
11133
11134        #[test]
11135        fn a_re_declared_global_is_listed_once() {
11136            // The merge can leave the long key on the merged flag and the short
11137            // key on the pre-merge one. Same flag; it must not be listed twice.
11138            let spec = r#"
11139bin "test"
11140flag "-y --yes" global=#true effect="write"
11141cmd "rm" {
11142    flag "-y --yes"
11143}
11144"#
11145            .parse::<Spec>()
11146            .unwrap();
11147            let flags = available_flags(&chain(&spec, &["rm"]));
11148            assert_eq!(flags.len(), 1, "{flags:?}");
11149            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
11150        }
11151
11152        #[test]
11153        fn a_re_declared_global_keeps_the_globals_declaration() {
11154            // `run` re-declares the long-only global `--raw` as `-r --raw`
11155            // without `global`. That is the same flag: the global's `effect`
11156            // survives, the orphan short is unioned in, and it stays global.
11157            let spec = spec();
11158            let flags = available_flags(&chain(&spec, &["run"]));
11159            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
11160            assert!(raw.global);
11161            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
11162            assert_eq!(raw.short, ['r']);
11163        }
11164
11165        #[test]
11166        fn it_matches_what_a_parse_accepts() {
11167            // The invariant. If these ever disagree, one of them is lying to a
11168            // caller about which flags a command takes.
11169            let spec = spec();
11170            for path in [vec![], vec!["run"], vec!["run", "once"]] {
11171                let argv = std::iter::once("test".to_string())
11172                    .chain(path.iter().map(|s| s.to_string()))
11173                    .collect::<Vec<_>>();
11174                let parsed = parse_partial(&spec, &argv).unwrap();
11175
11176                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
11177                    .map(|f| f.name.clone())
11178                    .collect();
11179                from_parse.sort();
11180                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
11181            }
11182        }
11183    }
11184
11185    // Provenance: which token bound what, and where a value came from when no token did.
11186
11187    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
11188    /// assert on the whole picture rather than on one field at a time.
11189    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
11190        parsed
11191            .tokens
11192            .iter()
11193            .find(|token| token.index == index)
11194            .unwrap_or_else(|| panic!("no token at {index}"))
11195            .roles
11196            .iter()
11197            .map(render_role)
11198            .collect()
11199    }
11200
11201    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
11202        parsed
11203            .flag_origins
11204            .iter()
11205            .find(|(f, _)| f.name == flag)
11206            .map(|(_, origins)| origins.clone())
11207            .unwrap_or_default()
11208    }
11209
11210    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
11211        let env = env
11212            .iter()
11213            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
11214            .collect();
11215        Parser::new(spec)
11216            .with_env(env)
11217            .explain(&input(words))
11218            .unwrap()
11219    }
11220
11221    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
11222        explain_with_env(spec, words, &[])
11223    }
11224
11225    #[test]
11226    fn an_attached_long_value_is_recorded_on_the_flag_token() {
11227        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11228            .parse()
11229            .unwrap();
11230
11231        let parsed = explain(&spec, &["ex", "--env=prod"]);
11232
11233        assert_eq!(roles(&parsed, 0), ["program"]);
11234        assert_eq!(
11235            roles(&parsed, 1),
11236            ["flag env as --env", "value of env = [\"prod\"], attached"]
11237        );
11238        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
11239        // form while the detached one worked, and nothing could show the difference.
11240        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
11241    }
11242
11243    #[test]
11244    fn a_detached_long_value_is_recorded_on_its_own_token() {
11245        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11246            .parse()
11247            .unwrap();
11248
11249        let parsed = explain(&spec, &["ex", "--env", "prod"]);
11250
11251        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
11252        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
11253    }
11254
11255    #[test]
11256    fn a_short_bundle_is_attributed_to_the_bundle_token() {
11257        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
11258            .parse()
11259            .unwrap();
11260
11261        let parsed = explain(&spec, &["ex", "-abj8"]);
11262
11263        // One word the caller wrote, four things it did — and the re-queued tails are
11264        // folded back onto it rather than appearing as tokens nobody typed.
11265        assert_eq!(
11266            roles(&parsed, 1),
11267            [
11268                "flag a as -a",
11269                "flag b as -b",
11270                "flag j as -j",
11271                "value of j = [\"8\"], attached",
11272            ]
11273        );
11274        assert_eq!(parsed.tokens.len(), 2);
11275    }
11276
11277    #[test]
11278    fn a_delimiter_splits_one_token_into_several_values() {
11279        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11280            .parse()
11281            .unwrap();
11282
11283        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11284
11285        assert_eq!(
11286            roles(&parsed, 2),
11287            ["value of tags = [\"a\", \"b\", \"c\"]"],
11288            "the values meant, not the word typed"
11289        );
11290    }
11291
11292    #[test]
11293    fn a_separator_and_the_words_after_it_are_distinguished() {
11294        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11295            .parse()
11296            .unwrap();
11297
11298        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11299
11300        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11301        assert_eq!(roles(&parsed, 2), ["separator"]);
11302        // Past the separator `-x` is data, not an unknown flag.
11303        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11304    }
11305
11306    #[test]
11307    fn a_second_separator_is_data() {
11308        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11309            .parse()
11310            .unwrap();
11311
11312        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11313
11314        assert_eq!(roles(&parsed, 1), ["separator"]);
11315        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11316    }
11317
11318    #[test]
11319    fn an_unknown_flag_says_what_took_it() {
11320        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11321            .parse()
11322            .unwrap();
11323
11324        let parsed = explain(&spec, &["ex", "--wat"]);
11325
11326        // The default is lax, so the word became data. Which is the useful thing to be
11327        // told: the alternative reading is "you have a typo".
11328        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11329    }
11330
11331    #[test]
11332    fn a_subcommand_word_is_not_a_positional() {
11333        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
11334            .parse()
11335            .unwrap();
11336
11337        let parsed = explain(&spec, &["ex", "build", "a"]);
11338
11339        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11340        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11341    }
11342
11343    #[test]
11344    fn a_multicall_applet_is_read_at_argv0() {
11345        let spec: Spec =
11346            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
11347                .parse()
11348                .unwrap();
11349
11350        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11351
11352        // argv[0] is both the program and the word that selected the applet, and the word
11353        // read there is not the word the caller wrote.
11354        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11355        assert!(parsed.tokens[0].synthesized);
11356        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11357    }
11358
11359    #[test]
11360    fn words_the_parse_never_reached_say_so() {
11361        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11362            .parse()
11363            .unwrap();
11364
11365        let parsed = Parser::new(&spec)
11366            .explain(&input(&["ex", "--help", "a"]))
11367            .unwrap();
11368
11369        assert_eq!(roles(&parsed, 2), ["unread"]);
11370    }
11371
11372    #[test]
11373    fn an_env_origin_names_the_variable_that_fired() {
11374        let spec: Spec =
11375            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11376                .parse()
11377                .unwrap();
11378
11379        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11380        assert_eq!(
11381            origins(&primary, "token"),
11382            [ValueOrigin::Env("EX_TOKEN".to_string())]
11383        );
11384
11385        // The fallback firing is a different fact from the primary firing, and which one it
11386        // was is what says which declaration to delete.
11387        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11388        assert_eq!(
11389            origins(&fallback, "token"),
11390            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11391        );
11392    }
11393
11394    #[test]
11395    fn a_default_origin_is_recorded_for_flags_and_args() {
11396        let spec: Spec =
11397            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11398                .parse()
11399                .unwrap();
11400
11401        let parsed = explain(&spec, &["ex"]);
11402
11403        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11404        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11405        assert_eq!(arg.name, "src");
11406        assert_eq!(origins, &[ValueOrigin::Default]);
11407    }
11408
11409    #[test]
11410    fn a_default_if_origin_carries_the_condition_that_fired() {
11411        let spec: Spec = r#"
11412name "ex"
11413bin "ex"
11414flag "--profile <p>"
11415flag "--strict" {
11416    default_if "--profile" "prod" "true"
11417}
11418        "#
11419        .parse()
11420        .unwrap();
11421
11422        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11423
11424        // The selector alone is ambiguous: several conditions may name it with different
11425        // `when` values, so the report has to say which one matched.
11426        assert_eq!(
11427            origins(&parsed, "strict"),
11428            [ValueOrigin::DefaultIf {
11429                selector: "--profile".to_string(),
11430                when: Some("prod".to_string()),
11431            }]
11432        );
11433    }
11434
11435    #[test]
11436    fn a_bare_optional_value_flag_records_default_missing() {
11437        let spec: Spec =
11438            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11439                .parse()
11440                .unwrap();
11441
11442        let parsed = explain(&spec, &["ex", "--color", "-v"]);
11443
11444        // The flag was typed and the value was not, which is the distinction a spec author
11445        // is asking about when they ask why `--color` came out `always`.
11446        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11447        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11448        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11449    }
11450
11451    #[test]
11452    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11453        let spec: Spec =
11454            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11455                .parse()
11456                .unwrap();
11457
11458        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11459
11460        // Why origins are a list: one declaration, two occurrences, two different answers.
11461        assert_eq!(
11462            roles(&parsed, 1),
11463            [
11464                "flag color as --color",
11465                "value of color = [\"red\"], attached"
11466            ]
11467        );
11468        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11469    }
11470
11471    #[test]
11472    fn an_override_names_the_flag_that_did_it() {
11473        let spec: Spec =
11474            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11475                .parse()
11476                .unwrap();
11477
11478        let parsed = explain(&spec, &["ex", "--loud"]);
11479
11480        // Without the overriding name, "`--quiet` is unset despite its default" has no
11481        // answer: the fallback phase silently declines to fill an overridden flag.
11482        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11483        assert!(origins(&parsed, "quiet").is_empty());
11484    }
11485
11486    #[test]
11487    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11488        let spec: Spec = r#"
11489name "ex"
11490bin "ex"
11491cmd "run" restart_token=":::" {
11492    arg "<task>" default="build"
11493}
11494        "#
11495        .parse()
11496        .unwrap();
11497
11498        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11499
11500        // The values belong to the last invocation, so provenance must too — but the words
11501        // of the first were still read, and a report that dropped them would show a command
11502        // line with a hole in it.
11503        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11504        // And the token that did the resetting says so: without a role of its own it reads
11505        // as a word that did nothing, next to a `lint` that filled an arg now empty.
11506        assert_eq!(roles(&parsed, 3), ["restart"]);
11507        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11508        assert!(parsed.arg_origins.is_empty());
11509    }
11510
11511    #[test]
11512    fn a_value_terminator_says_which_run_it_ended() {
11513        let spec: Spec = r#"
11514name "ex"
11515bin "ex"
11516flag "--exec <cmd>..." value_terminator=";"
11517arg "<src>"
11518        "#
11519        .parse()
11520        .unwrap();
11521
11522        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11523
11524        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11525        // The terminator is consumed and is not one of the values, which is the whole reason
11526        // it was declared — so it needs a row saying that rather than an empty one.
11527        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11528        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11529    }
11530
11531    #[test]
11532    fn an_args_value_terminator_says_which_run_it_ended() {
11533        let spec: Spec = r#"
11534name "ex"
11535bin "ex"
11536arg "<files>..." value_terminator=";"
11537arg "[dest]"
11538        "#
11539        .parse()
11540        .unwrap();
11541
11542        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11543
11544        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11545        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11546        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11547    }
11548
11549    #[test]
11550    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11551        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11552            .parse()
11553            .unwrap();
11554
11555        let parsed = Parser::new(&spec)
11556            .explain(&input(&["ex", "--env=prod"]))
11557            .unwrap();
11558
11559        // `parse` reports "missing required <src>" and nothing else, which is the report the
11560        // caller already had. This is the case the whole thing exists for.
11561        assert!(
11562            Parser::new(&spec)
11563                .parse(&input(&["ex", "--env=prod"]))
11564                .is_err()
11565        );
11566        assert_eq!(
11567            roles(&parsed, 1),
11568            ["flag env as --env", "value of env = [\"prod\"], attached"]
11569        );
11570        assert!(
11571            parsed.errors.iter().any(|e| e.to_string().contains("src")),
11572            "{:?}",
11573            parsed.errors
11574        );
11575    }
11576
11577    #[test]
11578    fn an_external_subcommand_forwards_whole_tokens() {
11579        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11580            .parse()
11581            .unwrap();
11582
11583        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11584
11585        assert_eq!(roles(&parsed, 1), ["external"]);
11586        assert_eq!(roles(&parsed, 2), ["external"]);
11587    }
11588
11589    #[test]
11590    fn a_view_keeps_the_callers_argv_positions() {
11591        let spec: Spec = r#"
11592bin "ex"
11593view "runner" root="run"
11594cmd "run" {
11595    flag "--token <token>"
11596}
11597        "#
11598        .parse()
11599        .unwrap();
11600
11601        let parsed = explain(&spec, &["runner", "--token", "secret"]);
11602
11603        // A view re-enters the parse with the same argv, so the positions still mean what
11604        // the caller wrote.
11605        assert_eq!(roles(&parsed, 0), ["program"]);
11606        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11607    }
11608
11609    #[test]
11610    fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11611        let spec: Spec = r#"
11612name "ex"
11613bin "ex"
11614default_subcommand "run"
11615default_subcommand_on_empty #true
11616flag "--value <value>"
11617cmd "run" arg_required_else_help=#true
11618"#
11619        .parse()
11620        .unwrap();
11621        let err = Parser::new(&spec)
11622            .parse(&input(&["ex", "--value"]))
11623            .unwrap_err();
11624        assert!(err.to_string().contains("requires an argument"), "{err}");
11625        assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11626    }
11627
11628    #[test]
11629    fn empty_default_honors_parent_args_conflict_policy() {
11630        let spec: Spec = r#"
11631name "ex"
11632bin "ex"
11633default_subcommand "run"
11634default_subcommand_on_empty #true
11635args_conflicts_with_subcommands #true
11636flag "--verbose"
11637cmd "run"
11638"#
11639        .parse()
11640        .unwrap();
11641        let err = Parser::new(&spec)
11642            .parse(&input(&["ex", "--verbose"]))
11643            .unwrap_err();
11644        assert!(
11645            err.to_string().contains("cannot be used with arguments"),
11646            "{err}"
11647        );
11648    }
11649
11650    #[test]
11651    fn the_parse_records_which_flags_a_mounted_program_declared() {
11652        let mut spec: Spec = r#"
11653bin ex
11654flag "-p --pick <tool>" global=#true
11655cmd run {
11656    mount run=tasks
11657}
11658"#
11659        .parse()
11660        .unwrap();
11661        let mounted = r#"
11662cmd outer {
11663    cmd inner {
11664        flag "-p --other <name>"
11665    }
11666}
11667cmd deep {
11668    flag "--pick <tool>" global=#true
11669    cmd leaf {
11670        flag "-p --pick <tool>"
11671    }
11672}
11673"#;
11674        spec.resolve_mount_outputs(&HashMap::from([("tasks".to_string(), mounted.to_string())]))
11675            .unwrap();
11676        let pending_is_mounted = |words: &[&str]| {
11677            let words: Vec<String> = words.iter().map(|word| word.to_string()).collect();
11678            let out = parse_partial(&spec, &words).unwrap();
11679            let flag = out
11680                .flag_awaiting_value
11681                .first()
11682                .expect("a flag awaiting its value");
11683            out.is_mounted_flag(flag)
11684        };
11685
11686        assert!(!pending_is_mounted(&["ex", "--pick"]));
11687        // `inner`'s own `-p` collides with the host's global `-p`, which the parser keeps.
11688        assert!(!pending_is_mounted(&["ex", "run", "outer", "inner", "-p"]));
11689        assert!(!pending_is_mounted(&[
11690            "ex", "run", "outer", "inner", "--pick"
11691        ]));
11692        assert!(pending_is_mounted(&[
11693            "ex", "run", "outer", "inner", "--other"
11694        ]));
11695        // `leaf` re-declares `deep`'s global `--pick`, and the parser merges the two into one
11696        // flag that matches neither declaration. It is still the mounted program's.
11697        assert!(pending_is_mounted(&["ex", "run", "deep", "--pick"]));
11698        assert!(pending_is_mounted(&["ex", "run", "deep", "leaf", "--pick"]));
11699        assert!(!pending_is_mounted(&["ex", "run", "deep", "leaf", "-p"]));
11700    }
11701
11702    #[test]
11703    fn empty_default_mount_matches_explicit_selection() {
11704        let spec: Spec = r#"
11705name "ex"
11706bin "ex"
11707default_subcommand "run"
11708default_subcommand_on_empty #true
11709cmd "run" { mount run="discover" }
11710"#
11711        .parse()
11712        .unwrap();
11713        let mut mounts = HashMap::new();
11714        mounts.insert(
11715            "discover".to_string(),
11716            "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11717        );
11718        for argv in [
11719            vec!["ex".to_string()],
11720            vec!["ex".to_string(), "run".to_string()],
11721        ] {
11722            let out = Parser::new(&spec)
11723                .with_mount_outputs(mounts.clone())
11724                .parse(&argv)
11725                .unwrap();
11726            assert_eq!(out.cmds.last().unwrap().name, "run");
11727            assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11728        }
11729    }
11730
11731    #[test]
11732    fn pending_parent_value_stays_pending_in_partial_parse() {
11733        let spec: Spec = r#"
11734name "ex"
11735bin "ex"
11736default_subcommand "run"
11737default_subcommand_on_empty #true
11738flag "--value <value>"
11739cmd "run" {}
11740"#
11741        .parse()
11742        .unwrap();
11743        let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11744        assert_eq!(out.cmd.name, "ex");
11745        assert_eq!(out.flag_awaiting_value[0].name, "value");
11746    }
11747
11748    #[test]
11749    fn optional_default_missing_still_selects_empty_child() {
11750        let spec: Spec = r#"
11751name "ex"
11752bin "ex"
11753default_subcommand "run"
11754default_subcommand_on_empty #true
11755flag "--value <value>" default_missing="fallback"
11756cmd "run" {}
11757"#
11758        .parse()
11759        .unwrap();
11760        let out = Parser::new(&spec)
11761            .parse(&input(&["ex", "--value"]))
11762            .unwrap();
11763        assert_eq!(out.cmd.name, "run");
11764        assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11765    }
11766
11767    #[test]
11768    fn empty_default_resets_cursor_after_root_sigil() {
11769        let spec: Spec = r#"
11770name "ex"
11771bin "ex"
11772default_subcommand "run"
11773default_subcommand_on_empty #true
11774arg "[tool]..." sigil="@"
11775cmd "run" { arg "[path]" }
11776"#
11777        .parse()
11778        .unwrap();
11779        let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11780        assert_eq!(out.cmd.name, "run");
11781        assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11782    }
11783
11784    #[test]
11785    fn root_clause_positional_prevents_empty_default() {
11786        let spec: Spec = r#"
11787name "ex"
11788bin "ex"
11789default_subcommand "run"
11790default_subcommand_on_empty #true
11791clause "tasks" {
11792  arg "<number>" allow_negative_numbers=#true
11793}
11794cmd "run" { arg "[task]" }
11795"#
11796        .parse()
11797        .unwrap();
11798        let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11799        assert!(
11800            out.cmds.iter().all(|cmd| cmd.name != "run"),
11801            "cmds={:?} args={:?}",
11802            out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11803            out.args
11804        );
11805        let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11806        assert_eq!(
11807            out.clauses["tasks"][0].get(number).unwrap().to_string(),
11808            "-7"
11809        );
11810    }
11811}