Skip to main content

usage/
parse.rs

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