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