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
284    while idx < input.len() {
285        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
286            let mut subcommand = subcommand.clone();
287            // Pass prefix words (global flags before this subcommand) to mount
288            subcommand.mount(&prefix_words)?;
289            out.available_flags.retain(|_, f| f.global);
290            out.available_flags.extend(gather_flags(&subcommand));
291            // Remove subcommand from input
292            input.remove(idx);
293            out.cmds.push(subcommand.clone());
294            out.cmd = subcommand.clone();
295            prefix_words.clear();
296            // Continue from current position (don't reset to 0)
297            // After remove(), idx now points to the next element
298        } else if input[idx].starts_with('-') {
299            // Check if this is a known flag and if it's global
300            let word = &input[idx];
301            let flag_key = get_flag_key(word);
302
303            if let Some(f) = out.available_flags.get(flag_key) {
304                // Only collect global flags for mount execution
305                if f.global {
306                    prefix_words.push(input[idx].clone());
307                    idx += 1;
308
309                    // Only consume next word if flag takes an argument AND value isn't embedded
310                    // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
311                    if f.arg.is_some()
312                        && !word.contains('=')
313                        && idx < input.len()
314                        && !input[idx].starts_with('-')
315                    {
316                        prefix_words.push(input[idx].clone());
317                        idx += 1;
318                    }
319                } else {
320                    // Non-global flag encountered - stop subcommand search
321                    // This prevents incorrect parsing like: "cmd --local-flag run"
322                    // where "run" might be mistaken for a subcommand
323                    break;
324                }
325            } else {
326                // Unknown flag - stop looking for subcommands
327                // Let the main parsing phase handle the error
328                break;
329            }
330        } else {
331            // Found a word that's not a flag or subcommand
332            // Check if we should use the default_subcommand
333            if let Some(default_name) = &spec.default_subcommand {
334                if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
335                    let mut subcommand = subcommand.clone();
336                    // Pass prefix words (global flags before this) to mount
337                    subcommand.mount(&prefix_words)?;
338                    out.available_flags.retain(|_, f| f.global);
339                    out.available_flags.extend(gather_flags(&subcommand));
340                    out.cmds.push(subcommand.clone());
341                    out.cmd = subcommand.clone();
342                    prefix_words.clear();
343                    // Don't remove the current word - it's an argument to the default subcommand
344                    // Don't increment idx - let Phase 2 handle this word as a positional arg
345                    break;
346                }
347            }
348            // This could be a positional argument, so stop subcommand search
349            break;
350        }
351    }
352
353    // Phase 2: Main argument and flag parsing
354    //
355    // Now that we've identified all subcommands and executed their mounts,
356    // we can parse the remaining arguments, flags, and their values.
357    let mut next_arg = out.cmd.args.first();
358    let mut enable_flags = true;
359    let mut grouped_flag = false;
360
361    while !input.is_empty() {
362        let mut w = input.pop_front().unwrap();
363
364        // Check for restart_token - resets argument parsing for multiple command invocations
365        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
366        if let Some(ref restart_token) = out.cmd.restart_token {
367            if w == *restart_token {
368                // Reset argument parsing state for a fresh command invocation
369                out.args.clear();
370                next_arg = out.cmd.args.first();
371                out.flag_awaiting_value.clear(); // Clear any pending flag values
372                enable_flags = true; // Reset -- separator effect
373                                     // Keep flags and continue parsing
374                continue;
375            }
376        }
377
378        if w == "--" {
379            // Always disable flag parsing after seeing a "--" token
380            enable_flags = false;
381
382            // Only preserve the double dash token if we're collecting values for a variadic arg
383            // in double_dash == `preserve` mode
384            let should_preserve = next_arg
385                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
386                .unwrap_or(false);
387
388            if should_preserve {
389                // Fall through to arg parsing
390            } else {
391                // Default behavior, skip the token
392                continue;
393            }
394        }
395
396        // long flags
397        if enable_flags && w.starts_with("--") {
398            grouped_flag = false;
399            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
400            if !val.is_empty() {
401                input.push_front(val.to_string());
402            }
403            if let Some(f) = out.available_flags.get(word) {
404                if f.arg.is_some() {
405                    out.flag_awaiting_value.push(Arc::clone(f));
406                } else if f.count {
407                    let arr = out
408                        .flags
409                        .entry(Arc::clone(f))
410                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
411                        .try_as_multi_bool_mut()
412                        .unwrap();
413                    arr.push(true);
414                } else {
415                    let negate = f.negate.clone().unwrap_or_default();
416                    out.flags
417                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
418                }
419                continue;
420            }
421            if is_help_arg(spec, &w) {
422                out.errors
423                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
424                return Ok(out);
425            }
426        }
427
428        // short flags
429        if enable_flags && w.starts_with('-') && w.len() > 1 {
430            let short = w.chars().nth(1).unwrap();
431            if let Some(f) = out.available_flags.get(&format!("-{short}")) {
432                if w.len() > 2 {
433                    input.push_front(format!("-{}", &w[2..]));
434                    grouped_flag = true;
435                }
436                if f.arg.is_some() {
437                    out.flag_awaiting_value.push(Arc::clone(f));
438                } else if f.count {
439                    let arr = out
440                        .flags
441                        .entry(Arc::clone(f))
442                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
443                        .try_as_multi_bool_mut()
444                        .unwrap();
445                    arr.push(true);
446                } else {
447                    let negate = f.negate.clone().unwrap_or_default();
448                    out.flags
449                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
450                }
451                continue;
452            }
453            if is_help_arg(spec, &w) {
454                out.errors
455                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
456                return Ok(out);
457            }
458            if grouped_flag {
459                grouped_flag = false;
460                w.remove(0);
461            }
462        }
463
464        if !out.flag_awaiting_value.is_empty() {
465            while let Some(flag) = out.flag_awaiting_value.pop() {
466                let arg = flag.arg.as_ref().unwrap();
467                if flag.var {
468                    let arr = out
469                        .flags
470                        .entry(flag)
471                        .or_insert_with(|| ParseValue::MultiString(vec![]))
472                        .try_as_multi_string_mut()
473                        .unwrap();
474                    arr.push(w);
475                } else {
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                    out.flags.insert(flag, ParseValue::String(w));
491                }
492                w = "".to_string();
493            }
494            continue;
495        }
496
497        if let Some(arg) = next_arg {
498            if arg.var {
499                let arr = out
500                    .args
501                    .entry(Arc::new(arg.clone()))
502                    .or_insert_with(|| ParseValue::MultiString(vec![]))
503                    .try_as_multi_string_mut()
504                    .unwrap();
505                arr.push(w);
506                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
507                    next_arg = out.cmd.args.get(out.args.len());
508                }
509            } else {
510                if let Some(choices) = &arg.choices {
511                    if !choices.choices.contains(&w) {
512                        if is_help_arg(spec, &w) {
513                            out.errors
514                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
515                            return Ok(out);
516                        }
517                        bail!(
518                            "Invalid choice for arg {}: {w}, expected one of {}",
519                            arg.name,
520                            choices.choices.join(", ")
521                        );
522                    }
523                }
524                out.args
525                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
526                next_arg = out.cmd.args.get(out.args.len());
527            }
528            continue;
529        }
530        if is_help_arg(spec, &w) {
531            out.errors
532                .push(render_help_err(spec, &out.cmd, w.len() > 2));
533            return Ok(out);
534        }
535        bail!("unexpected word: {w}");
536    }
537
538    for arg in out.cmd.args.iter().skip(out.args.len()) {
539        if arg.required && arg.default.is_empty() {
540            // Check if there's an env var available (custom env map takes precedence)
541            let has_env = arg.env.as_ref().is_some_and(|e| {
542                custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
543                    || std::env::var(e).is_ok()
544            });
545            if !has_env {
546                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
547            }
548        }
549    }
550
551    for flag in out.available_flags.values() {
552        if out.flags.contains_key(flag) {
553            continue;
554        }
555        let has_default =
556            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
557        // Check if there's an env var available (custom env map takes precedence)
558        let has_env = flag.env.as_ref().is_some_and(|e| {
559            custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
560        });
561        if flag.required && !has_default && !has_env {
562            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
563        }
564    }
565
566    // Validate var_min/var_max constraints for variadic args
567    for (arg, value) in &out.args {
568        if arg.var {
569            if let ParseValue::MultiString(values) = value {
570                if let Some(min) = arg.var_min {
571                    if values.len() < min {
572                        out.errors.push(UsageErr::VarArgTooFew {
573                            name: arg.name.clone(),
574                            min,
575                            got: values.len(),
576                        });
577                    }
578                }
579                if let Some(max) = arg.var_max {
580                    if values.len() > max {
581                        out.errors.push(UsageErr::VarArgTooMany {
582                            name: arg.name.clone(),
583                            max,
584                            got: values.len(),
585                        });
586                    }
587                }
588            }
589        }
590    }
591
592    // Validate var_min/var_max constraints for variadic flags
593    for (flag, value) in &out.flags {
594        if flag.var {
595            let count = match value {
596                ParseValue::MultiString(values) => values.len(),
597                ParseValue::MultiBool(values) => values.len(),
598                _ => continue,
599            };
600            if let Some(min) = flag.var_min {
601                if count < min {
602                    out.errors.push(UsageErr::VarFlagTooFew {
603                        name: flag.name.clone(),
604                        min,
605                        got: count,
606                    });
607                }
608            }
609            if let Some(max) = flag.var_max {
610                if count > max {
611                    out.errors.push(UsageErr::VarFlagTooMany {
612                        name: flag.name.clone(),
613                        max,
614                        got: count,
615                    });
616                }
617            }
618        }
619    }
620
621    Ok(out)
622}
623
624#[cfg(feature = "docs")]
625fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
626    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
627}
628
629#[cfg(not(feature = "docs"))]
630fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
631    UsageErr::Help("help".to_string())
632}
633
634fn is_help_arg(spec: &Spec, w: &str) -> bool {
635    spec.disable_help != Some(true)
636        && (w == "--help"
637            || w == "-h"
638            || w == "-?"
639            || (spec.cmd.subcommands.is_empty() && w == "help"))
640}
641
642impl ParseOutput {
643    pub fn as_env(&self) -> BTreeMap<String, String> {
644        let mut env = BTreeMap::new();
645        for (flag, val) in &self.flags {
646            let key = format!("usage_{}", flag.name.to_snake_case());
647            let val = match val {
648                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
649                ParseValue::String(s) => s.clone(),
650                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
651                ParseValue::MultiString(s) => shell_words::join(s),
652            };
653            env.insert(key, val);
654        }
655        for (arg, val) in &self.args {
656            let key = format!("usage_{}", arg.name.to_snake_case());
657            env.insert(key, val.to_string());
658        }
659        env
660    }
661}
662
663impl Display for ParseValue {
664    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
665        match self {
666            ParseValue::Bool(b) => write!(f, "{b}"),
667            ParseValue::String(s) => write!(f, "{s}"),
668            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
669            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
670        }
671    }
672}
673
674impl Debug for ParseOutput {
675    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
676        f.debug_struct("ParseOutput")
677            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
678            .field(
679                "args",
680                &self
681                    .args
682                    .iter()
683                    .map(|(a, w)| format!("{}: {w}", &a.name))
684                    .collect_vec(),
685            )
686            .field(
687                "available_flags",
688                &self
689                    .available_flags
690                    .iter()
691                    .map(|(f, w)| format!("{f}: {w}"))
692                    .collect_vec(),
693            )
694            .field(
695                "flags",
696                &self
697                    .flags
698                    .iter()
699                    .map(|(f, w)| format!("{}: {w}", &f.name))
700                    .collect_vec(),
701            )
702            .field("flag_awaiting_value", &self.flag_awaiting_value)
703            .field("errors", &self.errors)
704            .finish()
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn test_parse() {
714        let cmd = SpecCommand::builder()
715            .name("test")
716            .arg(SpecArg::builder().name("arg").build())
717            .flag(SpecFlag::builder().long("flag").build())
718            .build();
719        let spec = Spec {
720            name: "test".to_string(),
721            bin: "test".to_string(),
722            cmd,
723            ..Default::default()
724        };
725        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
726        let parsed = parse(&spec, &input).unwrap();
727        assert_eq!(parsed.cmds.len(), 1);
728        assert_eq!(parsed.cmds[0].name, "test");
729        assert_eq!(parsed.args.len(), 1);
730        assert_eq!(parsed.flags.len(), 1);
731        assert_eq!(parsed.available_flags.len(), 1);
732    }
733
734    #[test]
735    fn test_as_env() {
736        let cmd = SpecCommand::builder()
737            .name("test")
738            .arg(SpecArg::builder().name("arg").build())
739            .flag(SpecFlag::builder().long("flag").build())
740            .flag(
741                SpecFlag::builder()
742                    .long("force")
743                    .negate("--no-force")
744                    .build(),
745            )
746            .build();
747        let spec = Spec {
748            name: "test".to_string(),
749            bin: "test".to_string(),
750            cmd,
751            ..Default::default()
752        };
753        let input = vec![
754            "test".to_string(),
755            "--flag".to_string(),
756            "--no-force".to_string(),
757        ];
758        let parsed = parse(&spec, &input).unwrap();
759        let env = parsed.as_env();
760        assert_eq!(env.len(), 2);
761        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
762        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
763    }
764
765    #[test]
766    fn test_arg_env_var() {
767        let cmd = SpecCommand::builder()
768            .name("test")
769            .arg(
770                SpecArg::builder()
771                    .name("input")
772                    .env("TEST_ARG_INPUT")
773                    .required(true)
774                    .build(),
775            )
776            .build();
777        let spec = Spec {
778            name: "test".to_string(),
779            bin: "test".to_string(),
780            cmd,
781            ..Default::default()
782        };
783
784        // Set env var
785        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
786
787        let input = vec!["test".to_string()];
788        let parsed = parse(&spec, &input).unwrap();
789
790        assert_eq!(parsed.args.len(), 1);
791        let arg = parsed.args.keys().next().unwrap();
792        assert_eq!(arg.name, "input");
793        let value = parsed.args.values().next().unwrap();
794        assert_eq!(value.to_string(), "test_file.txt");
795
796        // Clean up
797        std::env::remove_var("TEST_ARG_INPUT");
798    }
799
800    #[test]
801    fn test_flag_env_var_with_arg() {
802        let cmd = SpecCommand::builder()
803            .name("test")
804            .flag(
805                SpecFlag::builder()
806                    .long("output")
807                    .env("TEST_FLAG_OUTPUT")
808                    .arg(SpecArg::builder().name("file").build())
809                    .build(),
810            )
811            .build();
812        let spec = Spec {
813            name: "test".to_string(),
814            bin: "test".to_string(),
815            cmd,
816            ..Default::default()
817        };
818
819        // Set env var
820        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
821
822        let input = vec!["test".to_string()];
823        let parsed = parse(&spec, &input).unwrap();
824
825        assert_eq!(parsed.flags.len(), 1);
826        let flag = parsed.flags.keys().next().unwrap();
827        assert_eq!(flag.name, "output");
828        let value = parsed.flags.values().next().unwrap();
829        assert_eq!(value.to_string(), "output.txt");
830
831        // Clean up
832        std::env::remove_var("TEST_FLAG_OUTPUT");
833    }
834
835    #[test]
836    fn test_flag_env_var_boolean() {
837        let cmd = SpecCommand::builder()
838            .name("test")
839            .flag(
840                SpecFlag::builder()
841                    .long("verbose")
842                    .env("TEST_FLAG_VERBOSE")
843                    .build(),
844            )
845            .build();
846        let spec = Spec {
847            name: "test".to_string(),
848            bin: "test".to_string(),
849            cmd,
850            ..Default::default()
851        };
852
853        // Set env var to true
854        std::env::set_var("TEST_FLAG_VERBOSE", "true");
855
856        let input = vec!["test".to_string()];
857        let parsed = parse(&spec, &input).unwrap();
858
859        assert_eq!(parsed.flags.len(), 1);
860        let flag = parsed.flags.keys().next().unwrap();
861        assert_eq!(flag.name, "verbose");
862        let value = parsed.flags.values().next().unwrap();
863        assert_eq!(value.to_string(), "true");
864
865        // Clean up
866        std::env::remove_var("TEST_FLAG_VERBOSE");
867    }
868
869    #[test]
870    fn test_env_var_precedence() {
871        // CLI args should take precedence over env vars
872        let cmd = SpecCommand::builder()
873            .name("test")
874            .arg(
875                SpecArg::builder()
876                    .name("input")
877                    .env("TEST_PRECEDENCE_INPUT")
878                    .required(true)
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
890        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
891
892        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
893        let parsed = parse(&spec, &input).unwrap();
894
895        assert_eq!(parsed.args.len(), 1);
896        let value = parsed.args.values().next().unwrap();
897        // CLI arg should take precedence
898        assert_eq!(value.to_string(), "cli_file.txt");
899
900        // Clean up
901        std::env::remove_var("TEST_PRECEDENCE_INPUT");
902    }
903
904    #[test]
905    fn test_flag_var_true_with_single_default() {
906        // When var=true and default="bar", the default should be MultiString(["bar"])
907        let cmd = SpecCommand::builder()
908            .name("test")
909            .flag(
910                SpecFlag::builder()
911                    .long("foo")
912                    .var(true)
913                    .arg(SpecArg::builder().name("foo").build())
914                    .default_value("bar")
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        // User doesn't provide the flag
926        let input = vec!["test".to_string()];
927        let parsed = parse(&spec, &input).unwrap();
928
929        assert_eq!(parsed.flags.len(), 1);
930        let flag = parsed.flags.keys().next().unwrap();
931        assert_eq!(flag.name, "foo");
932        let value = parsed.flags.values().next().unwrap();
933        // Should be MultiString, not String
934        match value {
935            ParseValue::MultiString(v) => {
936                assert_eq!(v.len(), 1);
937                assert_eq!(v[0], "bar");
938            }
939            _ => panic!("Expected MultiString, got {:?}", value),
940        }
941    }
942
943    #[test]
944    fn test_flag_var_true_with_multiple_defaults() {
945        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
946        let cmd = SpecCommand::builder()
947            .name("test")
948            .flag(
949                SpecFlag::builder()
950                    .long("foo")
951                    .var(true)
952                    .arg(SpecArg::builder().name("foo").build())
953                    .default_values(["xyz", "bar"])
954                    .build(),
955            )
956            .build();
957        let spec = Spec {
958            name: "test".to_string(),
959            bin: "test".to_string(),
960            cmd,
961            ..Default::default()
962        };
963
964        // User doesn't provide the flag
965        let input = vec!["test".to_string()];
966        let parsed = parse(&spec, &input).unwrap();
967
968        assert_eq!(parsed.flags.len(), 1);
969        let value = parsed.flags.values().next().unwrap();
970        // Should be MultiString with both values
971        match value {
972            ParseValue::MultiString(v) => {
973                assert_eq!(v.len(), 2);
974                assert_eq!(v[0], "xyz");
975                assert_eq!(v[1], "bar");
976            }
977            _ => panic!("Expected MultiString, got {:?}", value),
978        }
979    }
980
981    #[test]
982    fn test_flag_var_false_with_default_remains_string() {
983        // When var=false (default), the default should still be String("bar")
984        let cmd = SpecCommand::builder()
985            .name("test")
986            .flag(
987                SpecFlag::builder()
988                    .long("foo")
989                    .var(false) // Default behavior
990                    .arg(SpecArg::builder().name("foo").build())
991                    .default_value("bar")
992                    .build(),
993            )
994            .build();
995        let spec = Spec {
996            name: "test".to_string(),
997            bin: "test".to_string(),
998            cmd,
999            ..Default::default()
1000        };
1001
1002        // User doesn't provide the flag
1003        let input = vec!["test".to_string()];
1004        let parsed = parse(&spec, &input).unwrap();
1005
1006        assert_eq!(parsed.flags.len(), 1);
1007        let value = parsed.flags.values().next().unwrap();
1008        // Should be String, not MultiString
1009        match value {
1010            ParseValue::String(s) => {
1011                assert_eq!(s, "bar");
1012            }
1013            _ => panic!("Expected String, got {:?}", value),
1014        }
1015    }
1016
1017    #[test]
1018    fn test_arg_var_true_with_single_default() {
1019        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
1020        let cmd = SpecCommand::builder()
1021            .name("test")
1022            .arg(
1023                SpecArg::builder()
1024                    .name("files")
1025                    .var(true)
1026                    .default_value("default.txt")
1027                    .required(false)
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 arg
1039        let input = vec!["test".to_string()];
1040        let parsed = parse(&spec, &input).unwrap();
1041
1042        assert_eq!(parsed.args.len(), 1);
1043        let value = parsed.args.values().next().unwrap();
1044        // Should be MultiString, not String
1045        match value {
1046            ParseValue::MultiString(v) => {
1047                assert_eq!(v.len(), 1);
1048                assert_eq!(v[0], "default.txt");
1049            }
1050            _ => panic!("Expected MultiString, got {:?}", value),
1051        }
1052    }
1053
1054    #[test]
1055    fn test_arg_var_true_with_multiple_defaults() {
1056        // When arg has var=true and multiple defaults
1057        let cmd = SpecCommand::builder()
1058            .name("test")
1059            .arg(
1060                SpecArg::builder()
1061                    .name("files")
1062                    .var(true)
1063                    .default_values(["file1.txt", "file2.txt"])
1064                    .required(false)
1065                    .build(),
1066            )
1067            .build();
1068        let spec = Spec {
1069            name: "test".to_string(),
1070            bin: "test".to_string(),
1071            cmd,
1072            ..Default::default()
1073        };
1074
1075        // User doesn't provide the arg
1076        let input = vec!["test".to_string()];
1077        let parsed = parse(&spec, &input).unwrap();
1078
1079        assert_eq!(parsed.args.len(), 1);
1080        let value = parsed.args.values().next().unwrap();
1081        // Should be MultiString with both values
1082        match value {
1083            ParseValue::MultiString(v) => {
1084                assert_eq!(v.len(), 2);
1085                assert_eq!(v[0], "file1.txt");
1086                assert_eq!(v[1], "file2.txt");
1087            }
1088            _ => panic!("Expected MultiString, got {:?}", value),
1089        }
1090    }
1091
1092    #[test]
1093    fn test_arg_var_false_with_default_remains_string() {
1094        // When arg has var=false (default), the default should still be String
1095        let cmd = SpecCommand::builder()
1096            .name("test")
1097            .arg(
1098                SpecArg::builder()
1099                    .name("file")
1100                    .var(false)
1101                    .default_value("default.txt")
1102                    .required(false)
1103                    .build(),
1104            )
1105            .build();
1106        let spec = Spec {
1107            name: "test".to_string(),
1108            bin: "test".to_string(),
1109            cmd,
1110            ..Default::default()
1111        };
1112
1113        // User doesn't provide the arg
1114        let input = vec!["test".to_string()];
1115        let parsed = parse(&spec, &input).unwrap();
1116
1117        assert_eq!(parsed.args.len(), 1);
1118        let value = parsed.args.values().next().unwrap();
1119        // Should be String, not MultiString
1120        match value {
1121            ParseValue::String(s) => {
1122                assert_eq!(s, "default.txt");
1123            }
1124            _ => panic!("Expected String, got {:?}", value),
1125        }
1126    }
1127
1128    #[test]
1129    fn test_default_subcommand() {
1130        // Test that default_subcommand routes to the specified subcommand
1131        let run_cmd = SpecCommand::builder()
1132            .name("run")
1133            .arg(SpecArg::builder().name("task").build())
1134            .build();
1135        let mut cmd = SpecCommand::builder().name("test").build();
1136        cmd.subcommands.insert("run".to_string(), run_cmd);
1137
1138        let spec = Spec {
1139            name: "test".to_string(),
1140            bin: "test".to_string(),
1141            cmd,
1142            default_subcommand: Some("run".to_string()),
1143            ..Default::default()
1144        };
1145
1146        // "test mytask" should be parsed as if it were "test run mytask"
1147        let input = vec!["test".to_string(), "mytask".to_string()];
1148        let parsed = parse(&spec, &input).unwrap();
1149
1150        // Should have two commands: root and "run"
1151        assert_eq!(parsed.cmds.len(), 2);
1152        assert_eq!(parsed.cmds[1].name, "run");
1153
1154        // Should have parsed the task argument
1155        assert_eq!(parsed.args.len(), 1);
1156        let arg = parsed.args.keys().next().unwrap();
1157        assert_eq!(arg.name, "task");
1158        let value = parsed.args.values().next().unwrap();
1159        assert_eq!(value.to_string(), "mytask");
1160    }
1161
1162    #[test]
1163    fn test_default_subcommand_explicit_still_works() {
1164        // Test that explicit subcommand takes precedence
1165        let run_cmd = SpecCommand::builder()
1166            .name("run")
1167            .arg(SpecArg::builder().name("task").build())
1168            .build();
1169        let other_cmd = SpecCommand::builder()
1170            .name("other")
1171            .arg(SpecArg::builder().name("other_arg").build())
1172            .build();
1173        let mut cmd = SpecCommand::builder().name("test").build();
1174        cmd.subcommands.insert("run".to_string(), run_cmd);
1175        cmd.subcommands.insert("other".to_string(), other_cmd);
1176
1177        let spec = Spec {
1178            name: "test".to_string(),
1179            bin: "test".to_string(),
1180            cmd,
1181            default_subcommand: Some("run".to_string()),
1182            ..Default::default()
1183        };
1184
1185        // "test other foo" should use "other" subcommand, not default
1186        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1187        let parsed = parse(&spec, &input).unwrap();
1188
1189        // Should have used "other" subcommand
1190        assert_eq!(parsed.cmds.len(), 2);
1191        assert_eq!(parsed.cmds[1].name, "other");
1192    }
1193
1194    #[test]
1195    fn test_restart_token() {
1196        // Test that restart_token resets argument parsing
1197        let run_cmd = SpecCommand::builder()
1198            .name("run")
1199            .arg(SpecArg::builder().name("task").build())
1200            .restart_token(":::".to_string())
1201            .build();
1202        let mut cmd = SpecCommand::builder().name("test").build();
1203        cmd.subcommands.insert("run".to_string(), run_cmd);
1204
1205        let spec = Spec {
1206            name: "test".to_string(),
1207            bin: "test".to_string(),
1208            cmd,
1209            ..Default::default()
1210        };
1211
1212        // "test run task1 ::: task2" - should end up with task2 as the arg
1213        let input = vec![
1214            "test".to_string(),
1215            "run".to_string(),
1216            "task1".to_string(),
1217            ":::".to_string(),
1218            "task2".to_string(),
1219        ];
1220        let parsed = parse(&spec, &input).unwrap();
1221
1222        // After restart, args were cleared and task2 was parsed
1223        assert_eq!(parsed.args.len(), 1);
1224        let value = parsed.args.values().next().unwrap();
1225        assert_eq!(value.to_string(), "task2");
1226    }
1227
1228    #[test]
1229    fn test_restart_token_multiple() {
1230        // Test multiple restart tokens
1231        let run_cmd = SpecCommand::builder()
1232            .name("run")
1233            .arg(SpecArg::builder().name("task").build())
1234            .restart_token(":::".to_string())
1235            .build();
1236        let mut cmd = SpecCommand::builder().name("test").build();
1237        cmd.subcommands.insert("run".to_string(), run_cmd);
1238
1239        let spec = Spec {
1240            name: "test".to_string(),
1241            bin: "test".to_string(),
1242            cmd,
1243            ..Default::default()
1244        };
1245
1246        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
1247        let input = vec![
1248            "test".to_string(),
1249            "run".to_string(),
1250            "task1".to_string(),
1251            ":::".to_string(),
1252            "task2".to_string(),
1253            ":::".to_string(),
1254            "task3".to_string(),
1255        ];
1256        let parsed = parse(&spec, &input).unwrap();
1257
1258        // After multiple restarts, args were cleared and task3 was parsed
1259        assert_eq!(parsed.args.len(), 1);
1260        let value = parsed.args.values().next().unwrap();
1261        assert_eq!(value.to_string(), "task3");
1262    }
1263
1264    #[test]
1265    fn test_restart_token_clears_flag_awaiting_value() {
1266        // Test that restart_token clears pending flag values
1267        let run_cmd = SpecCommand::builder()
1268            .name("run")
1269            .arg(SpecArg::builder().name("task").build())
1270            .flag(
1271                SpecFlag::builder()
1272                    .name("jobs")
1273                    .long("jobs")
1274                    .arg(SpecArg::builder().name("count").build())
1275                    .build(),
1276            )
1277            .restart_token(":::".to_string())
1278            .build();
1279        let mut cmd = SpecCommand::builder().name("test").build();
1280        cmd.subcommands.insert("run".to_string(), run_cmd);
1281
1282        let spec = Spec {
1283            name: "test".to_string(),
1284            bin: "test".to_string(),
1285            cmd,
1286            ..Default::default()
1287        };
1288
1289        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
1290        let input = vec![
1291            "test".to_string(),
1292            "run".to_string(),
1293            "task1".to_string(),
1294            "--jobs".to_string(),
1295            ":::".to_string(),
1296            "task2".to_string(),
1297        ];
1298        let parsed = parse(&spec, &input).unwrap();
1299
1300        // task2 should be parsed as the task arg, not as --jobs value
1301        assert_eq!(parsed.args.len(), 1);
1302        let value = parsed.args.values().next().unwrap();
1303        assert_eq!(value.to_string(), "task2");
1304        // --jobs should not have a value
1305        assert!(parsed.flag_awaiting_value.is_empty());
1306    }
1307
1308    #[test]
1309    fn test_restart_token_resets_double_dash() {
1310        // Test that restart_token resets the -- separator effect
1311        let run_cmd = SpecCommand::builder()
1312            .name("run")
1313            .arg(SpecArg::builder().name("task").build())
1314            .arg(SpecArg::builder().name("extra_args").var(true).build())
1315            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1316            .restart_token(":::".to_string())
1317            .build();
1318        let mut cmd = SpecCommand::builder().name("test").build();
1319        cmd.subcommands.insert("run".to_string(), run_cmd);
1320
1321        let spec = Spec {
1322            name: "test".to_string(),
1323            bin: "test".to_string(),
1324            cmd,
1325            ..Default::default()
1326        };
1327
1328        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
1329        let input = vec![
1330            "test".to_string(),
1331            "run".to_string(),
1332            "task1".to_string(),
1333            "--".to_string(),
1334            "extra".to_string(),
1335            ":::".to_string(),
1336            "--verbose".to_string(),
1337            "task2".to_string(),
1338        ];
1339        let parsed = parse(&spec, &input).unwrap();
1340
1341        // --verbose should be parsed as a flag (not an arg) after the restart
1342        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1343        // task2 should be the arg after restart
1344        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1345        let value = parsed.args.get(task_arg).unwrap();
1346        assert_eq!(value.to_string(), "task2");
1347    }
1348
1349    #[test]
1350    fn test_double_dashes_without_preserve() {
1351        // Test that variadic args WITHOUT `preserve` skip "--" tokens (default behavior)
1352        let run_cmd = SpecCommand::builder()
1353            .name("run")
1354            .arg(SpecArg::builder().name("args").var(true).build())
1355            .build();
1356        let mut cmd = SpecCommand::builder().name("test").build();
1357        cmd.subcommands.insert("run".to_string(), run_cmd);
1358
1359        let spec = Spec {
1360            name: "test".to_string(),
1361            bin: "test".to_string(),
1362            cmd,
1363            ..Default::default()
1364        };
1365
1366        // "test run arg1 -- arg2 -- arg3" - all double dashes should be skipped
1367        let input = vec![
1368            "test".to_string(),
1369            "run".to_string(),
1370            "arg1".to_string(),
1371            "--".to_string(),
1372            "arg2".to_string(),
1373            "--".to_string(),
1374            "arg3".to_string(),
1375        ];
1376        let parsed = parse(&spec, &input).unwrap();
1377
1378        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1379        let value = parsed.args.get(args_arg).unwrap();
1380        assert_eq!(value.to_string(), "arg1 arg2 arg3");
1381    }
1382
1383    #[test]
1384    fn test_double_dashes_with_preserve() {
1385        // Test that variadic args WITH `preserve` keep all double dashes
1386        let run_cmd = SpecCommand::builder()
1387            .name("run")
1388            .arg(
1389                SpecArg::builder()
1390                    .name("args")
1391                    .var(true)
1392                    .double_dash(SpecDoubleDashChoices::Preserve)
1393                    .build(),
1394            )
1395            .build();
1396        let mut cmd = SpecCommand::builder().name("test").build();
1397        cmd.subcommands.insert("run".to_string(), run_cmd);
1398
1399        let spec = Spec {
1400            name: "test".to_string(),
1401            bin: "test".to_string(),
1402            cmd,
1403            ..Default::default()
1404        };
1405
1406        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
1407        let input = vec![
1408            "test".to_string(),
1409            "run".to_string(),
1410            "arg1".to_string(),
1411            "--".to_string(),
1412            "arg2".to_string(),
1413            "--".to_string(),
1414            "arg3".to_string(),
1415        ];
1416        let parsed = parse(&spec, &input).unwrap();
1417
1418        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1419        let value = parsed.args.get(args_arg).unwrap();
1420        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1421    }
1422
1423    #[test]
1424    fn test_double_dashes_with_preserve_only_dashes() {
1425        // Test that variadic args WITH `preserve` keep all double dashes even
1426        // if the values are just double dashes
1427        let run_cmd = SpecCommand::builder()
1428            .name("run")
1429            .arg(
1430                SpecArg::builder()
1431                    .name("args")
1432                    .var(true)
1433                    .double_dash(SpecDoubleDashChoices::Preserve)
1434                    .build(),
1435            )
1436            .build();
1437        let mut cmd = SpecCommand::builder().name("test").build();
1438        cmd.subcommands.insert("run".to_string(), run_cmd);
1439
1440        let spec = Spec {
1441            name: "test".to_string(),
1442            bin: "test".to_string(),
1443            cmd,
1444            ..Default::default()
1445        };
1446
1447        // "test run -- --" - all double dashes should be preserved
1448        let input = vec![
1449            "test".to_string(),
1450            "run".to_string(),
1451            "--".to_string(),
1452            "--".to_string(),
1453        ];
1454        let parsed = parse(&spec, &input).unwrap();
1455
1456        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1457        let value = parsed.args.get(args_arg).unwrap();
1458        assert_eq!(value.to_string(), "-- --");
1459    }
1460
1461    #[test]
1462    fn test_double_dashes_with_preserve_multiple_args() {
1463        // Test with multiple args where only the second has has `preserve`
1464        let run_cmd = SpecCommand::builder()
1465            .name("run")
1466            .arg(SpecArg::builder().name("task").build())
1467            .arg(
1468                SpecArg::builder()
1469                    .name("extra_args")
1470                    .var(true)
1471                    .double_dash(SpecDoubleDashChoices::Preserve)
1472                    .build(),
1473            )
1474            .build();
1475        let mut cmd = SpecCommand::builder().name("test").build();
1476        cmd.subcommands.insert("run".to_string(), run_cmd);
1477
1478        let spec = Spec {
1479            name: "test".to_string(),
1480            bin: "test".to_string(),
1481            cmd,
1482            ..Default::default()
1483        };
1484
1485        // The first arg "task1" is captured normally
1486        // Then extra_args with `preserve` captures everything, including the "--" tokens
1487        let input = vec![
1488            "test".to_string(),
1489            "run".to_string(),
1490            "task1".to_string(),
1491            "--".to_string(),
1492            "arg1".to_string(),
1493            "--".to_string(),
1494            "--foo".to_string(),
1495        ];
1496        let parsed = parse(&spec, &input).unwrap();
1497
1498        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1499        let task_value = parsed.args.get(task_arg).unwrap();
1500        assert_eq!(task_value.to_string(), "task1");
1501
1502        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1503        let extra_value = parsed.args.get(extra_arg).unwrap();
1504        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1505    }
1506
1507    #[test]
1508    fn test_parser_with_custom_env_for_required_arg() {
1509        // Test that Parser::with_env works for required args with env vars
1510        // This should NOT fail validation even though the env var is not in std::env
1511        let cmd = SpecCommand::builder()
1512            .name("test")
1513            .arg(
1514                SpecArg::builder()
1515                    .name("name")
1516                    .env("NAME")
1517                    .required(true)
1518                    .build(),
1519            )
1520            .build();
1521        let spec = Spec {
1522            name: "test".to_string(),
1523            bin: "test".to_string(),
1524            cmd,
1525            ..Default::default()
1526        };
1527
1528        // Ensure NAME is not in process env
1529        std::env::remove_var("NAME");
1530
1531        // Provide env through custom env map
1532        let mut env = HashMap::new();
1533        env.insert("NAME".to_string(), "john".to_string());
1534
1535        let input = vec!["test".to_string()];
1536        let result = Parser::new(&spec).with_env(env).parse(&input);
1537
1538        // Should succeed - custom env map should be used for validation
1539        let parsed = result.expect("parse should succeed with custom env");
1540        assert_eq!(parsed.args.len(), 1);
1541        let value = parsed.args.values().next().unwrap();
1542        assert_eq!(value.to_string(), "john");
1543    }
1544
1545    #[test]
1546    fn test_parser_with_custom_env_for_required_flag() {
1547        // Test that Parser::with_env works for required flags with env vars
1548        let cmd = SpecCommand::builder()
1549            .name("test")
1550            .flag(
1551                SpecFlag::builder()
1552                    .long("name")
1553                    .env("NAME")
1554                    .required(true)
1555                    .arg(SpecArg::builder().name("name").build())
1556                    .build(),
1557            )
1558            .build();
1559        let spec = Spec {
1560            name: "test".to_string(),
1561            bin: "test".to_string(),
1562            cmd,
1563            ..Default::default()
1564        };
1565
1566        // Ensure NAME is not in process env
1567        std::env::remove_var("NAME");
1568
1569        // Provide env through custom env map
1570        let mut env = HashMap::new();
1571        env.insert("NAME".to_string(), "jane".to_string());
1572
1573        let input = vec!["test".to_string()];
1574        let result = Parser::new(&spec).with_env(env).parse(&input);
1575
1576        // Should succeed - custom env map should be used for validation
1577        let parsed = result.expect("parse should succeed with custom env");
1578        assert_eq!(parsed.flags.len(), 1);
1579        let value = parsed.flags.values().next().unwrap();
1580        assert_eq!(value.to_string(), "jane");
1581    }
1582
1583    #[test]
1584    fn test_parser_with_custom_env_still_fails_when_missing() {
1585        // Test that validation still fails when env var is missing from both maps
1586        let cmd = SpecCommand::builder()
1587            .name("test")
1588            .arg(
1589                SpecArg::builder()
1590                    .name("name")
1591                    .env("NAME")
1592                    .required(true)
1593                    .build(),
1594            )
1595            .build();
1596        let spec = Spec {
1597            name: "test".to_string(),
1598            bin: "test".to_string(),
1599            cmd,
1600            ..Default::default()
1601        };
1602
1603        // Ensure NAME is not in process env
1604        std::env::remove_var("NAME");
1605
1606        // Provide a custom env map WITHOUT the required env var
1607        let env = HashMap::new();
1608
1609        let input = vec!["test".to_string()];
1610        let result = Parser::new(&spec).with_env(env).parse(&input);
1611
1612        // Should fail - env var is missing from both custom and process env
1613        assert!(result.is_err());
1614    }
1615}