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