Skip to main content

usage/
parse.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use std::sync::Arc;
9use strum::EnumTryAs;
10
11#[cfg(feature = "docs")]
12use crate::docs;
13use crate::error::UsageErr;
14use crate::spec::arg::SpecDoubleDashChoices;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27///
28/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
29/// command describes another program, which does not accept the mounting CLI's globals.
30/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
31/// re-parses them), but the mounted command's own flags take precedence over them, so its
32/// choices/completions are not replaced by a global's. Which flags a completion may offer
33/// there is a separate question, answered by [`ParseOutput::completion_flags`].
34fn merge_subcommand_flags(
35    available: &mut BTreeMap<String, Arc<SpecFlag>>,
36    new_flags: BTreeMap<String, Arc<SpecFlag>>,
37    crossing_mount: bool,
38) {
39    // Keep only inherited global flags from the parent.
40    available.retain(|_, f| f.global);
41
42    if crossing_mount {
43        // A mounted command owns its flags outright, including names an inherited global also
44        // uses: a word after the mounted command belongs to the mounted program. Words before
45        // it keep resolving to the global they were read as, via `prefix_bindings`. Aliases the
46        // mounted command does not declare (e.g. a global's short) stay inherited.
47        for (key, flag) in new_flags {
48            available.insert(key, flag);
49        }
50        return;
51    }
52
53    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
54    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
55    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56    // Maps each merged flag produced below back to the inherited global it was merged from, so
57    // the collision check can compare *origins*: a flag this loop already merged is not a
58    // different global, even though it is a different `Arc`.
59    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
61    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62        let ptr = Arc::as_ptr(flag) as usize;
63        *merged_origin.get(&ptr).unwrap_or(&ptr)
64    }
65
66    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
67    // map's existing intra-subcommand collision resolution: when two flags in the same command
68    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
69    // to its last-declared owner, and we must not change which flag owns it.
70    for (key, flag) in new_flags {
71        if flag.global {
72            // A child that re-declares (or adds) a global flag stays recognized everywhere.
73            available.insert(key, flag);
74            continue;
75        }
76
77        // A non-global re-declaration that shares a LONG name with an inherited global flag is
78        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
79        // global). Keep the global flag (global precedence, so it survives the next descent's
80        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
81        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
82        // deliberate: a re-declaration sharing only a short letter with an unrelated global
83        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
84        // handled by the `contains_key` skip below instead.
85        let inherited_global = flag.long.iter().find_map(|l| {
86            available
87                .get(&format!("--{l}"))
88                .filter(|f| f.global)
89                .cloned()
90        });
91        if let Some(global_flag) = inherited_global {
92            // Never clobber a *different* inherited global's alias. If this re-declaration's
93            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
94            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
95            // precedence dictates, instead of stealing the alias for the merged flag.
96            //
97            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
98            // earlier key of this same child already replaced some of them with the merged flag,
99            // which the lookups above may now resolve to. That is the same logical flag, so it
100            // must not read as a collision and leave this key on the pre-merge global.
101            let global_origin = origin_of(&merged_origin, &global_flag);
102            if available.get(&key).is_some_and(|existing| {
103                existing.global && origin_of(&merged_origin, existing) != global_origin
104            }) {
105                continue;
106            }
107            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108                Some(merged) => merged.clone(),
109                None => {
110                    let mut merged = (*global_flag).clone();
111                    for s in &flag.short {
112                        if !merged.short.contains(s) {
113                            merged.short.push(*s);
114                        }
115                    }
116                    for l in &flag.long {
117                        if !merged.long.contains(l) {
118                            merged.long.push(l.clone());
119                        }
120                    }
121                    let merged = Arc::new(merged);
122                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
123                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
124                    // Rebind the global's *other* aliases onto the merged flag. The loop only
125                    // visits keys the child declared, so an alias the child left out (the `-y` of
126                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
127                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
128                    // logical flag must be one object under every key it answers to.
129                    for existing in available.values_mut() {
130                        if origin_of(&merged_origin, existing) == global_origin {
131                            *existing = Arc::clone(&merged);
132                        }
133                    }
134                    merged
135                }
136            };
137            available.insert(key, merged);
138            continue;
139        }
140
141        // Purely-local flag (shares nothing with an inherited global), or one that collides only
142        // on a short with an unrelated global. Insert this alias but never shadow an inherited
143        // global flag. Such non-global flags are dropped by the next descent's `retain`.
144        if available.contains_key(&key) {
145            continue;
146        }
147        available.insert(key, flag);
148    }
149}
150
151/// Build the lookup keys a flag is registered under in `available_flags`:
152/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
153fn flag_keys(flag: &SpecFlag) -> Vec<String> {
154    let mut keys: Vec<String> = flag
155        .long
156        .iter()
157        .map(|l| format!("--{l}"))
158        .chain(flag.short.iter().map(|s| format!("-{s}")))
159        .collect();
160    if let Some(negate) = &flag.negate {
161        keys.push(negate.clone());
162    }
163    keys
164}
165
166/// The flags a command declares, keyed by each of their aliases.
167fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
168    cmd.flags
169        .iter()
170        .flat_map(|f| {
171            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
172            flag_keys(&f)
173                .into_iter()
174                .map(|key| (key, Arc::clone(&f)))
175                .collect::<Vec<_>>()
176        })
177        .collect()
178}
179
180fn unique_flags<'a>(
181    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
182) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
183    let mut seen = HashSet::new();
184    flags
185        .into_iter()
186        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
187}
188
189/// Every flag a command accepts, resolved the way parsing an invocation of it
190/// resolves them.
191///
192/// `chain` runs from the root command (`spec.cmd`) down to the command in
193/// question; an empty chain yields no flags.
194///
195/// This is not "the command's flags plus its ancestors' globals". A subcommand
196/// that re-declares a global's long name is describing the *same* flag rather
197/// than a new one, so the global's help, argument and effect survive and only
198/// the re-declaration's extra aliases are added — see
199/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
200/// going through this will disagree with what the parser actually accepts.
201pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
202    let Some((root, rest)) = chain.split_first() else {
203        return vec![];
204    };
205    let mut available = gather_flags(root);
206    for cmd in rest {
207        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
208    }
209
210    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
211    // global that has both a short and a long, the merged flag is written under
212    // the long key while the short key keeps pointing at the pre-merge `Arc` —
213    // two objects for one logical flag. That is harmless for parsing, which
214    // looks flags up by key, but a caller listing flags would see it twice.
215    //
216    // Names break the tie because a long key always sorts before a short one
217    // (`--x` < `-y` at the second byte), so the merged declaration is the one
218    // reached first. Two genuinely distinct flags sharing a name is a spec bug
219    // that `usage lint` reports as a duplicate flag.
220    let mut seen_names = HashSet::new();
221    unique_flags(available.values())
222        .filter(|f| seen_names.insert(f.name.clone()))
223        .cloned()
224        .collect()
225}
226
227/// Extract the flag key from a flag word for lookup in available_flags map
228/// Handles both long flags (--flag, --flag=value) and short flags (-f)
229fn get_flag_key(word: &str) -> &str {
230    if word.starts_with("--") {
231        // Long flag: strip =value if present
232        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
233    } else if word.len() >= 2 {
234        // Short flag: first two chars (-X)
235        &word[0..2]
236    } else {
237        word
238    }
239}
240
241pub struct ParseOutput {
242    pub cmd: SpecCommand,
243    pub cmds: Vec<SpecCommand>,
244    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
245    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
246    /// Every flag the parser recognizes at this point, keyed by each of its aliases
247    /// (`--long`, `-s`, negations).
248    ///
249    /// This includes flags that only remain recognized because they may appear *before* a
250    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
251    /// should offer.
252    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
253    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
254    pub errors: Vec<UsageErr>,
255    /// The positional argument the next word would have filled, i.e. where the parser's
256    /// cursor stopped. `None` once every argument is satisfied.
257    ///
258    /// Completions need exactly this: the parser already accounts for `var_max`, for
259    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
260    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
261    pub next_arg: Option<Arc<SpecArg>>,
262    /// Whether an explicit `--` was consumed *as a separator*.
263    ///
264    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
265    /// of the variadic argument collecting it, not a separator, so it does not unlock a
266    /// `double_dash="required"` argument.
267    pub double_dash_seen: bool,
268}
269
270impl ParseOutput {
271    /// The flags a completion should offer for the parsed command.
272    ///
273    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
274    /// command has been reached, though, the commands above it belong to the mounting CLI and
275    /// their flags are not accepted there — mise, for example, forwards everything after a task
276    /// name to the task itself — so only the flags declared from the mount boundary down are
277    /// offered. Those globals stay in `available_flags` because they may legitimately appear
278    /// *before* the mounted command.
279    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
280        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
281            return self.available_flags.clone();
282        };
283        // A mount can also merge flags from its spec's root into the command it is mounted on
284        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
285        // replay starts one level up to inherit its globals.
286        let start = match boundary.checked_sub(1) {
287            Some(prev) if self.cmds[prev].flags_from_mount => prev,
288            _ => boundary,
289        };
290        // Re-run the descent from there, which starts with no inherited flags. Below the
291        // boundary the mounted program's commands are ordinary commands, so the descents use
292        // the same merge as the real parse.
293        let mut offered = gather_flags(&self.cmds[start]);
294        for cmd in &self.cmds[start + 1..] {
295            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
296        }
297        offered
298    }
299}
300
301#[derive(Debug, EnumTryAs, Clone)]
302pub enum ParseValue {
303    Bool(bool),
304    String(String),
305    MultiBool(Vec<bool>),
306    MultiString(Vec<String>),
307}
308
309/// Builder for parsing command-line arguments with custom options.
310///
311/// Use this when you need to customize parsing behavior, such as providing
312/// a custom environment variable map instead of using the process environment.
313///
314/// # Example
315/// ```
316/// use std::collections::HashMap;
317/// use usage::Spec;
318/// use usage::parse::Parser;
319///
320/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
321/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
322///
323/// let result = Parser::new(&spec)
324///     .with_env(env)
325///     .parse(&["cmd".into()])
326///     .unwrap();
327/// ```
328#[non_exhaustive]
329pub struct Parser<'a> {
330    spec: &'a Spec,
331    env: Option<HashMap<String, String>>,
332}
333
334impl<'a> Parser<'a> {
335    /// Create a new parser for the given spec.
336    pub fn new(spec: &'a Spec) -> Self {
337        Self { spec, env: None }
338    }
339
340    /// Use a custom environment variable map instead of the process environment.
341    ///
342    /// This is useful when parsing for tasks in a monorepo where the env vars
343    /// come from a child config file rather than the current process environment.
344    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
345        self.env = Some(env);
346        self
347    }
348
349    /// Parse the input arguments.
350    ///
351    /// Returns the parsed arguments and flags, with defaults and env vars applied.
352    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
353        let custom_env = self.env.as_ref();
354        let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
355        trace!("{out:?}");
356
357        let get_env = |key: &str| -> Option<String> {
358            if let Some(env_map) = custom_env {
359                env_map.get(key).cloned()
360            } else {
361                std::env::var(key).ok()
362            }
363        };
364
365        // Apply env vars and defaults for args
366        //
367        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
368        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
369        for arg in out.cmd.args.iter() {
370            if out.args.contains_key(arg) {
371                continue;
372            }
373            if let Some(env_var) = arg.env.as_ref() {
374                if let Some(env_value) = get_env(env_var) {
375                    validate_choice_value(
376                        ChoiceTarget::arg(arg),
377                        &env_value,
378                        arg.choices.as_ref(),
379                        custom_env,
380                    )?;
381                    out.args
382                        .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
383                    continue;
384                }
385            }
386            if !arg.default.is_empty() {
387                // Consider var when deciding the type of default return value
388                if arg.var {
389                    validate_choice_values(
390                        ChoiceTarget::arg(arg),
391                        &arg.default,
392                        arg.choices.as_ref(),
393                        custom_env,
394                    )?;
395                    // For var=true, always return a vec (MultiString)
396                    out.args.insert(
397                        Arc::new(arg.clone()),
398                        ParseValue::MultiString(arg.default.clone()),
399                    );
400                } else {
401                    validate_choice_value(
402                        ChoiceTarget::arg(arg),
403                        &arg.default[0],
404                        arg.choices.as_ref(),
405                        custom_env,
406                    )?;
407                    // For var=false, return the first default value as String
408                    out.args.insert(
409                        Arc::new(arg.clone()),
410                        ParseValue::String(arg.default[0].clone()),
411                    );
412                }
413            }
414        }
415
416        // Apply env vars and defaults for flags
417        for flag in out.available_flags.values() {
418            if out.flags.contains_key(flag) {
419                continue;
420            }
421            if let Some(env_var) = flag.env.as_ref() {
422                if let Some(env_value) = get_env(env_var) {
423                    if let Some(arg) = flag.arg.as_ref() {
424                        validate_choice_value(
425                            ChoiceTarget::option(flag),
426                            &env_value,
427                            arg.choices.as_ref(),
428                            custom_env,
429                        )?;
430                        out.flags
431                            .insert(Arc::clone(flag), ParseValue::String(env_value));
432                    } else {
433                        // For boolean flags, check if env value is truthy
434                        let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
435                        out.flags
436                            .insert(Arc::clone(flag), ParseValue::Bool(is_true));
437                    }
438                    continue;
439                }
440            }
441            // Apply flag default
442            if !flag.default.is_empty() {
443                // Consider var when deciding the type of default return value
444                if flag.var {
445                    // For var=true, always return a vec (MultiString for flags with args, MultiBool for boolean flags)
446                    if let Some(arg) = flag.arg.as_ref() {
447                        validate_choice_values(
448                            ChoiceTarget::option(flag),
449                            &flag.default,
450                            arg.choices.as_ref(),
451                            custom_env,
452                        )?;
453                        out.flags.insert(
454                            Arc::clone(flag),
455                            ParseValue::MultiString(flag.default.clone()),
456                        );
457                    } else {
458                        // For boolean flags with var=true, convert default strings to bools
459                        let bools: Vec<bool> = flag
460                            .default
461                            .iter()
462                            .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
463                            .collect();
464                        out.flags
465                            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
466                    }
467                } else {
468                    // For var=false, return the first default value
469                    if let Some(arg) = flag.arg.as_ref() {
470                        validate_choice_value(
471                            ChoiceTarget::option(flag),
472                            &flag.default[0],
473                            arg.choices.as_ref(),
474                            custom_env,
475                        )?;
476                        out.flags.insert(
477                            Arc::clone(flag),
478                            ParseValue::String(flag.default[0].clone()),
479                        );
480                    } else {
481                        // For boolean flags, convert default string to bool
482                        let is_true =
483                            matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
484                        out.flags
485                            .insert(Arc::clone(flag), ParseValue::Bool(is_true));
486                    }
487                }
488            }
489            // Also check nested arg defaults (for flags like --foo <arg> where the arg has a default)
490            if let Some(arg) = flag.arg.as_ref() {
491                if !out.flags.contains_key(flag) && !arg.default.is_empty() {
492                    if flag.var {
493                        validate_choice_values(
494                            ChoiceTarget::option(flag),
495                            &arg.default,
496                            arg.choices.as_ref(),
497                            custom_env,
498                        )?;
499                        out.flags.insert(
500                            Arc::clone(flag),
501                            ParseValue::MultiString(arg.default.clone()),
502                        );
503                    } else {
504                        validate_choice_value(
505                            ChoiceTarget::option(flag),
506                            &arg.default[0],
507                            arg.choices.as_ref(),
508                            custom_env,
509                        )?;
510                        out.flags
511                            .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
512                    }
513                }
514            }
515        }
516        if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
517            bail!("{err}");
518        }
519        if !out.errors.is_empty() {
520            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
521        }
522        Ok(out)
523    }
524}
525
526/// Parse command-line arguments according to a spec.
527///
528/// Returns the parsed arguments and flags, with defaults and env vars applied.
529/// Uses `std::env::var` for environment variable lookups.
530///
531/// For custom environment variable handling, use [`Parser`] instead.
532#[must_use = "parsing result should be used"]
533pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
534    Parser::new(spec).parse(input)
535}
536
537/// Parse command-line arguments without applying defaults.
538///
539/// Use this for help text generation or when you need the raw parsed values.
540#[must_use = "parsing result should be used"]
541pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
542    parse_partial_with_env(spec, input, None)
543}
544
545/// Internal version of parse_partial that accepts an optional custom env map.
546fn parse_partial_with_env(
547    spec: &Spec,
548    input: &[String],
549    custom_env: Option<&HashMap<String, String>>,
550) -> Result<ParseOutput, miette::Error> {
551    trace!("parse_partial: {input:?}");
552    let mut input = input.iter().cloned().collect::<VecDeque<_>>();
553    input.pop_front();
554
555    let mut out = ParseOutput {
556        cmd: spec.cmd.clone(),
557        cmds: vec![spec.cmd.clone()],
558        args: IndexMap::new(),
559        flags: IndexMap::new(),
560        available_flags: gather_flags(&spec.cmd),
561        flag_awaiting_value: vec![],
562        errors: vec![],
563        next_arg: None,
564        double_dash_seen: false,
565    };
566
567    // Phase 1: Scan for subcommands and collect global flags
568    //
569    // This phase identifies subcommands early because they may have mount points
570    // that need to be executed with the global flags that appeared before them.
571    //
572    // Example: "usage --verbose run task"
573    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
574    //   -> then finds "task" as a subcommand of "run" (if it exists)
575    //
576    // We only collect global flags for mounts because:
577    // - Non-global flags are specific to the current command, not subcommands
578    // - Global flags affect all commands and should be passed to mount points
579    let mut prefix_words: Vec<String> = vec![];
580    // Which flag each word skipped here belongs to, aligned with the leading words left in
581    // `input`: `Some(flag)` for a flag word, `None` for its value (or anything unresolved).
582    //
583    // The words stay in `input` for Phase 2 to re-parse — that is how they reach `out.flags`
584    // and `as_env()` — but by then the recognized flags have changed, because each descent
585    // drops the parent's non-global flags and a mounted command may declare the same name as
586    // a global seen here. Recording the owner keeps a word bound to the flag it was read as.
587    let mut prefix_bindings: VecDeque<Option<Arc<SpecFlag>>> = VecDeque::new();
588    let mut idx = 0;
589    // Track whether we've already applied the default_subcommand to prevent
590    // multiple switches (e.g., if default is "run" and there's a task named "run")
591    let mut used_default_subcommand = false;
592
593    while idx < input.len() {
594        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
595            let mut subcommand = subcommand.clone();
596            // Pass prefix words (global flags before this subcommand) to mount
597            subcommand.mount(&prefix_words)?;
598            // Only the *boundary* is a mount crossing: below it, the mounted program's own
599            // commands are ordinary commands relative to each other.
600            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
601            merge_subcommand_flags(
602                &mut out.available_flags,
603                gather_flags(&subcommand),
604                crossing_mount,
605            );
606            // Remove subcommand from input
607            input.remove(idx);
608            out.cmds.push(subcommand.clone());
609            out.cmd = subcommand.clone();
610            prefix_words.clear();
611            // Continue from current position (don't reset to 0)
612            // After remove(), idx now points to the next element
613        } else if input[idx].starts_with('-') {
614            // Check if this is a known flag
615            let word = input[idx].clone();
616            let flag_key = get_flag_key(&word);
617
618            if let Some(f) = out.available_flags.get(flag_key).cloned() {
619                // Skip the flag and keep scanning. Both global and non-global flags may precede
620                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
621                // stopping at one would hide the subcommand — and any mount on it — from the
622                // parse, leaving the subcommand name to be mis-read as a positional argument.
623                //
624                // Only globals are forwarded to mounts: a non-global flag belongs to the
625                // command that declared it, not to what is mounted below it.
626                prefix_bindings.push_back(Some(Arc::clone(&f)));
627                if f.global {
628                    prefix_words.push(word.clone());
629                }
630                idx += 1;
631
632                // Only consume next word if flag takes an argument AND value isn't embedded
633                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
634                if f.arg.is_some()
635                    && !word.contains('=')
636                    && idx < input.len()
637                    && !input[idx].starts_with('-')
638                {
639                    if f.global {
640                        prefix_words.push(input[idx].clone());
641                    }
642                    prefix_bindings.push_back(None);
643                    idx += 1;
644                }
645            } else {
646                // Unknown flag - stop looking for subcommands
647                // Let the main parsing phase handle the error
648                break;
649            }
650        } else {
651            // Found a word that's not a flag or subcommand
652            // Check if we should use the default_subcommand (only once)
653            if !used_default_subcommand {
654                if let Some(default_name) = &spec.default_subcommand {
655                    if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
656                        let mut subcommand = subcommand.clone();
657                        // Pass prefix words (global flags before this) to mount
658                        subcommand.mount(&prefix_words)?;
659                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
660                        merge_subcommand_flags(
661                            &mut out.available_flags,
662                            gather_flags(&subcommand),
663                            crossing_mount,
664                        );
665                        out.cmds.push(subcommand.clone());
666                        out.cmd = subcommand.clone();
667                        prefix_words.clear();
668                        used_default_subcommand = true;
669                        // Continue the loop to check if this word is a subcommand of the
670                        // default subcommand (e.g., a task name added via mount).
671                        // If it's not a subcommand, the next iteration will break and
672                        // Phase 2 will handle it as a positional arg.
673                        continue;
674                    }
675                }
676            }
677            // This could be a positional argument, so stop subcommand search
678            break;
679        }
680    }
681
682    // Phase 2: Main argument and flag parsing
683    //
684    // Now that we've identified all subcommands and executed their mounts,
685    // we can parse the remaining arguments, flags, and their values.
686
687    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
688    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
689    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
690    // argument filled?" has to consult `out.args` by key instead of counting.
691    let mut next_arg_idx: usize = 0;
692    let mut enable_flags = true;
693    let mut grouped_flag = false;
694    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
695    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
696    // words that come after it — see `report_double_dash_violation`.
697    let mut seen_double_dash = false;
698    // Args already reported as having been offered a word before the `--` they require, so a
699    // variadic one does not report the same violation for every word it is offered.
700    let mut double_dash_violations: HashSet<String> = HashSet::new();
701
702    while !input.is_empty() {
703        let mut w = input.pop_front().unwrap();
704        // The flag this word was read as in Phase 1, if it skipped it (see `prefix_bindings`).
705        // Words pushed back below get a `None` so the two queues stay aligned.
706        let binding = prefix_bindings.pop_front().flatten();
707
708        // Check for restart_token - resets argument parsing for multiple command invocations
709        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
710        if let Some(ref restart_token) = out.cmd.restart_token {
711            if w == *restart_token {
712                // Reset argument parsing state for a fresh command invocation, keeping the
713                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
714                // is not cleared here either, so clearing it would let one arg report the same
715                // violation once per invocation.
716                out.args.clear();
717                next_arg_idx = 0;
718                out.flag_awaiting_value.clear(); // Clear any pending flag values
719                enable_flags = true; // Reset -- separator effect
720                seen_double_dash = false; // The next invocation needs its own `--`
721                continue;
722            }
723        }
724
725        if w == "--" {
726            // Always disable flag parsing after seeing a "--" token
727            enable_flags = false;
728
729            // Only preserve the double dash token if we're collecting values for a variadic arg
730            // in double_dash == `preserve` mode
731            let should_preserve = out
732                .cmd
733                .args
734                .get(next_arg_idx)
735                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
736                .unwrap_or(false);
737
738            if should_preserve {
739                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
740                // neither counts as one nor unlocks a `double_dash="required"` arg.
741            } else {
742                seen_double_dash = true;
743
744                // Everything after an explicit `--` belongs to the arg that requires one, so
745                // jump the cursor there — past any earlier arg, including a greedy variadic
746                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
747                // which is what `double_dash="required"` is generated from. Specs without such
748                // an arg find nothing and keep the cursor where it was.
749                let target = out.cmd.args.iter().position(|arg| {
750                    arg.double_dash == SpecDoubleDashChoices::Required
751                        && !out.args.contains_key(arg)
752                });
753                if let Some(target) = target {
754                    // Forward only. An unfilled required arg declared *before* the cursor is
755                    // left where it is rather than rewound to — words already assigned to
756                    // later args would have to be taken back for that to mean anything, and
757                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
758                    // `Arg::last(true)`, which is the final positional, so a spec that puts
759                    // one ahead of others is already outside what this models.
760                    if target > next_arg_idx {
761                        next_arg_idx = target;
762                    }
763                }
764                continue;
765            }
766        }
767
768        if w.starts_with('-')
769            && out
770                .flag_awaiting_value
771                .last()
772                .is_some_and(|flag| flag.allow_hyphen_values())
773        {
774            let should_return = drain_pending_flag_values(
775                spec,
776                &out.cmd,
777                &mut out.errors,
778                &mut out.flags,
779                &mut out.flag_awaiting_value,
780                &mut w,
781                custom_env,
782            )?;
783            if should_return {
784                record_cursor(&mut out, next_arg_idx, seen_double_dash);
785                return Ok(out);
786            }
787            continue;
788        }
789
790        // long flags
791        if enable_flags && w.starts_with("--") {
792            grouped_flag = false;
793            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
794            if let Some(f) = binding.as_ref().or_else(|| out.available_flags.get(word)) {
795                // Only push the embedded value back when the flag is known so that
796                // unknown --flag=value tokens fall through intact to positional arg
797                // handling without also injecting a stray "value" positional.
798                if !val.is_empty() {
799                    input.push_front(val.to_string());
800                    prefix_bindings.push_front(None);
801                }
802                if f.arg.is_some() {
803                    out.flag_awaiting_value.push(Arc::clone(f));
804                } else if f.count {
805                    let arr = out
806                        .flags
807                        .entry(Arc::clone(f))
808                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
809                        .try_as_multi_bool_mut()
810                        .unwrap();
811                    arr.push(true);
812                } else {
813                    let negate = f.negate.clone().unwrap_or_default();
814                    out.flags
815                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
816                }
817                continue;
818            }
819            if is_help_arg(spec, &w) {
820                out.errors
821                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
822                record_cursor(&mut out, next_arg_idx, seen_double_dash);
823                return Ok(out);
824            }
825        }
826
827        // short flags
828        if enable_flags && w.starts_with('-') && w.len() > 1 {
829            let short = w.chars().nth(1).unwrap();
830            if let Some(f) = binding
831                .as_ref()
832                .or_else(|| out.available_flags.get(&format!("-{short}")))
833            {
834                if w.len() > 2 {
835                    input.push_front(format!("-{}", &w[2..]));
836                    prefix_bindings.push_front(None);
837                    grouped_flag = true;
838                }
839                if f.arg.is_some() {
840                    out.flag_awaiting_value.push(Arc::clone(f));
841                } else if f.count {
842                    let arr = out
843                        .flags
844                        .entry(Arc::clone(f))
845                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
846                        .try_as_multi_bool_mut()
847                        .unwrap();
848                    arr.push(true);
849                } else {
850                    let negate = f.negate.clone().unwrap_or_default();
851                    out.flags
852                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
853                }
854                continue;
855            }
856            if is_help_arg(spec, &w) {
857                out.errors
858                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
859                record_cursor(&mut out, next_arg_idx, seen_double_dash);
860                return Ok(out);
861            }
862            if grouped_flag {
863                grouped_flag = false;
864                w.remove(0);
865            }
866        }
867
868        if !out.flag_awaiting_value.is_empty() {
869            let should_return = drain_pending_flag_values(
870                spec,
871                &out.cmd,
872                &mut out.errors,
873                &mut out.flags,
874                &mut out.flag_awaiting_value,
875                &mut w,
876                custom_env,
877            )?;
878            if should_return {
879                record_cursor(&mut out, next_arg_idx, seen_double_dash);
880                return Ok(out);
881            }
882            continue;
883        }
884
885        if let Some(arg) = out.cmd.args.get(next_arg_idx) {
886            // Before anything else: an arg that requires `--` accepts nothing until one has been
887            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
888            // reported as an invalid choice, and from reaching that function's help escape.
889            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
890                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
891                // Drop the word without filling the arg or advancing the cursor: every later
892                // word hits the same arg and is rejected the same way, so the parse still ends
893                // in an error rather than in `unexpected word`.
894                continue;
895            }
896            if validate_choices(
897                spec,
898                &out.cmd,
899                &mut out.errors,
900                ChoiceTarget::arg(arg),
901                &w,
902                arg.choices.as_ref(),
903                custom_env,
904            )? {
905                record_cursor(&mut out, next_arg_idx, seen_double_dash);
906                return Ok(out);
907            }
908            if arg.var {
909                let arr = out
910                    .args
911                    .entry(Arc::new(arg.clone()))
912                    .or_insert_with(|| ParseValue::MultiString(vec![]))
913                    .try_as_multi_string_mut()
914                    .unwrap();
915                arr.push(w);
916                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
917                    next_arg_idx += 1;
918                }
919            } else {
920                out.args
921                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
922                next_arg_idx += 1;
923            }
924            continue;
925        }
926        if is_help_arg(spec, &w) {
927            out.errors
928                .push(render_help_err(spec, &out.cmd, w.len() > 2));
929            record_cursor(&mut out, next_arg_idx, seen_double_dash);
930            return Ok(out);
931        }
932        bail!("unexpected word: {w}");
933    }
934
935    record_cursor(&mut out, next_arg_idx, seen_double_dash);
936
937    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
938    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
939    for arg in out.cmd.args.iter() {
940        if out.args.contains_key(arg) {
941            continue;
942        }
943        // Already reported as needing a `--`; one mistake should not yield two messages.
944        if double_dash_violations.contains(&arg.name) {
945            continue;
946        }
947        if arg.required && arg.default.is_empty() {
948            // Check if there's an env var available (custom env map takes precedence)
949            let has_env = arg.env.as_ref().is_some_and(|e| {
950                custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
951                    || std::env::var(e).is_ok()
952            });
953            if !has_env {
954                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
955            }
956        }
957    }
958
959    for flag in unique_flags(out.available_flags.values()) {
960        if out.flags.contains_key(flag) {
961            continue;
962        }
963        let has_default =
964            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
965        // Check if there's an env var available (custom env map takes precedence)
966        let has_env = flag.env.as_ref().is_some_and(|e| {
967            custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
968        });
969        if flag.required && !has_default && !has_env {
970            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
971        }
972    }
973
974    // Validate var_min/var_max constraints for variadic args
975    for (arg, value) in &out.args {
976        if arg.var {
977            if let ParseValue::MultiString(values) = value {
978                if let Some(min) = arg.var_min {
979                    if values.len() < min {
980                        out.errors.push(UsageErr::VarArgTooFew {
981                            name: arg.name.clone(),
982                            min,
983                            got: values.len(),
984                        });
985                    }
986                }
987                if let Some(max) = arg.var_max {
988                    if values.len() > max {
989                        out.errors.push(UsageErr::VarArgTooMany {
990                            name: arg.name.clone(),
991                            max,
992                            got: values.len(),
993                        });
994                    }
995                }
996            }
997        }
998    }
999
1000    // Validate var_min/var_max constraints for variadic flags
1001    for (flag, value) in &out.flags {
1002        if flag.var {
1003            let count = match value {
1004                ParseValue::MultiString(values) => values.len(),
1005                ParseValue::MultiBool(values) => values.len(),
1006                _ => continue,
1007            };
1008            if let Some(min) = flag.var_min {
1009                if count < min {
1010                    out.errors.push(UsageErr::VarFlagTooFew {
1011                        name: flag.name.clone(),
1012                        min,
1013                        got: count,
1014                    });
1015                }
1016            }
1017            if let Some(max) = flag.var_max {
1018                if count > max {
1019                    out.errors.push(UsageErr::VarFlagTooMany {
1020                        name: flag.name.clone(),
1021                        max,
1022                        got: count,
1023                    });
1024                }
1025            }
1026        }
1027    }
1028
1029    Ok(out)
1030}
1031
1032#[cfg(feature = "docs")]
1033fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
1034    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
1035}
1036
1037#[cfg(not(feature = "docs"))]
1038fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
1039    UsageErr::Help("help".to_string())
1040}
1041
1042#[derive(Copy, Clone)]
1043struct ChoiceTarget<'a> {
1044    kind: &'a str,
1045    name: &'a str,
1046}
1047
1048impl<'a> ChoiceTarget<'a> {
1049    fn arg(arg: &'a SpecArg) -> Self {
1050        Self {
1051            kind: "arg",
1052            name: &arg.name,
1053        }
1054    }
1055
1056    fn option(flag: &'a SpecFlag) -> Self {
1057        Self {
1058            kind: "option",
1059            name: &flag.name,
1060        }
1061    }
1062}
1063
1064fn drain_pending_flag_values(
1065    spec: &Spec,
1066    cmd: &SpecCommand,
1067    errors: &mut Vec<UsageErr>,
1068    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
1069    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
1070    word: &mut String,
1071    custom_env: Option<&HashMap<String, String>>,
1072) -> miette::Result<bool> {
1073    while let Some(flag) = flag_awaiting_value.pop() {
1074        let arg = flag.arg.as_ref().unwrap();
1075        if validate_choices(
1076            spec,
1077            cmd,
1078            errors,
1079            ChoiceTarget::option(&flag),
1080            word,
1081            arg.choices.as_ref(),
1082            custom_env,
1083        )? {
1084            return Ok(true);
1085        }
1086        let value = std::mem::take(word);
1087        if flag.var {
1088            let arr = flags
1089                .entry(flag)
1090                .or_insert_with(|| ParseValue::MultiString(vec![]))
1091                .try_as_multi_string_mut()
1092                .unwrap();
1093            arr.push(value);
1094        } else {
1095            flags.insert(flag, ParseValue::String(value));
1096        }
1097    }
1098    Ok(false)
1099}
1100
1101fn choice_error(
1102    target: ChoiceTarget<'_>,
1103    value: &str,
1104    choices: Option<&SpecChoices>,
1105    custom_env: Option<&HashMap<String, String>>,
1106) -> Option<String> {
1107    let choices = choices?;
1108    let values = choices.values_with_env(custom_env);
1109    if values.iter().any(|choice| choice == value) {
1110        return None;
1111    }
1112    if let Some(env) = choices.env() {
1113        if values.is_empty() {
1114            return Some(format!(
1115                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
1116                target.kind, target.name,
1117            ));
1118        }
1119    }
1120    Some(format!(
1121        "Invalid choice for {} {}: {value}, expected one of {}",
1122        target.kind,
1123        target.name,
1124        values.join(", ")
1125    ))
1126}
1127
1128fn validate_choices(
1129    spec: &Spec,
1130    cmd: &SpecCommand,
1131    errors: &mut Vec<UsageErr>,
1132    target: ChoiceTarget<'_>,
1133    value: &str,
1134    choices: Option<&SpecChoices>,
1135    custom_env: Option<&HashMap<String, String>>,
1136) -> miette::Result<bool> {
1137    if is_help_arg(spec, value)
1138        && choices.is_some_and(|choices| {
1139            !choices
1140                .values_with_env(custom_env)
1141                .iter()
1142                .any(|choice| choice == value)
1143        })
1144    {
1145        errors.push(render_help_err(spec, cmd, value.len() > 2));
1146        return Ok(true);
1147    }
1148
1149    if let Some(err) = choice_error(target, value, choices, custom_env) {
1150        bail!("{err}");
1151    }
1152    Ok(false)
1153}
1154
1155fn validate_choice_value(
1156    target: ChoiceTarget<'_>,
1157    value: &str,
1158    choices: Option<&SpecChoices>,
1159    custom_env: Option<&HashMap<String, String>>,
1160) -> miette::Result<()> {
1161    if let Some(err) = choice_error(target, value, choices, custom_env) {
1162        bail!("{err}");
1163    }
1164    Ok(())
1165}
1166
1167fn validate_choice_values(
1168    target: ChoiceTarget<'_>,
1169    values: &[String],
1170    choices: Option<&SpecChoices>,
1171    custom_env: Option<&HashMap<String, String>>,
1172) -> miette::Result<()> {
1173    for value in values {
1174        validate_choice_value(target, value, choices, custom_env)?;
1175    }
1176    Ok(())
1177}
1178
1179/// Publish where Phase 2 left its positional cursor, so callers that do not re-run the parse —
1180/// completions, above all — agree with it. Called on every exit from the loop, including the
1181/// early ones that render help, where the cursor is still the useful answer.
1182fn record_cursor(out: &mut ParseOutput, next_arg_idx: usize, seen_double_dash: bool) {
1183    out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
1184    out.double_dash_seen = seen_double_dash;
1185}
1186
1187/// Record that `arg` was handed a word before the `--` it requires.
1188///
1189/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
1190/// message is emitted only the first time each arg is seen. The set is also what suppresses the
1191/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
1192/// end of the parse.
1193fn report_double_dash_violation(
1194    arg: &SpecArg,
1195    errors: &mut Vec<UsageErr>,
1196    violations: &mut HashSet<String>,
1197) {
1198    if violations.insert(arg.name.clone()) {
1199        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
1200    }
1201}
1202
1203fn is_help_arg(spec: &Spec, w: &str) -> bool {
1204    spec.disable_help != Some(true)
1205        && (w == "--help"
1206            || w == "-h"
1207            || w == "-?"
1208            || (spec.cmd.subcommands.is_empty() && w == "help"))
1209}
1210
1211impl ParseOutput {
1212    pub fn as_env(&self) -> BTreeMap<String, String> {
1213        let mut env = BTreeMap::new();
1214        for (flag, val) in &self.flags {
1215            let key = format!("usage_{}", flag.name.to_snake_case());
1216            let val = match val {
1217                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1218                ParseValue::String(s) => s.clone(),
1219                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
1220                ParseValue::MultiString(s) => shell_words::join(s),
1221            };
1222            env.insert(key, val);
1223        }
1224        for (arg, val) in &self.args {
1225            let key = format!("usage_{}", arg.name.to_snake_case());
1226            env.insert(key, val.to_string());
1227        }
1228        env
1229    }
1230}
1231
1232impl Display for ParseValue {
1233    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1234        match self {
1235            ParseValue::Bool(b) => write!(f, "{b}"),
1236            ParseValue::String(s) => write!(f, "{s}"),
1237            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
1238            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
1239        }
1240    }
1241}
1242
1243impl Debug for ParseOutput {
1244    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1245        f.debug_struct("ParseOutput")
1246            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
1247            .field(
1248                "args",
1249                &self
1250                    .args
1251                    .iter()
1252                    .map(|(a, w)| format!("{}: {w}", a.name))
1253                    .collect_vec(),
1254            )
1255            .field(
1256                "available_flags",
1257                &self
1258                    .available_flags
1259                    .iter()
1260                    .map(|(f, w)| format!("{f}: {w}"))
1261                    .collect_vec(),
1262            )
1263            .field(
1264                "flags",
1265                &self
1266                    .flags
1267                    .iter()
1268                    .map(|(f, w)| format!("{}: {w}", f.name))
1269                    .collect_vec(),
1270            )
1271            .field("flag_awaiting_value", &self.flag_awaiting_value)
1272            .field("errors", &self.errors)
1273            .finish()
1274    }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279    use super::*;
1280
1281    fn input(words: &[&str]) -> Vec<String> {
1282        words.iter().map(|word| (*word).to_string()).collect()
1283    }
1284
1285    fn spec_with_arg(arg: SpecArg) -> Spec {
1286        let cmd = SpecCommand::builder().name("test").arg(arg).build();
1287        Spec {
1288            name: "test".to_string(),
1289            bin: "test".to_string(),
1290            cmd,
1291            ..Default::default()
1292        }
1293    }
1294
1295    fn spec_with_flag(flag: SpecFlag) -> Spec {
1296        let cmd = SpecCommand::builder().name("test").flag(flag).build();
1297        Spec {
1298            name: "test".to_string(),
1299            bin: "test".to_string(),
1300            cmd,
1301            ..Default::default()
1302        }
1303    }
1304
1305    fn parse_with_env(
1306        spec: &Spec,
1307        words: &[&str],
1308        env: &[(&str, &str)],
1309    ) -> Result<ParseOutput, miette::Error> {
1310        let env = env
1311            .iter()
1312            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1313            .collect();
1314        Parser::new(spec).with_env(env).parse(&input(words))
1315    }
1316
1317    fn first_string_value(parsed: &ParseOutput) -> &str {
1318        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1319            return value;
1320        }
1321        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1322            return value;
1323        }
1324        panic!("expected first parsed value to be ParseValue::String");
1325    }
1326
1327    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
1328        let flag = parsed
1329            .flags
1330            .keys()
1331            .find(|flag| flag.name == name)
1332            .unwrap_or_else(|| panic!("expected flag {name}"));
1333        let value = parsed
1334            .flags
1335            .get(flag)
1336            .unwrap_or_else(|| panic!("expected value for flag {name}"));
1337        match value {
1338            ParseValue::String(value) => value,
1339            _ => panic!("expected flag {name} to be ParseValue::String"),
1340        }
1341    }
1342
1343    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1344        let err = result.expect_err("expected parser error");
1345        assert_eq!(format!("{err}"), expected);
1346    }
1347
1348    #[cfg(feature = "unstable_choices_env")]
1349    fn spec_arg_choices_env(key: &str) -> Spec {
1350        spec_with_arg(
1351            SpecArg::builder()
1352                .name("env")
1353                .choices_env(key)
1354                .required(false)
1355                .build(),
1356        )
1357    }
1358
1359    #[cfg(feature = "unstable_choices_env")]
1360    fn spec_flag_choices_env(key: &str) -> Spec {
1361        spec_with_flag(
1362            SpecFlag::builder()
1363                .long("env")
1364                .arg(SpecArg::builder().name("env").choices_env(key).build())
1365                .build(),
1366        )
1367    }
1368
1369    #[test]
1370    fn test_parse() {
1371        let cmd = SpecCommand::builder()
1372            .name("test")
1373            .arg(SpecArg::builder().name("arg").build())
1374            .flag(SpecFlag::builder().long("flag").build())
1375            .build();
1376        let spec = Spec {
1377            name: "test".to_string(),
1378            bin: "test".to_string(),
1379            cmd,
1380            ..Default::default()
1381        };
1382        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1383        let parsed = parse(&spec, &input).unwrap();
1384        assert_eq!(parsed.cmds.len(), 1);
1385        assert_eq!(parsed.cmds[0].name, "test");
1386        assert_eq!(parsed.args.len(), 1);
1387        assert_eq!(parsed.flags.len(), 1);
1388        assert_eq!(parsed.available_flags.len(), 1);
1389    }
1390
1391    #[test]
1392    fn test_as_env() {
1393        let cmd = SpecCommand::builder()
1394            .name("test")
1395            .arg(SpecArg::builder().name("arg").build())
1396            .flag(SpecFlag::builder().long("flag").build())
1397            .flag(
1398                SpecFlag::builder()
1399                    .long("force")
1400                    .negate("--no-force")
1401                    .build(),
1402            )
1403            .build();
1404        let spec = Spec {
1405            name: "test".to_string(),
1406            bin: "test".to_string(),
1407            cmd,
1408            ..Default::default()
1409        };
1410        let input = vec![
1411            "test".to_string(),
1412            "--flag".to_string(),
1413            "--no-force".to_string(),
1414        ];
1415        let parsed = parse(&spec, &input).unwrap();
1416        let env = parsed.as_env();
1417        assert_eq!(env.len(), 2);
1418        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1419        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1420    }
1421
1422    #[test]
1423    fn test_arg_env_var() {
1424        let cmd = SpecCommand::builder()
1425            .name("test")
1426            .arg(
1427                SpecArg::builder()
1428                    .name("input")
1429                    .env("TEST_ARG_INPUT")
1430                    .required(true)
1431                    .build(),
1432            )
1433            .build();
1434        let spec = Spec {
1435            name: "test".to_string(),
1436            bin: "test".to_string(),
1437            cmd,
1438            ..Default::default()
1439        };
1440
1441        // Set env var
1442        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1443
1444        let input = vec!["test".to_string()];
1445        let parsed = parse(&spec, &input).unwrap();
1446
1447        assert_eq!(parsed.args.len(), 1);
1448        let arg = parsed.args.keys().next().unwrap();
1449        assert_eq!(arg.name, "input");
1450        let value = parsed.args.values().next().unwrap();
1451        assert_eq!(value.to_string(), "test_file.txt");
1452
1453        // Clean up
1454        std::env::remove_var("TEST_ARG_INPUT");
1455    }
1456
1457    #[test]
1458    fn test_flag_env_var_with_arg() {
1459        let cmd = SpecCommand::builder()
1460            .name("test")
1461            .flag(
1462                SpecFlag::builder()
1463                    .long("output")
1464                    .env("TEST_FLAG_OUTPUT")
1465                    .arg(SpecArg::builder().name("file").build())
1466                    .build(),
1467            )
1468            .build();
1469        let spec = Spec {
1470            name: "test".to_string(),
1471            bin: "test".to_string(),
1472            cmd,
1473            ..Default::default()
1474        };
1475
1476        // Set env var
1477        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1478
1479        let input = vec!["test".to_string()];
1480        let parsed = parse(&spec, &input).unwrap();
1481
1482        assert_eq!(parsed.flags.len(), 1);
1483        let flag = parsed.flags.keys().next().unwrap();
1484        assert_eq!(flag.name, "output");
1485        let value = parsed.flags.values().next().unwrap();
1486        assert_eq!(value.to_string(), "output.txt");
1487
1488        // Clean up
1489        std::env::remove_var("TEST_FLAG_OUTPUT");
1490    }
1491
1492    #[test]
1493    fn test_flag_env_var_boolean() {
1494        let cmd = SpecCommand::builder()
1495            .name("test")
1496            .flag(
1497                SpecFlag::builder()
1498                    .long("verbose")
1499                    .env("TEST_FLAG_VERBOSE")
1500                    .build(),
1501            )
1502            .build();
1503        let spec = Spec {
1504            name: "test".to_string(),
1505            bin: "test".to_string(),
1506            cmd,
1507            ..Default::default()
1508        };
1509
1510        // Set env var to true
1511        std::env::set_var("TEST_FLAG_VERBOSE", "true");
1512
1513        let input = vec!["test".to_string()];
1514        let parsed = parse(&spec, &input).unwrap();
1515
1516        assert_eq!(parsed.flags.len(), 1);
1517        let flag = parsed.flags.keys().next().unwrap();
1518        assert_eq!(flag.name, "verbose");
1519        let value = parsed.flags.values().next().unwrap();
1520        assert_eq!(value.to_string(), "true");
1521
1522        // Clean up
1523        std::env::remove_var("TEST_FLAG_VERBOSE");
1524    }
1525
1526    #[test]
1527    fn test_env_var_precedence() {
1528        // CLI args should take precedence over env vars
1529        let cmd = SpecCommand::builder()
1530            .name("test")
1531            .arg(
1532                SpecArg::builder()
1533                    .name("input")
1534                    .env("TEST_PRECEDENCE_INPUT")
1535                    .required(true)
1536                    .build(),
1537            )
1538            .build();
1539        let spec = Spec {
1540            name: "test".to_string(),
1541            bin: "test".to_string(),
1542            cmd,
1543            ..Default::default()
1544        };
1545
1546        // Set env var
1547        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1548
1549        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1550        let parsed = parse(&spec, &input).unwrap();
1551
1552        assert_eq!(parsed.args.len(), 1);
1553        let value = parsed.args.values().next().unwrap();
1554        // CLI arg should take precedence
1555        assert_eq!(value.to_string(), "cli_file.txt");
1556
1557        // Clean up
1558        std::env::remove_var("TEST_PRECEDENCE_INPUT");
1559    }
1560
1561    #[test]
1562    fn test_flag_var_true_with_single_default() {
1563        // When var=true and default="bar", the default should be MultiString(["bar"])
1564        let cmd = SpecCommand::builder()
1565            .name("test")
1566            .flag(
1567                SpecFlag::builder()
1568                    .long("foo")
1569                    .var(true)
1570                    .arg(SpecArg::builder().name("foo").build())
1571                    .default_value("bar")
1572                    .build(),
1573            )
1574            .build();
1575        let spec = Spec {
1576            name: "test".to_string(),
1577            bin: "test".to_string(),
1578            cmd,
1579            ..Default::default()
1580        };
1581
1582        // User doesn't provide the flag
1583        let input = vec!["test".to_string()];
1584        let parsed = parse(&spec, &input).unwrap();
1585
1586        assert_eq!(parsed.flags.len(), 1);
1587        let flag = parsed.flags.keys().next().unwrap();
1588        assert_eq!(flag.name, "foo");
1589        let value = parsed.flags.values().next().unwrap();
1590        // Should be MultiString, not String
1591        match value {
1592            ParseValue::MultiString(v) => {
1593                assert_eq!(v.len(), 1);
1594                assert_eq!(v[0], "bar");
1595            }
1596            _ => panic!("Expected MultiString, got {:?}", value),
1597        }
1598    }
1599
1600    #[test]
1601    fn test_flag_var_true_with_multiple_defaults() {
1602        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
1603        let cmd = SpecCommand::builder()
1604            .name("test")
1605            .flag(
1606                SpecFlag::builder()
1607                    .long("foo")
1608                    .var(true)
1609                    .arg(SpecArg::builder().name("foo").build())
1610                    .default_values(["xyz", "bar"])
1611                    .build(),
1612            )
1613            .build();
1614        let spec = Spec {
1615            name: "test".to_string(),
1616            bin: "test".to_string(),
1617            cmd,
1618            ..Default::default()
1619        };
1620
1621        // User doesn't provide the flag
1622        let input = vec!["test".to_string()];
1623        let parsed = parse(&spec, &input).unwrap();
1624
1625        assert_eq!(parsed.flags.len(), 1);
1626        let value = parsed.flags.values().next().unwrap();
1627        // Should be MultiString with both values
1628        match value {
1629            ParseValue::MultiString(v) => {
1630                assert_eq!(v.len(), 2);
1631                assert_eq!(v[0], "xyz");
1632                assert_eq!(v[1], "bar");
1633            }
1634            _ => panic!("Expected MultiString, got {:?}", value),
1635        }
1636    }
1637
1638    #[test]
1639    fn test_flag_var_false_with_default_remains_string() {
1640        // When var=false (default), the default should still be String("bar")
1641        let cmd = SpecCommand::builder()
1642            .name("test")
1643            .flag(
1644                SpecFlag::builder()
1645                    .long("foo")
1646                    .var(false) // Default behavior
1647                    .arg(SpecArg::builder().name("foo").build())
1648                    .default_value("bar")
1649                    .build(),
1650            )
1651            .build();
1652        let spec = Spec {
1653            name: "test".to_string(),
1654            bin: "test".to_string(),
1655            cmd,
1656            ..Default::default()
1657        };
1658
1659        // User doesn't provide the flag
1660        let input = vec!["test".to_string()];
1661        let parsed = parse(&spec, &input).unwrap();
1662
1663        assert_eq!(parsed.flags.len(), 1);
1664        let value = parsed.flags.values().next().unwrap();
1665        // Should be String, not MultiString
1666        match value {
1667            ParseValue::String(s) => {
1668                assert_eq!(s, "bar");
1669            }
1670            _ => panic!("Expected String, got {:?}", value),
1671        }
1672    }
1673
1674    #[test]
1675    fn test_arg_var_true_with_single_default() {
1676        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
1677        let cmd = SpecCommand::builder()
1678            .name("test")
1679            .arg(
1680                SpecArg::builder()
1681                    .name("files")
1682                    .var(true)
1683                    .default_value("default.txt")
1684                    .required(false)
1685                    .build(),
1686            )
1687            .build();
1688        let spec = Spec {
1689            name: "test".to_string(),
1690            bin: "test".to_string(),
1691            cmd,
1692            ..Default::default()
1693        };
1694
1695        // User doesn't provide the arg
1696        let input = vec!["test".to_string()];
1697        let parsed = parse(&spec, &input).unwrap();
1698
1699        assert_eq!(parsed.args.len(), 1);
1700        let value = parsed.args.values().next().unwrap();
1701        // Should be MultiString, not String
1702        match value {
1703            ParseValue::MultiString(v) => {
1704                assert_eq!(v.len(), 1);
1705                assert_eq!(v[0], "default.txt");
1706            }
1707            _ => panic!("Expected MultiString, got {:?}", value),
1708        }
1709    }
1710
1711    #[test]
1712    fn test_arg_var_true_with_multiple_defaults() {
1713        // When arg has var=true and multiple defaults
1714        let cmd = SpecCommand::builder()
1715            .name("test")
1716            .arg(
1717                SpecArg::builder()
1718                    .name("files")
1719                    .var(true)
1720                    .default_values(["file1.txt", "file2.txt"])
1721                    .required(false)
1722                    .build(),
1723            )
1724            .build();
1725        let spec = Spec {
1726            name: "test".to_string(),
1727            bin: "test".to_string(),
1728            cmd,
1729            ..Default::default()
1730        };
1731
1732        // User doesn't provide the arg
1733        let input = vec!["test".to_string()];
1734        let parsed = parse(&spec, &input).unwrap();
1735
1736        assert_eq!(parsed.args.len(), 1);
1737        let value = parsed.args.values().next().unwrap();
1738        // Should be MultiString with both values
1739        match value {
1740            ParseValue::MultiString(v) => {
1741                assert_eq!(v.len(), 2);
1742                assert_eq!(v[0], "file1.txt");
1743                assert_eq!(v[1], "file2.txt");
1744            }
1745            _ => panic!("Expected MultiString, got {:?}", value),
1746        }
1747    }
1748
1749    #[test]
1750    fn test_arg_var_false_with_default_remains_string() {
1751        // When arg has var=false (default), the default should still be String
1752        let cmd = SpecCommand::builder()
1753            .name("test")
1754            .arg(
1755                SpecArg::builder()
1756                    .name("file")
1757                    .var(false)
1758                    .default_value("default.txt")
1759                    .required(false)
1760                    .build(),
1761            )
1762            .build();
1763        let spec = Spec {
1764            name: "test".to_string(),
1765            bin: "test".to_string(),
1766            cmd,
1767            ..Default::default()
1768        };
1769
1770        // User doesn't provide the arg
1771        let input = vec!["test".to_string()];
1772        let parsed = parse(&spec, &input).unwrap();
1773
1774        assert_eq!(parsed.args.len(), 1);
1775        let value = parsed.args.values().next().unwrap();
1776        // Should be String, not MultiString
1777        match value {
1778            ParseValue::String(s) => {
1779                assert_eq!(s, "default.txt");
1780            }
1781            _ => panic!("Expected String, got {:?}", value),
1782        }
1783    }
1784
1785    #[test]
1786    fn test_scalar_defaults_validate_only_first_default_choice() {
1787        let specs = [
1788            spec_with_arg(
1789                SpecArg::builder()
1790                    .name("env")
1791                    .var(false)
1792                    .default_values(["dev", "prod"])
1793                    .choices(["dev"])
1794                    .required(false)
1795                    .build(),
1796            ),
1797            spec_with_flag(
1798                SpecFlag::builder()
1799                    .long("env")
1800                    .arg(
1801                        SpecArg::builder()
1802                            .name("env")
1803                            .default_values(["dev", "prod"])
1804                            .choices(["dev"])
1805                            .build(),
1806                    )
1807                    .build(),
1808            ),
1809        ];
1810
1811        for spec in specs {
1812            let parsed = parse(&spec, &input(&["test"])).unwrap();
1813            assert_eq!(first_string_value(&parsed), "dev");
1814        }
1815    }
1816
1817    #[test]
1818    fn test_default_subcommand() {
1819        // Test that default_subcommand routes to the specified subcommand
1820        let run_cmd = SpecCommand::builder()
1821            .name("run")
1822            .arg(SpecArg::builder().name("task").build())
1823            .build();
1824        let mut cmd = SpecCommand::builder().name("test").build();
1825        cmd.subcommands.insert("run".to_string(), run_cmd);
1826
1827        let spec = Spec {
1828            name: "test".to_string(),
1829            bin: "test".to_string(),
1830            cmd,
1831            default_subcommand: Some("run".to_string()),
1832            ..Default::default()
1833        };
1834
1835        // "test mytask" should be parsed as if it were "test run mytask"
1836        let input = vec!["test".to_string(), "mytask".to_string()];
1837        let parsed = parse(&spec, &input).unwrap();
1838
1839        // Should have two commands: root and "run"
1840        assert_eq!(parsed.cmds.len(), 2);
1841        assert_eq!(parsed.cmds[1].name, "run");
1842
1843        // Should have parsed the task argument
1844        assert_eq!(parsed.args.len(), 1);
1845        let arg = parsed.args.keys().next().unwrap();
1846        assert_eq!(arg.name, "task");
1847        let value = parsed.args.values().next().unwrap();
1848        assert_eq!(value.to_string(), "mytask");
1849    }
1850
1851    #[test]
1852    fn test_default_subcommand_explicit_still_works() {
1853        // Test that explicit subcommand takes precedence
1854        let run_cmd = SpecCommand::builder()
1855            .name("run")
1856            .arg(SpecArg::builder().name("task").build())
1857            .build();
1858        let other_cmd = SpecCommand::builder()
1859            .name("other")
1860            .arg(SpecArg::builder().name("other_arg").build())
1861            .build();
1862        let mut cmd = SpecCommand::builder().name("test").build();
1863        cmd.subcommands.insert("run".to_string(), run_cmd);
1864        cmd.subcommands.insert("other".to_string(), other_cmd);
1865
1866        let spec = Spec {
1867            name: "test".to_string(),
1868            bin: "test".to_string(),
1869            cmd,
1870            default_subcommand: Some("run".to_string()),
1871            ..Default::default()
1872        };
1873
1874        // "test other foo" should use "other" subcommand, not default
1875        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1876        let parsed = parse(&spec, &input).unwrap();
1877
1878        // Should have used "other" subcommand
1879        assert_eq!(parsed.cmds.len(), 2);
1880        assert_eq!(parsed.cmds[1].name, "other");
1881    }
1882
1883    #[test]
1884    fn test_default_subcommand_with_nested_subcommands() {
1885        // Test that default_subcommand works when the default subcommand has nested subcommands.
1886        // This is the mise use case: "mise say" should be parsed as "mise run say"
1887        // where "say" is a subcommand of "run" (a task).
1888        let say_cmd = SpecCommand::builder()
1889            .name("say")
1890            .arg(SpecArg::builder().name("name").build())
1891            .build();
1892        let mut run_cmd = SpecCommand::builder().name("run").build();
1893        run_cmd.subcommands.insert("say".to_string(), say_cmd);
1894
1895        let mut cmd = SpecCommand::builder().name("test").build();
1896        cmd.subcommands.insert("run".to_string(), run_cmd);
1897
1898        let spec = Spec {
1899            name: "test".to_string(),
1900            bin: "test".to_string(),
1901            cmd,
1902            default_subcommand: Some("run".to_string()),
1903            ..Default::default()
1904        };
1905
1906        // "test say hello" should be parsed as "test run say hello"
1907        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1908        let parsed = parse(&spec, &input).unwrap();
1909
1910        // Should have three commands: root, "run", and "say"
1911        assert_eq!(parsed.cmds.len(), 3);
1912        assert_eq!(parsed.cmds[0].name, "test");
1913        assert_eq!(parsed.cmds[1].name, "run");
1914        assert_eq!(parsed.cmds[2].name, "say");
1915
1916        // Should have parsed the "name" argument
1917        assert_eq!(parsed.args.len(), 1);
1918        let arg = parsed.args.keys().next().unwrap();
1919        assert_eq!(arg.name, "name");
1920        let value = parsed.args.values().next().unwrap();
1921        assert_eq!(value.to_string(), "hello");
1922    }
1923
1924    /// Build a spec equivalent to the post-mount structure produced by mise's
1925    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
1926    /// subcommand that re-declares the same flag as NON-global, and a mounted task
1927    /// (`sample:run`) carrying a positional arg with `choices`.
1928    ///
1929    /// We construct the merged structure directly instead of executing a real mount so the
1930    /// test stays hermetic and cross-platform while still exercising the parser defect.
1931    fn mounted_global_flag_spec() -> Spec {
1932        let task_cmd = SpecCommand::builder()
1933            .name("sample:run")
1934            .arg(
1935                SpecArg::builder()
1936                    .name("profile")
1937                    .choices(["alpha", "beta", "gamma"])
1938                    .build(),
1939            )
1940            .build();
1941        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
1942        let mut run_cmd = SpecCommand::builder()
1943            .name("run")
1944            .flag(
1945                SpecFlag::builder()
1946                    .name("cd")
1947                    .short('C')
1948                    .long("cd")
1949                    .arg(SpecArg::builder().name("dir").build())
1950                    .global(false)
1951                    .build(),
1952            )
1953            .build();
1954        run_cmd
1955            .subcommands
1956            .insert("sample:run".to_string(), task_cmd);
1957
1958        let mut cmd = SpecCommand::builder()
1959            .name("test")
1960            .flag(
1961                SpecFlag::builder()
1962                    .name("cd")
1963                    .short('C')
1964                    .long("cd")
1965                    .arg(SpecArg::builder().name("dir").build())
1966                    .global(true)
1967                    .build(),
1968            )
1969            .build();
1970        cmd.subcommands.insert("run".to_string(), run_cmd);
1971
1972        Spec {
1973            name: "test".to_string(),
1974            bin: "test".to_string(),
1975            cmd,
1976            ..Default::default()
1977        }
1978    }
1979
1980    #[test]
1981    fn test_prefix_global_flag_does_not_pollute_choices() {
1982        // Regression for the parser-side root cause referenced by jdx/mise#10069.
1983        //
1984        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
1985        // then into the mounted `sample:run`) used to drop the inherited global flag from
1986        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
1987        // mis-validated against the task's `choices` positional arg.
1988        let spec = mounted_global_flag_spec();
1989
1990        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
1991        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
1992        for words in [
1993            &["test", "-C", "/tmp", "run", "sample:run"][..],
1994            // Embedded-value form must behave identically.
1995            &["test", "--cd=/tmp", "run", "sample:run"][..],
1996        ] {
1997            let parsed = parse_partial(&spec, &input(words)).unwrap();
1998            assert_eq!(
1999                parsed
2000                    .cmds
2001                    .iter()
2002                    .map(|c| c.name.as_str())
2003                    .collect::<Vec<_>>(),
2004                vec!["test", "run", "sample:run"],
2005            );
2006            // No positional arg should have been consumed by the leftover global-flag tokens.
2007            assert!(
2008                parsed.args.is_empty(),
2009                "args should be empty, got {:?}",
2010                parsed.args
2011            );
2012
2013            // Fix (B): the inherited global flag survives the descent even though `run`
2014            // re-declares `-C/--cd` as non-global.
2015            let cd = parsed
2016                .available_flags
2017                .get("--cd")
2018                .expect("--cd should remain available after descending into the subcommand");
2019            assert!(cd.global, "--cd must stay global after descent");
2020            assert!(
2021                parsed.available_flags.get("-C").is_some_and(|f| f.global),
2022                "-C must stay global after descent",
2023            );
2024
2025            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
2026            // for normal execution and for the env passed to mount scripts. (Removing the
2027            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
2028            assert_eq!(
2029                parsed.as_env().get("usage_cd").map(String::as_str),
2030                Some("/tmp"),
2031                "global flag value must survive in as_env(), got {:?}",
2032                parsed.as_env(),
2033            );
2034        }
2035
2036        // A real, valid choice still parses through the global flag prefix.
2037        let parsed = parse_partial(
2038            &spec,
2039            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
2040        )
2041        .unwrap();
2042        assert_eq!(parsed.args.len(), 1);
2043        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
2044
2045        // And genuinely invalid choices are still rejected (we didn't disable validation).
2046        assert_parse_err(
2047            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
2048            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2049        );
2050    }
2051
2052    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
2053    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
2054    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
2055    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
2056    fn mounted_orphan_short_spec() -> Spec {
2057        let task_cmd = SpecCommand::builder()
2058            .name("sample:run")
2059            .arg(
2060                SpecArg::builder()
2061                    .name("profile")
2062                    .choices(["alpha", "beta", "gamma"])
2063                    .build(),
2064            )
2065            .build();
2066        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
2067        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
2068        let mut run_cmd = SpecCommand::builder()
2069            .name("run")
2070            .flag(
2071                SpecFlag::builder()
2072                    .name("raw")
2073                    .short('r')
2074                    .long("raw")
2075                    .global(false)
2076                    .build(),
2077            )
2078            .flag(
2079                SpecFlag::builder()
2080                    .name("force")
2081                    .short('f')
2082                    .long("force")
2083                    .global(false)
2084                    .build(),
2085            )
2086            .build();
2087        run_cmd
2088            .subcommands
2089            .insert("sample:run".to_string(), task_cmd);
2090
2091        // Root global is LONG-ONLY: `--raw` with no short.
2092        let mut cmd = SpecCommand::builder()
2093            .name("test")
2094            .flag(
2095                SpecFlag::builder()
2096                    .name("raw")
2097                    .long("raw")
2098                    .global(true)
2099                    .build(),
2100            )
2101            .build();
2102        cmd.subcommands.insert("run".to_string(), run_cmd);
2103
2104        Spec {
2105            name: "test".to_string(),
2106            bin: "test".to_string(),
2107            cmd,
2108            ..Default::default()
2109        }
2110    }
2111
2112    #[test]
2113    fn test_orphan_short_alias_survives_merge() {
2114        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
2115        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
2116        // added short `-r` must be unioned onto the surviving inherited global flag instead of
2117        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
2118        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
2119        let spec = mounted_orphan_short_spec();
2120
2121        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
2122        assert_eq!(
2123            parsed
2124                .cmds
2125                .iter()
2126                .map(|c| c.name.as_str())
2127                .collect::<Vec<_>>(),
2128            vec!["test", "run", "sample:run"],
2129        );
2130
2131        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
2132        // and the original long `--raw` is still global too.
2133        assert!(
2134            parsed.available_flags.get("-r").is_some_and(|f| f.global),
2135            "-r must be merged onto the inherited global flag and stay global after descent",
2136        );
2137        assert!(
2138            parsed
2139                .available_flags
2140                .get("--raw")
2141                .is_some_and(|f| f.global),
2142            "--raw must stay global after descent",
2143        );
2144
2145        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
2146        assert!(
2147            parsed.args.is_empty(),
2148            "args should be empty, got {:?}",
2149            parsed.args
2150        );
2151
2152        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
2153        assert_eq!(
2154            parsed.as_env().get("usage_raw").map(String::as_str),
2155            Some("true"),
2156            "merged short's value must survive in as_env(), got {:?}",
2157            parsed.as_env(),
2158        );
2159
2160        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
2161        // promoted/merged — it is correctly dropped when descending into the mount.
2162        assert!(
2163            !parsed.available_flags.contains_key("-f"),
2164            "purely-local -f must not be promoted onto a global",
2165        );
2166        assert!(
2167            !parsed.available_flags.contains_key("--force"),
2168            "purely-local --force must not be promoted onto a global",
2169        );
2170
2171        // A real, valid choice still parses through the merged short prefix.
2172        let parsed =
2173            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
2174        assert_eq!(parsed.args.len(), 1);
2175        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
2176
2177        // And genuinely invalid choices are still rejected.
2178        assert_parse_err(
2179            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
2180            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2181        );
2182    }
2183
2184    #[test]
2185    fn test_orphan_short_does_not_clobber_unrelated_global() {
2186        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
2187        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
2188        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
2189        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
2190        let run_cmd = SpecCommand::builder()
2191            .name("run")
2192            .flag(
2193                SpecFlag::builder()
2194                    .name("raw")
2195                    .short('r')
2196                    .long("raw")
2197                    .global(false)
2198                    .build(),
2199            )
2200            .build();
2201        let mut cmd = SpecCommand::builder()
2202            .name("test")
2203            .flag(
2204                SpecFlag::builder()
2205                    .name("raw")
2206                    .long("raw")
2207                    .global(true)
2208                    .build(),
2209            )
2210            .flag(
2211                SpecFlag::builder()
2212                    .name("restrict")
2213                    .short('r')
2214                    .long("restrict")
2215                    .global(true)
2216                    .build(),
2217            )
2218            .build();
2219        cmd.subcommands.insert("run".to_string(), run_cmd);
2220        let spec = Spec {
2221            name: "test".to_string(),
2222            bin: "test".to_string(),
2223            cmd,
2224            ..Default::default()
2225        };
2226
2227        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2228        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
2229        assert_eq!(
2230            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
2231            Some("restrict"),
2232            "-r must remain owned by the unrelated global it already belonged to",
2233        );
2234        // Both globals are still recognized and global after the descent.
2235        assert!(parsed
2236            .available_flags
2237            .get("--raw")
2238            .is_some_and(|f| f.global));
2239        assert!(parsed
2240            .available_flags
2241            .get("--restrict")
2242            .is_some_and(|f| f.global));
2243    }
2244
2245    #[test]
2246    fn test_redeclared_global_aliases_share_one_flag() {
2247        // A global declared with BOTH a short and a long, re-declared non-globally by a
2248        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
2249        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
2250        // the time `-y` is reached the long already points at the merged flag. That merged flag is
2251        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
2252        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
2253        let spec = r#"
2254flag "-y --yes" global=#true effect="write"
2255cmd "run" {
2256    flag "-y --yes --assume-yes"
2257}
2258"#
2259        .parse::<Spec>()
2260        .unwrap();
2261
2262        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2263
2264        for key in ["-y", "--yes", "--assume-yes"] {
2265            let flag = parsed
2266                .available_flags
2267                .get(key)
2268                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2269            assert!(flag.global, "{key} must stay global after the descent");
2270            assert_eq!(
2271                flag.long,
2272                vec!["yes".to_string(), "assume-yes".to_string()],
2273                "{key} must resolve to the flag carrying every alias",
2274            );
2275            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
2276        }
2277
2278        // One logical flag means one object: all three keys share a single `Arc`.
2279        assert_eq!(
2280            unique_flags(parsed.available_flags.values()).count(),
2281            1,
2282            "all aliases must point at one flag object, got {:?}",
2283            parsed.available_flags,
2284        );
2285
2286        // The global's effect survives the merge, so `-y` still marks the command as writing.
2287        assert_eq!(
2288            parsed.available_flags["-y"].effect,
2289            Some(crate::SpecCommandEffect::Write),
2290        );
2291    }
2292
2293    #[test]
2294    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
2295        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
2296        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
2297        // aliases the child omits are never visited by the merge loop, so they must be rebound to
2298        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
2299        // pre-merge global and miss the added `assume-yes`.
2300        let spec = r#"
2301flag "-y --yes --confirm" global=#true
2302cmd "run" {
2303    flag "--yes --assume-yes"
2304}
2305"#
2306        .parse::<Spec>()
2307        .unwrap();
2308
2309        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2310
2311        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
2312            let flag = parsed
2313                .available_flags
2314                .get(key)
2315                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2316            assert!(flag.global, "{key} must stay global after the descent");
2317            assert_eq!(
2318                flag.long,
2319                vec![
2320                    "yes".to_string(),
2321                    "confirm".to_string(),
2322                    "assume-yes".to_string()
2323                ],
2324                "{key} must resolve to the flag carrying every alias",
2325            );
2326        }
2327
2328        assert_eq!(
2329            unique_flags(parsed.available_flags.values()).count(),
2330            1,
2331            "all aliases must point at one flag object, got {:?}",
2332            parsed.available_flags,
2333        );
2334    }
2335
2336    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
2337    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
2338    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
2339    ///
2340    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
2341    /// commands it merges in, so the test stays hermetic (no mount subprocess).
2342    fn mounted_task_flag_spec() -> Spec {
2343        let mut task_cmd = SpecCommand::builder()
2344            .name("mytask")
2345            .flag(
2346                SpecFlag::builder()
2347                    .name("env")
2348                    .long("env")
2349                    .arg(
2350                        SpecArg::builder()
2351                            .name("name")
2352                            .choices(["dev", "stage", "prod"])
2353                            .build(),
2354                    )
2355                    .global(false)
2356                    .build(),
2357            )
2358            .flag(
2359                SpecFlag::builder()
2360                    .name("bump")
2361                    .long("bump")
2362                    .arg(
2363                        SpecArg::builder()
2364                            .name("type")
2365                            .choices(["auto", "major"])
2366                            .build(),
2367                    )
2368                    .global(false)
2369                    .build(),
2370            )
2371            .build();
2372        task_cmd.mounted = true;
2373
2374        let mut run_cmd = SpecCommand::builder()
2375            .name("run")
2376            .flag(
2377                SpecFlag::builder()
2378                    .name("force")
2379                    .short('f')
2380                    .long("force")
2381                    .global(false)
2382                    .build(),
2383            )
2384            .build();
2385        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
2386
2387        let mut cmd = SpecCommand::builder()
2388            .name("test")
2389            .flag(
2390                SpecFlag::builder()
2391                    .name("env")
2392                    .short('E')
2393                    .long("env")
2394                    .arg(SpecArg::builder().name("ENV").build())
2395                    .global(true)
2396                    .build(),
2397            )
2398            .flag(
2399                SpecFlag::builder()
2400                    .name("silent")
2401                    .long("silent")
2402                    .global(true)
2403                    .build(),
2404            )
2405            .build();
2406        cmd.subcommands.insert("run".to_string(), run_cmd);
2407
2408        Spec {
2409            name: "test".to_string(),
2410            bin: "test".to_string(),
2411            cmd,
2412            ..Default::default()
2413        }
2414    }
2415
2416    #[test]
2417    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
2418        // The mounted program's own commands are ordinary commands relative to each other, so
2419        // descending *within* the mounted tree must follow the normal rules — including keeping
2420        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
2421        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
2422        // global, which the next descent's `retain(global)` then dropped entirely.
2423        let deep = SpecCommand::builder().name("deep").build();
2424        let mut sub = SpecCommand::builder()
2425            .name("sub")
2426            // Re-declares the mounted program's own global as non-global.
2427            .flag(
2428                SpecFlag::builder()
2429                    .name("cd")
2430                    .short('C')
2431                    .long("cd")
2432                    .arg(SpecArg::builder().name("dir").build())
2433                    .global(false)
2434                    .build(),
2435            )
2436            .build();
2437        sub.subcommands.insert("deep".to_string(), deep);
2438        let mut task = SpecCommand::builder()
2439            .name("task")
2440            .flag(
2441                SpecFlag::builder()
2442                    .name("cd")
2443                    .short('C')
2444                    .long("cd")
2445                    .arg(SpecArg::builder().name("dir").build())
2446                    .global(true)
2447                    .build(),
2448            )
2449            .build();
2450        task.subcommands.insert("sub".to_string(), sub);
2451        task.mark_mounted();
2452
2453        let mut run_cmd = SpecCommand::builder().name("run").build();
2454        run_cmd.subcommands.insert("task".to_string(), task);
2455        let mut cmd = SpecCommand::builder().name("test").build();
2456        cmd.subcommands.insert("run".to_string(), run_cmd);
2457        let spec = Spec {
2458            name: "test".to_string(),
2459            bin: "test".to_string(),
2460            cmd,
2461            ..Default::default()
2462        };
2463
2464        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
2465        assert!(
2466            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
2467            "the mounted program's own global must survive descents inside the mounted tree",
2468        );
2469        assert!(
2470            parsed.completion_flags().contains_key("--cd"),
2471            "and must still be offered there: it belongs to the mounted program",
2472        );
2473        assert!(
2474            parsed.completion_flags().contains_key("-C"),
2475            "including the short the nested command re-declared",
2476        );
2477    }
2478
2479    #[test]
2480    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
2481        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
2482        // into the command the mount sits on. They belong to the mounted program, so they must
2483        // be offered inside the mounted commands rather than filtered out with the mounting
2484        // CLI's own flags.
2485        let mut task = SpecCommand::builder()
2486            .name("task")
2487            .flag(
2488                SpecFlag::builder()
2489                    .name("bump")
2490                    .long("bump")
2491                    .global(false)
2492                    .build(),
2493            )
2494            .build();
2495        task.mark_mounted();
2496
2497        let mut run_cmd = SpecCommand::builder().name("run").build();
2498        run_cmd.subcommands.insert("task".to_string(), task);
2499        // What `mount()` leaves behind when the mounted spec's root declares flags.
2500        run_cmd.flags = vec![
2501            SpecFlag::builder()
2502                .name("tglobal")
2503                .long("tglobal")
2504                .global(true)
2505                .build(),
2506            SpecFlag::builder()
2507                .name("tlocal")
2508                .long("tlocal")
2509                .global(false)
2510                .build(),
2511        ];
2512        run_cmd.flags_from_mount = true;
2513
2514        let mut cmd = SpecCommand::builder()
2515            .name("test")
2516            .flag(
2517                SpecFlag::builder()
2518                    .name("silent")
2519                    .long("silent")
2520                    .global(true)
2521                    .build(),
2522            )
2523            .build();
2524        cmd.subcommands.insert("run".to_string(), run_cmd);
2525        let spec = Spec {
2526            name: "test".to_string(),
2527            bin: "test".to_string(),
2528            cmd,
2529            ..Default::default()
2530        };
2531
2532        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
2533        assert_eq!(
2534            parsed.completion_flags().keys().collect::<Vec<_>>(),
2535            vec!["--bump", "--tglobal"],
2536            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
2537             `--silent` does not, and the mount's non-global root flag is not inherited",
2538        );
2539    }
2540
2541    #[test]
2542    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
2543        // Regression for jdx/mise#11282. A mounted command describes another program, which
2544        // does not accept the mounting CLI's globals (mise forwards everything after a task
2545        // name to the task). They must stay recognized — they may appear before the mounted
2546        // command — but must not be offered in completions there.
2547        let spec = mounted_task_flag_spec();
2548        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
2549
2550        // Still recognized for parsing...
2551        assert!(parsed.available_flags.contains_key("--silent"));
2552        assert!(parsed.available_flags.contains_key("-E"));
2553        // ...but belonging to a command above the mount, so not offered.
2554        assert_eq!(
2555            parsed.completion_flags().keys().collect::<Vec<_>>(),
2556            vec!["--bump", "--env"],
2557            "only the mounted command's own flags may be offered",
2558        );
2559
2560        // `run`'s own non-global flag is dropped on descent, as it always was.
2561        assert!(!parsed.available_flags.contains_key("--force"));
2562    }
2563
2564    #[test]
2565    fn test_mounted_cmd_flag_wins_over_inherited_global() {
2566        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
2567        // by the root's `--env` global, so completing its value fell back to file completion.
2568        let spec = mounted_task_flag_spec();
2569        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
2570
2571        let awaiting = parsed
2572            .flag_awaiting_value
2573            .first()
2574            .expect("--env should await a value");
2575        assert_eq!(
2576            awaiting
2577                .arg
2578                .as_ref()
2579                .and_then(|a| a.choices.as_ref())
2580                .map(|c| c.choices.clone()),
2581            Some(vec![
2582                "dev".to_string(),
2583                "stage".to_string(),
2584                "prod".to_string()
2585            ]),
2586            "the mounted command's own --env must win over the inherited global",
2587        );
2588
2589        // The global's short is not declared by the mounted command, so it keeps pointing at
2590        // the global and a value passed before the mounted command still parses.
2591        let parsed =
2592            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
2593        assert!(
2594            parsed.args.is_empty(),
2595            "prefix global tokens must not be consumed as positionals, got {:?}",
2596            parsed.args
2597        );
2598        assert_eq!(
2599            parsed.as_env().get("usage_env").map(String::as_str),
2600            Some("anything"),
2601        );
2602    }
2603
2604    #[test]
2605    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
2606        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
2607        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
2608        // global's value would be validated against the mounted flag's choices and a legitimate
2609        // value would be rejected.
2610        let spec = mounted_task_flag_spec();
2611        let parsed = parse_partial(
2612            &spec,
2613            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
2614        )
2615        .unwrap();
2616        assert!(
2617            parsed.errors.is_empty(),
2618            "prefix global value must not be validated against the mounted flag: {:?}",
2619            parsed
2620                .errors
2621                .iter()
2622                .map(|e| e.to_string())
2623                .collect::<Vec<_>>(),
2624        );
2625        assert_eq!(
2626            parsed.as_env().get("usage_env").map(String::as_str),
2627            Some("not-a-task-choice"),
2628        );
2629
2630        // The embedded-value form binds the same way.
2631        let parsed = parse_partial(
2632            &spec,
2633            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
2634        )
2635        .unwrap();
2636        assert!(parsed.errors.is_empty());
2637        assert_eq!(
2638            parsed.as_env().get("usage_env").map(String::as_str),
2639            Some("not-a-task-choice"),
2640        );
2641
2642        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
2643        // the same name was already used before it.
2644        let parsed = parse_partial(
2645            &spec,
2646            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
2647        )
2648        .unwrap();
2649        let awaiting = parsed
2650            .flag_awaiting_value
2651            .first()
2652            .expect("--env should await a value");
2653        assert_eq!(
2654            awaiting
2655                .arg
2656                .as_ref()
2657                .and_then(|a| a.choices.as_ref())
2658                .map(|c| c.choices.clone()),
2659            Some(vec![
2660                "dev".to_string(),
2661                "stage".to_string(),
2662                "prod".to_string()
2663            ]),
2664            "the mounted command's --env must own the name after the mounted command",
2665        );
2666    }
2667
2668    #[test]
2669    fn test_non_global_flag_does_not_hide_subcommand() {
2670        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
2671        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
2672        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
2673        let spec = mounted_task_flag_spec();
2674
2675        for words in [
2676            // `run` declares `-f/--force` as non-global.
2677            &["test", "run", "--force", "mytask"][..],
2678            &["test", "run", "-f", "mytask"][..],
2679            // Mixed with a global before the subcommand.
2680            &["test", "-E", "prod", "run", "--force", "mytask"][..],
2681        ] {
2682            let parsed = parse_partial(&spec, &input(words)).unwrap();
2683            assert_eq!(
2684                parsed
2685                    .cmds
2686                    .iter()
2687                    .map(|c| c.name.as_str())
2688                    .collect::<Vec<_>>(),
2689                vec!["test", "run", "mytask"],
2690                "{words:?} should descend into the mounted command",
2691            );
2692            assert!(
2693                parsed.args.is_empty(),
2694                "{words:?} should not consume a positional, got {:?}",
2695                parsed.args,
2696            );
2697            assert_eq!(
2698                parsed.as_env().get("usage_force").map(String::as_str),
2699                Some("true"),
2700                "the non-global flag must still be recorded for {words:?}",
2701            );
2702        }
2703
2704        // A non-global flag that takes a value consumes it, rather than reading the value as the
2705        // subcommand.
2706        let mut run_cmd = SpecCommand::builder()
2707            .name("run")
2708            .flag(
2709                SpecFlag::builder()
2710                    .name("output")
2711                    .short('o')
2712                    .long("output")
2713                    .arg(SpecArg::builder().name("mode").build())
2714                    .global(false)
2715                    .build(),
2716            )
2717            .build();
2718        run_cmd.subcommands.insert(
2719            "task".to_string(),
2720            SpecCommand::builder().name("task").build(),
2721        );
2722        let mut cmd = SpecCommand::builder().name("test").build();
2723        cmd.subcommands.insert("run".to_string(), run_cmd);
2724        let spec = Spec {
2725            name: "test".to_string(),
2726            bin: "test".to_string(),
2727            cmd,
2728            ..Default::default()
2729        };
2730
2731        let parsed =
2732            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
2733        assert_eq!(
2734            parsed
2735                .cmds
2736                .iter()
2737                .map(|c| c.name.as_str())
2738                .collect::<Vec<_>>(),
2739            vec!["test", "run", "task"],
2740        );
2741        assert_eq!(
2742            parsed.as_env().get("usage_output").map(String::as_str),
2743            Some("quiet"),
2744        );
2745
2746        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
2747        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
2748        assert_parse_err(
2749            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
2750            "unexpected word: --nope",
2751        );
2752    }
2753
2754    #[test]
2755    fn test_non_mounted_subcommand_offers_inherited_globals() {
2756        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
2757        // still both recognized and offered.
2758        let mut run_cmd = SpecCommand::builder().name("run").build();
2759        run_cmd.subcommands.insert(
2760            "nested".to_string(),
2761            SpecCommand::builder().name("nested").build(),
2762        );
2763        let mut cmd = SpecCommand::builder()
2764            .name("test")
2765            .flag(
2766                SpecFlag::builder()
2767                    .name("silent")
2768                    .long("silent")
2769                    .global(true)
2770                    .build(),
2771            )
2772            .build();
2773        cmd.subcommands.insert("run".to_string(), run_cmd);
2774        let spec = Spec {
2775            name: "test".to_string(),
2776            bin: "test".to_string(),
2777            cmd,
2778            ..Default::default()
2779        };
2780
2781        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
2782        assert_eq!(
2783            parsed.completion_flags().keys().collect::<Vec<_>>(),
2784            parsed.available_flags.keys().collect::<Vec<_>>(),
2785        );
2786        assert!(parsed.completion_flags().contains_key("--silent"));
2787    }
2788
2789    #[test]
2790    fn test_subcommand_alias_collision_keeps_last_owner() {
2791        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
2792        // share an alias are resolved. Historically the flattened flag map gave the shared
2793        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
2794        let run_cmd = SpecCommand::builder()
2795            .name("run")
2796            .flag(
2797                SpecFlag::builder()
2798                    .name("alpha")
2799                    .short('x')
2800                    .long("alpha")
2801                    .global(false)
2802                    .build(),
2803            )
2804            .flag(
2805                SpecFlag::builder()
2806                    .name("beta")
2807                    .short('x')
2808                    .long("beta")
2809                    .global(false)
2810                    .build(),
2811            )
2812            .build();
2813        let mut cmd = SpecCommand::builder().name("test").build();
2814        cmd.subcommands.insert("run".to_string(), run_cmd);
2815        let spec = Spec {
2816            name: "test".to_string(),
2817            bin: "test".to_string(),
2818            cmd,
2819            ..Default::default()
2820        };
2821
2822        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2823        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
2824        assert_eq!(
2825            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
2826            Some("beta"),
2827            "the last-declared flag must keep a shared short alias",
2828        );
2829        // Both distinct long aliases remain recognized and point to their own flag.
2830        assert_eq!(
2831            parsed
2832                .available_flags
2833                .get("--alpha")
2834                .map(|f| f.name.as_str()),
2835            Some("alpha"),
2836        );
2837        assert_eq!(
2838            parsed
2839                .available_flags
2840                .get("--beta")
2841                .map(|f| f.name.as_str()),
2842            Some("beta"),
2843        );
2844    }
2845
2846    #[test]
2847    fn test_default_subcommand_same_name_child() {
2848        // Test that default_subcommand doesn't cause issues when the default subcommand
2849        // has a child with the same name (e.g., "run" has a task named "run").
2850        // This verifies we don't switch multiple times or get stuck in a loop.
2851        let run_task = SpecCommand::builder()
2852            .name("run")
2853            .arg(SpecArg::builder().name("args").build())
2854            .build();
2855        let mut run_cmd = SpecCommand::builder().name("run").build();
2856        run_cmd.subcommands.insert("run".to_string(), run_task);
2857
2858        let mut cmd = SpecCommand::builder().name("test").build();
2859        cmd.subcommands.insert("run".to_string(), run_cmd);
2860
2861        let spec = Spec {
2862            name: "test".to_string(),
2863            bin: "test".to_string(),
2864            cmd,
2865            default_subcommand: Some("run".to_string()),
2866            ..Default::default()
2867        };
2868
2869        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
2870        let input = vec!["test".to_string(), "run".to_string()];
2871        let parsed = parse(&spec, &input).unwrap();
2872
2873        // Should have two commands: root and "run"
2874        assert_eq!(parsed.cmds.len(), 2);
2875        assert_eq!(parsed.cmds[0].name, "test");
2876        assert_eq!(parsed.cmds[1].name, "run");
2877
2878        // "test run run" should descend into the "run" task (child of "run" subcommand)
2879        let input = vec![
2880            "test".to_string(),
2881            "run".to_string(),
2882            "run".to_string(),
2883            "hello".to_string(),
2884        ];
2885        let parsed = parse(&spec, &input).unwrap();
2886
2887        assert_eq!(parsed.cmds.len(), 3);
2888        assert_eq!(parsed.cmds[0].name, "test");
2889        assert_eq!(parsed.cmds[1].name, "run");
2890        assert_eq!(parsed.cmds[2].name, "run");
2891        assert_eq!(parsed.args.len(), 1);
2892        let value = parsed.args.values().next().unwrap();
2893        assert_eq!(value.to_string(), "hello");
2894
2895        // Key test case: "test other" should switch to default subcommand "run"
2896        // and treat "other" as a positional arg (not try to switch again because
2897        // "run" also has a "run" child).
2898        let mut run_cmd = SpecCommand::builder()
2899            .name("run")
2900            .arg(SpecArg::builder().name("task").build())
2901            .build();
2902        let run_task = SpecCommand::builder().name("run").build();
2903        run_cmd.subcommands.insert("run".to_string(), run_task);
2904
2905        let mut cmd = SpecCommand::builder().name("test").build();
2906        cmd.subcommands.insert("run".to_string(), run_cmd);
2907
2908        let spec = Spec {
2909            name: "test".to_string(),
2910            bin: "test".to_string(),
2911            cmd,
2912            default_subcommand: Some("run".to_string()),
2913            ..Default::default()
2914        };
2915
2916        let input = vec!["test".to_string(), "other".to_string()];
2917        let parsed = parse(&spec, &input).unwrap();
2918
2919        // Should have two commands: root and "run" (the default)
2920        // We should NOT have switched again to the "run" task child
2921        assert_eq!(parsed.cmds.len(), 2);
2922        assert_eq!(parsed.cmds[0].name, "test");
2923        assert_eq!(parsed.cmds[1].name, "run");
2924
2925        // "other" should be parsed as a positional arg
2926        assert_eq!(parsed.args.len(), 1);
2927        let value = parsed.args.values().next().unwrap();
2928        assert_eq!(value.to_string(), "other");
2929    }
2930
2931    #[test]
2932    fn test_restart_token() {
2933        // Test that restart_token resets argument parsing
2934        let run_cmd = SpecCommand::builder()
2935            .name("run")
2936            .arg(SpecArg::builder().name("task").build())
2937            .restart_token(":::".to_string())
2938            .build();
2939        let mut cmd = SpecCommand::builder().name("test").build();
2940        cmd.subcommands.insert("run".to_string(), run_cmd);
2941
2942        let spec = Spec {
2943            name: "test".to_string(),
2944            bin: "test".to_string(),
2945            cmd,
2946            ..Default::default()
2947        };
2948
2949        // "test run task1 ::: task2" - should end up with task2 as the arg
2950        let input = vec![
2951            "test".to_string(),
2952            "run".to_string(),
2953            "task1".to_string(),
2954            ":::".to_string(),
2955            "task2".to_string(),
2956        ];
2957        let parsed = parse(&spec, &input).unwrap();
2958
2959        // After restart, args were cleared and task2 was parsed
2960        assert_eq!(parsed.args.len(), 1);
2961        let value = parsed.args.values().next().unwrap();
2962        assert_eq!(value.to_string(), "task2");
2963    }
2964
2965    #[test]
2966    fn test_restart_token_multiple() {
2967        // Test multiple restart tokens
2968        let run_cmd = SpecCommand::builder()
2969            .name("run")
2970            .arg(SpecArg::builder().name("task").build())
2971            .restart_token(":::".to_string())
2972            .build();
2973        let mut cmd = SpecCommand::builder().name("test").build();
2974        cmd.subcommands.insert("run".to_string(), run_cmd);
2975
2976        let spec = Spec {
2977            name: "test".to_string(),
2978            bin: "test".to_string(),
2979            cmd,
2980            ..Default::default()
2981        };
2982
2983        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
2984        let input = vec![
2985            "test".to_string(),
2986            "run".to_string(),
2987            "task1".to_string(),
2988            ":::".to_string(),
2989            "task2".to_string(),
2990            ":::".to_string(),
2991            "task3".to_string(),
2992        ];
2993        let parsed = parse(&spec, &input).unwrap();
2994
2995        // After multiple restarts, args were cleared and task3 was parsed
2996        assert_eq!(parsed.args.len(), 1);
2997        let value = parsed.args.values().next().unwrap();
2998        assert_eq!(value.to_string(), "task3");
2999    }
3000
3001    #[test]
3002    fn test_restart_token_clears_flag_awaiting_value() {
3003        // Test that restart_token clears pending flag values
3004        let run_cmd = SpecCommand::builder()
3005            .name("run")
3006            .arg(SpecArg::builder().name("task").build())
3007            .flag(
3008                SpecFlag::builder()
3009                    .name("jobs")
3010                    .long("jobs")
3011                    .arg(SpecArg::builder().name("count").build())
3012                    .build(),
3013            )
3014            .restart_token(":::".to_string())
3015            .build();
3016        let mut cmd = SpecCommand::builder().name("test").build();
3017        cmd.subcommands.insert("run".to_string(), run_cmd);
3018
3019        let spec = Spec {
3020            name: "test".to_string(),
3021            bin: "test".to_string(),
3022            cmd,
3023            ..Default::default()
3024        };
3025
3026        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
3027        let input = vec![
3028            "test".to_string(),
3029            "run".to_string(),
3030            "task1".to_string(),
3031            "--jobs".to_string(),
3032            ":::".to_string(),
3033            "task2".to_string(),
3034        ];
3035        let parsed = parse(&spec, &input).unwrap();
3036
3037        // task2 should be parsed as the task arg, not as --jobs value
3038        assert_eq!(parsed.args.len(), 1);
3039        let value = parsed.args.values().next().unwrap();
3040        assert_eq!(value.to_string(), "task2");
3041        // --jobs should not have a value
3042        assert!(parsed.flag_awaiting_value.is_empty());
3043    }
3044
3045    #[test]
3046    fn test_restart_token_resets_double_dash() {
3047        // Test that restart_token resets the -- separator effect
3048        let run_cmd = SpecCommand::builder()
3049            .name("run")
3050            .arg(SpecArg::builder().name("task").build())
3051            .arg(SpecArg::builder().name("extra_args").var(true).build())
3052            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
3053            .restart_token(":::".to_string())
3054            .build();
3055        let mut cmd = SpecCommand::builder().name("test").build();
3056        cmd.subcommands.insert("run".to_string(), run_cmd);
3057
3058        let spec = Spec {
3059            name: "test".to_string(),
3060            bin: "test".to_string(),
3061            cmd,
3062            ..Default::default()
3063        };
3064
3065        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
3066        let input = vec![
3067            "test".to_string(),
3068            "run".to_string(),
3069            "task1".to_string(),
3070            "--".to_string(),
3071            "extra".to_string(),
3072            ":::".to_string(),
3073            "--verbose".to_string(),
3074            "task2".to_string(),
3075        ];
3076        let parsed = parse(&spec, &input).unwrap();
3077
3078        // --verbose should be parsed as a flag (not an arg) after the restart
3079        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
3080        // task2 should be the arg after restart
3081        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
3082        let value = parsed.args.get(task_arg).unwrap();
3083        assert_eq!(value.to_string(), "task2");
3084    }
3085
3086    #[test]
3087    fn test_double_dashes_without_preserve() {
3088        // Test that variadic args WITHOUT `preserve` skip "--" tokens (default behavior)
3089        let run_cmd = SpecCommand::builder()
3090            .name("run")
3091            .arg(SpecArg::builder().name("args").var(true).build())
3092            .build();
3093        let mut cmd = SpecCommand::builder().name("test").build();
3094        cmd.subcommands.insert("run".to_string(), run_cmd);
3095
3096        let spec = Spec {
3097            name: "test".to_string(),
3098            bin: "test".to_string(),
3099            cmd,
3100            ..Default::default()
3101        };
3102
3103        // "test run arg1 -- arg2 -- arg3" - all double dashes should be skipped
3104        let input = vec![
3105            "test".to_string(),
3106            "run".to_string(),
3107            "arg1".to_string(),
3108            "--".to_string(),
3109            "arg2".to_string(),
3110            "--".to_string(),
3111            "arg3".to_string(),
3112        ];
3113        let parsed = parse(&spec, &input).unwrap();
3114
3115        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3116        let value = parsed.args.get(args_arg).unwrap();
3117        assert_eq!(value.to_string(), "arg1 arg2 arg3");
3118    }
3119
3120    #[test]
3121    fn test_double_dashes_with_preserve() {
3122        // Test that variadic args WITH `preserve` keep all double dashes
3123        let run_cmd = SpecCommand::builder()
3124            .name("run")
3125            .arg(
3126                SpecArg::builder()
3127                    .name("args")
3128                    .var(true)
3129                    .double_dash(SpecDoubleDashChoices::Preserve)
3130                    .build(),
3131            )
3132            .build();
3133        let mut cmd = SpecCommand::builder().name("test").build();
3134        cmd.subcommands.insert("run".to_string(), run_cmd);
3135
3136        let spec = Spec {
3137            name: "test".to_string(),
3138            bin: "test".to_string(),
3139            cmd,
3140            ..Default::default()
3141        };
3142
3143        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
3144        let input = vec![
3145            "test".to_string(),
3146            "run".to_string(),
3147            "arg1".to_string(),
3148            "--".to_string(),
3149            "arg2".to_string(),
3150            "--".to_string(),
3151            "arg3".to_string(),
3152        ];
3153        let parsed = parse(&spec, &input).unwrap();
3154
3155        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3156        let value = parsed.args.get(args_arg).unwrap();
3157        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
3158    }
3159
3160    #[test]
3161    fn test_double_dashes_with_preserve_only_dashes() {
3162        // Test that variadic args WITH `preserve` keep all double dashes even
3163        // if the values are just double dashes
3164        let run_cmd = SpecCommand::builder()
3165            .name("run")
3166            .arg(
3167                SpecArg::builder()
3168                    .name("args")
3169                    .var(true)
3170                    .double_dash(SpecDoubleDashChoices::Preserve)
3171                    .build(),
3172            )
3173            .build();
3174        let mut cmd = SpecCommand::builder().name("test").build();
3175        cmd.subcommands.insert("run".to_string(), run_cmd);
3176
3177        let spec = Spec {
3178            name: "test".to_string(),
3179            bin: "test".to_string(),
3180            cmd,
3181            ..Default::default()
3182        };
3183
3184        // "test run -- --" - all double dashes should be preserved
3185        let input = vec![
3186            "test".to_string(),
3187            "run".to_string(),
3188            "--".to_string(),
3189            "--".to_string(),
3190        ];
3191        let parsed = parse(&spec, &input).unwrap();
3192
3193        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3194        let value = parsed.args.get(args_arg).unwrap();
3195        assert_eq!(value.to_string(), "-- --");
3196    }
3197
3198    #[test]
3199    fn test_double_dashes_with_preserve_multiple_args() {
3200        // Test with multiple args where only the second has has `preserve`
3201        let run_cmd = SpecCommand::builder()
3202            .name("run")
3203            .arg(SpecArg::builder().name("task").build())
3204            .arg(
3205                SpecArg::builder()
3206                    .name("extra_args")
3207                    .var(true)
3208                    .double_dash(SpecDoubleDashChoices::Preserve)
3209                    .build(),
3210            )
3211            .build();
3212        let mut cmd = SpecCommand::builder().name("test").build();
3213        cmd.subcommands.insert("run".to_string(), run_cmd);
3214
3215        let spec = Spec {
3216            name: "test".to_string(),
3217            bin: "test".to_string(),
3218            cmd,
3219            ..Default::default()
3220        };
3221
3222        // The first arg "task1" is captured normally
3223        // Then extra_args with `preserve` captures everything, including the "--" tokens
3224        let input = vec![
3225            "test".to_string(),
3226            "run".to_string(),
3227            "task1".to_string(),
3228            "--".to_string(),
3229            "arg1".to_string(),
3230            "--".to_string(),
3231            "--foo".to_string(),
3232        ];
3233        let parsed = parse(&spec, &input).unwrap();
3234
3235        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
3236        let task_value = parsed.args.get(task_arg).unwrap();
3237        assert_eq!(task_value.to_string(), "task1");
3238
3239        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
3240        let extra_value = parsed.args.get(extra_arg).unwrap();
3241        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
3242    }
3243
3244    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
3245        let cmd = SpecCommand::builder().name("test").args(args).build();
3246        Spec {
3247            name: "test".to_string(),
3248            bin: "test".to_string(),
3249            cmd,
3250            ..Default::default()
3251        }
3252    }
3253
3254    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
3255        let arg = parsed
3256            .args
3257            .keys()
3258            .find(|a| a.name == name)
3259            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
3260        parsed.args.get(arg).unwrap().to_string()
3261    }
3262
3263    fn required_arg(name: &str) -> SpecArg {
3264        SpecArg::builder()
3265            .name(name)
3266            .var(true)
3267            .required(false)
3268            .double_dash(SpecDoubleDashChoices::Required)
3269            .build()
3270    }
3271
3272    #[test]
3273    fn test_double_dash_required_reports_error_once_for_variadic() {
3274        // A variadic arg is offered every remaining word, but the mistake is one mistake.
3275        let spec = spec_with_args([required_arg("files")]);
3276
3277        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
3278
3279        assert!(parsed.args.is_empty());
3280        assert_eq!(parsed.errors.len(), 1);
3281        assert!(
3282            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
3283        );
3284    }
3285
3286    #[test]
3287    fn test_double_dash_required_suppresses_missing_arg() {
3288        // The arg is never filled, so the end-of-parse check would also call it missing.
3289        let spec = spec_with_args([SpecArg::builder()
3290            .name("file")
3291            .required(true)
3292            .double_dash(SpecDoubleDashChoices::Required)
3293            .build()]);
3294
3295        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
3296
3297        assert_eq!(parsed.errors.len(), 1);
3298        assert!(matches!(
3299            &parsed.errors[0],
3300            UsageErr::ArgRequiresDoubleDash(_)
3301        ));
3302        // The cursor stays put, so a completion keeps offering the same arg.
3303        assert_eq!(
3304            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
3305            Some("file")
3306        );
3307        assert!(!parsed.double_dash_seen);
3308    }
3309
3310    #[test]
3311    fn test_double_dash_routes_to_required_arg() {
3312        // Everything after `--` belongs to the arg that requires it, even though the greedy
3313        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
3314        let spec = spec_with_args([
3315            SpecArg::builder()
3316                .name("tool")
3317                .var(true)
3318                .required(false)
3319                .build(),
3320            required_arg("command"),
3321        ]);
3322
3323        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
3324
3325        assert_eq!(arg_value(&parsed, "tool"), "node@20");
3326        assert_eq!(arg_value(&parsed, "command"), "node app.js");
3327        assert!(parsed.double_dash_seen);
3328    }
3329
3330    #[test]
3331    fn test_double_dash_routes_with_gap_reports_missing_arg() {
3332        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
3333        // "is it filled?" check cannot be a count of how many args were filled.
3334        let spec = spec_with_args([
3335            SpecArg::builder()
3336                .name("tool")
3337                .var(true)
3338                .required(true)
3339                .build(),
3340            required_arg("command"),
3341        ]);
3342
3343        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
3344
3345        assert_eq!(arg_value(&parsed, "command"), "ls");
3346        assert!(parsed.args.keys().all(|a| a.name != "tool"));
3347        assert!(parsed
3348            .errors
3349            .iter()
3350            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
3351    }
3352
3353    #[test]
3354    fn test_double_dash_gap_applies_defaults() {
3355        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
3356        let spec = spec_with_args([
3357            SpecArg::builder()
3358                .name("tool")
3359                .var(true)
3360                .required(false)
3361                .default_value("node@20")
3362                .build(),
3363            required_arg("command"),
3364        ]);
3365
3366        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
3367
3368        assert_eq!(arg_value(&parsed, "command"), "ls");
3369        assert_eq!(arg_value(&parsed, "tool"), "node@20");
3370    }
3371
3372    fn spec_with_restart_token_and_required_arg() -> Spec {
3373        let run_cmd = SpecCommand::builder()
3374            .name("run")
3375            .arg(SpecArg::builder().name("task").build())
3376            .arg(required_arg("run_args"))
3377            .restart_token(":::".to_string())
3378            .build();
3379        let mut cmd = SpecCommand::builder().name("test").build();
3380        cmd.subcommands.insert("run".to_string(), run_cmd);
3381        Spec {
3382            name: "test".to_string(),
3383            bin: "test".to_string(),
3384            cmd,
3385            ..Default::default()
3386        }
3387    }
3388
3389    #[test]
3390    fn test_double_dash_required_restart_token_resets_separator() {
3391        // The `--` before `:::` belongs to the previous invocation only.
3392        let spec = spec_with_restart_token_and_required_arg();
3393
3394        let parsed = parse_partial(
3395            &spec,
3396            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
3397        )
3398        .unwrap();
3399
3400        assert_eq!(arg_value(&parsed, "task"), "task2");
3401        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
3402        // Reported once even though the arg was violated after already succeeding once.
3403        assert_eq!(
3404            parsed
3405                .errors
3406                .iter()
3407                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
3408                .count(),
3409            1
3410        );
3411    }
3412
3413    #[test]
3414    fn test_double_dash_required_restart_token_accepts_new_separator() {
3415        let spec = spec_with_restart_token_and_required_arg();
3416
3417        let parsed = parse(
3418            &spec,
3419            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
3420        )
3421        .unwrap();
3422
3423        assert_eq!(arg_value(&parsed, "task"), "task2");
3424        assert_eq!(arg_value(&parsed, "run_args"), "c");
3425    }
3426
3427    #[test]
3428    fn test_double_dash_preserve_is_not_a_separator() {
3429        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
3430        // arg that requires a separator. Deliberate: one token cannot be both.
3431        let spec = spec_with_args([
3432            SpecArg::builder()
3433                .name("kept")
3434                .var(true)
3435                .var_max(1)
3436                .required(false)
3437                .double_dash(SpecDoubleDashChoices::Preserve)
3438                .build(),
3439            required_arg("rest"),
3440        ]);
3441
3442        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
3443
3444        assert_eq!(arg_value(&parsed, "kept"), "--");
3445        assert!(parsed.args.keys().all(|a| a.name != "rest"));
3446        assert!(!parsed.double_dash_seen);
3447        assert_eq!(parsed.errors.len(), 1);
3448    }
3449
3450    #[test]
3451    fn test_double_dash_required_does_not_bail_in_parse_partial() {
3452        // Completions parse half-typed command lines; they must still get a result.
3453        let spec = spec_with_args([required_arg("file")]);
3454
3455        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
3456        assert!(parse(&spec, &input(&["test", "x"])).is_err());
3457    }
3458
3459    #[test]
3460    fn test_double_dash_without_required_arg_does_not_move_cursor() {
3461        // Specs with no `double_dash="required"` arg are untouched by the jump.
3462        let spec = spec_with_args([
3463            SpecArg::builder().name("first").required(false).build(),
3464            SpecArg::builder().name("second").required(false).build(),
3465        ]);
3466
3467        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
3468
3469        assert_eq!(arg_value(&parsed, "first"), "a");
3470        assert_eq!(arg_value(&parsed, "second"), "b");
3471        assert!(parsed.next_arg.is_none());
3472    }
3473
3474    #[test]
3475    fn test_parser_with_custom_env_for_required_arg() {
3476        let spec = spec_with_arg(
3477            SpecArg::builder()
3478                .name("name")
3479                .env("NAME")
3480                .required(true)
3481                .build(),
3482        );
3483        std::env::remove_var("NAME");
3484
3485        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
3486            .expect("parse should succeed with custom env");
3487        assert_eq!(parsed.args.len(), 1);
3488        assert_eq!(first_string_value(&parsed), "john");
3489    }
3490
3491    #[test]
3492    fn test_parser_with_custom_env_for_required_flag() {
3493        let spec = spec_with_flag(
3494            SpecFlag::builder()
3495                .long("name")
3496                .env("NAME")
3497                .required(true)
3498                .arg(SpecArg::builder().name("name").build())
3499                .build(),
3500        );
3501        std::env::remove_var("NAME");
3502
3503        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
3504            .expect("parse should succeed with custom env");
3505        assert_eq!(parsed.flags.len(), 1);
3506        assert_eq!(first_string_value(&parsed), "jane");
3507    }
3508
3509    #[test]
3510    fn test_parser_with_custom_env_still_fails_when_missing() {
3511        let spec = spec_with_arg(
3512            SpecArg::builder()
3513                .name("name")
3514                .env("NAME")
3515                .required(true)
3516                .build(),
3517        );
3518        std::env::remove_var("NAME");
3519        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
3520    }
3521
3522    #[test]
3523    fn test_parser_does_not_treat_env_choice_value_as_help() {
3524        let spec = spec_with_arg(
3525            SpecArg::builder()
3526                .name("env")
3527                .env("CURRENT_ENV")
3528                .choices(["dev", "staging"])
3529                .required(false)
3530                .build(),
3531        );
3532
3533        assert_parse_err(
3534            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
3535            "Invalid choice for arg env: --help, expected one of dev, staging",
3536        );
3537    }
3538
3539    #[test]
3540    fn test_parser_does_not_treat_default_choice_value_as_help() {
3541        let spec = spec_with_flag(
3542            SpecFlag::builder()
3543                .long("env")
3544                .arg(
3545                    SpecArg::builder()
3546                        .name("env")
3547                        .choices(["dev", "staging"])
3548                        .build(),
3549                )
3550                .default_value("--help")
3551                .build(),
3552        );
3553
3554        assert_parse_err(
3555            parse_with_env(&spec, &["test"], &[]),
3556            "Invalid choice for option env: --help, expected one of dev, staging",
3557        );
3558    }
3559
3560    #[cfg(feature = "unstable_choices_env")]
3561    #[test]
3562    fn test_parser_arg_choices_from_custom_env() {
3563        let spec = spec_arg_choices_env("DEPLOY_ENVS");
3564
3565        let parsed =
3566            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
3567        assert_eq!(first_string_value(&parsed), "bar");
3568
3569        assert_parse_err(
3570            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
3571            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
3572        );
3573        assert_parse_err(
3574            parse_with_env(&spec, &["test", "prod"], &[]),
3575            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
3576        );
3577    }
3578
3579    #[cfg(feature = "unstable_choices_env")]
3580    #[test]
3581    fn test_parser_validates_flag_choices_from_custom_env() {
3582        let spec = spec_flag_choices_env("DEPLOY_ENVS");
3583        let parsed = parse_with_env(
3584            &spec,
3585            &["test", "--env", "baz"],
3586            &[("DEPLOY_ENVS", "foo,bar baz")],
3587        )
3588        .unwrap();
3589        assert_eq!(first_string_value(&parsed), "baz");
3590    }
3591
3592    #[cfg(feature = "unstable_choices_env")]
3593    #[test]
3594    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
3595        let arg_env_spec = spec_with_arg(
3596            SpecArg::builder()
3597                .name("env")
3598                .env("CURRENT_ENV")
3599                .choices_env("DEPLOY_ENVS")
3600                .build(),
3601        );
3602        assert_parse_err(
3603            parse_with_env(
3604                &arg_env_spec,
3605                &["test"],
3606                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
3607            ),
3608            "Invalid choice for arg env: prod, expected one of dev, staging",
3609        );
3610
3611        let flag_default_spec = spec_with_flag(
3612            SpecFlag::builder()
3613                .long("env")
3614                .arg(
3615                    SpecArg::builder()
3616                        .name("env")
3617                        .choices_env("DEPLOY_ENVS")
3618                        .build(),
3619                )
3620                .default_value("prod")
3621                .build(),
3622        );
3623        assert_parse_err(
3624            parse_with_env(
3625                &flag_default_spec,
3626                &["test"],
3627                &[("DEPLOY_ENVS", "dev,staging")],
3628            ),
3629            "Invalid choice for option env: prod, expected one of dev, staging",
3630        );
3631    }
3632
3633    #[test]
3634    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
3635        let spec: Spec = r#"
3636            flag "-v --verbose" var=#true
3637            arg "[database]" default="myapp_dev"
3638            arg "[args...]"
3639        "#
3640        .parse()
3641        .unwrap();
3642        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3643            .into_iter()
3644            .map(String::from)
3645            .collect();
3646        let parsed = parse(&spec, &input).unwrap();
3647        let env = parsed.as_env();
3648        assert_eq!(env.get("usage_database").unwrap(), "mydb");
3649        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
3650    }
3651
3652    #[test]
3653    fn test_variadic_arg_captures_unknown_flags() {
3654        let cmd = SpecCommand::builder()
3655            .name("test")
3656            .flag(SpecFlag::builder().short('v').long("verbose").build())
3657            .arg(SpecArg::builder().name("database").required(false).build())
3658            .arg(
3659                SpecArg::builder()
3660                    .name("args")
3661                    .required(false)
3662                    .var(true)
3663                    .build(),
3664            )
3665            .build();
3666        let spec = Spec {
3667            name: "test".to_string(),
3668            bin: "test".to_string(),
3669            cmd,
3670            ..Default::default()
3671        };
3672
3673        // Unknown --host flag and its value should be captured by [args...]
3674        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3675            .into_iter()
3676            .map(String::from)
3677            .collect();
3678        let parsed = parse(&spec, &input).unwrap();
3679        assert_eq!(parsed.args.len(), 2);
3680        let args_val = parsed
3681            .args
3682            .iter()
3683            .find(|(a, _)| a.name == "args")
3684            .unwrap()
3685            .1;
3686        match args_val {
3687            ParseValue::MultiString(v) => {
3688                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3689            }
3690            _ => panic!("Expected MultiString, got {:?}", args_val),
3691        }
3692    }
3693
3694    #[test]
3695    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
3696        let cmd = SpecCommand::builder()
3697            .name("test")
3698            .flag(SpecFlag::builder().short('v').long("verbose").build())
3699            .arg(SpecArg::builder().name("database").required(false).build())
3700            .arg(
3701                SpecArg::builder()
3702                    .name("args")
3703                    .required(false)
3704                    .var(true)
3705                    .build(),
3706            )
3707            .build();
3708        let spec = Spec {
3709            name: "test".to_string(),
3710            bin: "test".to_string(),
3711            cmd,
3712            ..Default::default()
3713        };
3714
3715        // With explicit -- separator
3716        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
3717            .into_iter()
3718            .map(String::from)
3719            .collect();
3720        let parsed = parse(&spec, &input).unwrap();
3721        assert_eq!(parsed.args.len(), 2);
3722        let args_val = parsed
3723            .args
3724            .iter()
3725            .find(|(a, _)| a.name == "args")
3726            .unwrap()
3727            .1;
3728        match args_val {
3729            ParseValue::MultiString(v) => {
3730                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3731            }
3732            _ => panic!("Expected MultiString, got {:?}", args_val),
3733        }
3734    }
3735
3736    #[test]
3737    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
3738        // Regression: --flag=value should be treated as a single positional token when
3739        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
3740        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
3741
3742        // Single unknown --flag=value: must not produce a stray "3" positional.
3743        // as_env() shell-joins via shell_words::join, so "=" gets quoted.
3744        let input: Vec<String> = vec!["test", "--option=3"]
3745            .into_iter()
3746            .map(String::from)
3747            .collect();
3748        let parsed = parse(&spec, &input).unwrap();
3749        let env = parsed.as_env();
3750        assert_eq!(
3751            env.get("usage_other_args").map(String::as_str),
3752            Some("'--option=3'"),
3753            "expected a single --option=3 token, got {:?}",
3754            env.get("usage_other_args"),
3755        );
3756
3757        // Multiple unknown --flag=value args should each be kept intact
3758        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
3759            .into_iter()
3760            .map(String::from)
3761            .collect();
3762        let parsed2 = parse(&spec, &input2).unwrap();
3763        let env2 = parsed2.as_env();
3764        assert_eq!(
3765            env2.get("usage_other_args").map(String::as_str),
3766            Some("'--foo=bar' '--baz=qux'"),
3767            "expected two intact tokens, got {:?}",
3768            env2.get("usage_other_args"),
3769        );
3770
3771        // Mix of plain positional args and unknown --flag=value tokens
3772        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
3773            .into_iter()
3774            .map(String::from)
3775            .collect();
3776        let parsed3 = parse(&spec, &input3).unwrap();
3777        let env3 = parsed3.as_env();
3778        assert_eq!(
3779            env3.get("usage_other_args").map(String::as_str),
3780            Some("positional1 '--option=3' positional2"),
3781            "expected positional args and intact flag token, got {:?}",
3782            env3.get("usage_other_args"),
3783        );
3784    }
3785
3786    #[test]
3787    fn test_allow_hyphen_values_consumes_short_flag_collision() {
3788        let spec = r#"
3789flag "-d --working-dir <DIR>"
3790flag "-a --args <ARGS>" allow_hyphen_values=#true
3791"#
3792        .parse::<Spec>()
3793        .unwrap();
3794
3795        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3796
3797        assert_eq!(parsed.flags.len(), 1);
3798        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3799    }
3800
3801    #[test]
3802    fn test_allow_hyphen_values_consumes_embedded_long_value() {
3803        let spec = r#"
3804flag "-d --working-dir <DIR>"
3805flag "-a --args <ARGS>" allow_hyphen_values=#true
3806"#
3807        .parse::<Spec>()
3808        .unwrap();
3809
3810        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
3811
3812        assert_eq!(parsed.flags.len(), 1);
3813        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3814    }
3815
3816    #[test]
3817    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
3818        let spec = r#"
3819flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
3820"#
3821        .parse::<Spec>()
3822        .unwrap();
3823
3824        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
3825
3826        let flag = parsed
3827            .flags
3828            .keys()
3829            .find(|flag| flag.name == "args")
3830            .expect("expected args flag");
3831        let value = parsed.flags.get(flag).expect("expected args value");
3832        match value {
3833            ParseValue::MultiString(values) => {
3834                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
3835            }
3836            _ => panic!("expected MultiString, got {value:?}"),
3837        }
3838    }
3839
3840    #[test]
3841    fn test_hyphen_values_still_default_to_short_flag_parsing() {
3842        let spec = r#"
3843flag "-d --working-dir <DIR>"
3844flag "-a --args <ARGS>"
3845"#
3846        .parse::<Spec>()
3847        .unwrap();
3848
3849        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3850
3851        assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
3852    }
3853
3854    /// `available_flags` has to agree with what an actual parse accepts, since
3855    /// its whole reason to exist is answering that question without one.
3856    mod available_flags {
3857        use super::*;
3858
3859        fn spec() -> Spec {
3860            r#"
3861bin "test"
3862flag "-v --verbose" global=#true
3863flag "--raw" global=#true effect="write"
3864flag "--local-only"
3865cmd "run" {
3866    flag "-r --raw"
3867    flag "-w --watch"
3868    cmd "once"
3869}
3870"#
3871            .parse::<Spec>()
3872            .unwrap()
3873        }
3874
3875        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
3876            let mut chain = vec![&spec.cmd];
3877            for segment in path {
3878                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
3879            }
3880            chain
3881        }
3882
3883        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
3884            let mut names: Vec<_> = available_flags(&chain(spec, path))
3885                .iter()
3886                .map(|f| f.name.clone())
3887                .collect();
3888            names.sort();
3889            names
3890        }
3891
3892        #[test]
3893        fn an_empty_chain_yields_nothing() {
3894            assert!(available_flags(&[]).is_empty());
3895        }
3896
3897        #[test]
3898        fn the_root_gets_its_own_flags() {
3899            let spec = spec();
3900            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
3901        }
3902
3903        #[test]
3904        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
3905            let spec = spec();
3906            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
3907        }
3908
3909        #[test]
3910        fn a_re_declared_global_is_listed_once() {
3911            // The merge can leave the long key on the merged flag and the short
3912            // key on the pre-merge one. Same flag; it must not be listed twice.
3913            let spec = r#"
3914bin "test"
3915flag "-y --yes" global=#true effect="write"
3916cmd "rm" {
3917    flag "-y --yes"
3918}
3919"#
3920            .parse::<Spec>()
3921            .unwrap();
3922            let flags = available_flags(&chain(&spec, &["rm"]));
3923            assert_eq!(flags.len(), 1, "{flags:?}");
3924            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
3925        }
3926
3927        #[test]
3928        fn a_re_declared_global_keeps_the_globals_declaration() {
3929            // `run` re-declares the long-only global `--raw` as `-r --raw`
3930            // without `global`. That is the same flag: the global's `effect`
3931            // survives, the orphan short is unioned in, and it stays global.
3932            let spec = spec();
3933            let flags = available_flags(&chain(&spec, &["run"]));
3934            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
3935            assert!(raw.global);
3936            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
3937            assert_eq!(raw.short, ['r']);
3938        }
3939
3940        #[test]
3941        fn it_matches_what_a_parse_accepts() {
3942            // The invariant. If these ever disagree, one of them is lying to a
3943            // caller about which flags a command takes.
3944            let spec = spec();
3945            for path in [vec![], vec!["run"], vec!["run", "once"]] {
3946                let argv = std::iter::once("test".to_string())
3947                    .chain(path.iter().map(|s| s.to_string()))
3948                    .collect::<Vec<_>>();
3949                let parsed = parse_partial(&spec, &argv).unwrap();
3950
3951                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
3952                    .map(|f| f.name.clone())
3953                    .collect();
3954                from_parse.sort();
3955                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
3956            }
3957        }
3958    }
3959}