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