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