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