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                    let arr = out
477                        .flags
478                        .entry(flag)
479                        .or_insert_with(|| ParseValue::MultiString(vec![]))
480                        .try_as_multi_string_mut()
481                        .unwrap();
482                    arr.push(w);
483                } else {
484                    if let Some(choices) = &arg.choices {
485                        if !choices.choices.contains(&w) {
486                            if is_help_arg(spec, &w) {
487                                out.errors
488                                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
489                                return Ok(out);
490                            }
491                            bail!(
492                                "Invalid choice for option {}: {w}, expected one of {}",
493                                flag.name,
494                                choices.choices.join(", ")
495                            );
496                        }
497                    }
498                    out.flags.insert(flag, ParseValue::String(w));
499                }
500                w = "".to_string();
501            }
502            continue;
503        }
504
505        if let Some(arg) = next_arg {
506            if arg.var {
507                let arr = out
508                    .args
509                    .entry(Arc::new(arg.clone()))
510                    .or_insert_with(|| ParseValue::MultiString(vec![]))
511                    .try_as_multi_string_mut()
512                    .unwrap();
513                arr.push(w);
514                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
515                    next_arg = out.cmd.args.get(out.args.len());
516                }
517            } else {
518                if let Some(choices) = &arg.choices {
519                    if !choices.choices.contains(&w) {
520                        if is_help_arg(spec, &w) {
521                            out.errors
522                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
523                            return Ok(out);
524                        }
525                        bail!(
526                            "Invalid choice for arg {}: {w}, expected one of {}",
527                            arg.name,
528                            choices.choices.join(", ")
529                        );
530                    }
531                }
532                out.args
533                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
534                next_arg = out.cmd.args.get(out.args.len());
535            }
536            continue;
537        }
538        if is_help_arg(spec, &w) {
539            out.errors
540                .push(render_help_err(spec, &out.cmd, w.len() > 2));
541            return Ok(out);
542        }
543        bail!("unexpected word: {w}");
544    }
545
546    for arg in out.cmd.args.iter().skip(out.args.len()) {
547        if arg.required && arg.default.is_empty() {
548            // Check if there's an env var available (custom env map takes precedence)
549            let has_env = arg.env.as_ref().is_some_and(|e| {
550                custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
551                    || std::env::var(e).is_ok()
552            });
553            if !has_env {
554                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
555            }
556        }
557    }
558
559    for flag in out.available_flags.values() {
560        if out.flags.contains_key(flag) {
561            continue;
562        }
563        let has_default =
564            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
565        // Check if there's an env var available (custom env map takes precedence)
566        let has_env = flag.env.as_ref().is_some_and(|e| {
567            custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
568        });
569        if flag.required && !has_default && !has_env {
570            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
571        }
572    }
573
574    // Validate var_min/var_max constraints for variadic args
575    for (arg, value) in &out.args {
576        if arg.var {
577            if let ParseValue::MultiString(values) = value {
578                if let Some(min) = arg.var_min {
579                    if values.len() < min {
580                        out.errors.push(UsageErr::VarArgTooFew {
581                            name: arg.name.clone(),
582                            min,
583                            got: values.len(),
584                        });
585                    }
586                }
587                if let Some(max) = arg.var_max {
588                    if values.len() > max {
589                        out.errors.push(UsageErr::VarArgTooMany {
590                            name: arg.name.clone(),
591                            max,
592                            got: values.len(),
593                        });
594                    }
595                }
596            }
597        }
598    }
599
600    // Validate var_min/var_max constraints for variadic flags
601    for (flag, value) in &out.flags {
602        if flag.var {
603            let count = match value {
604                ParseValue::MultiString(values) => values.len(),
605                ParseValue::MultiBool(values) => values.len(),
606                _ => continue,
607            };
608            if let Some(min) = flag.var_min {
609                if count < min {
610                    out.errors.push(UsageErr::VarFlagTooFew {
611                        name: flag.name.clone(),
612                        min,
613                        got: count,
614                    });
615                }
616            }
617            if let Some(max) = flag.var_max {
618                if count > max {
619                    out.errors.push(UsageErr::VarFlagTooMany {
620                        name: flag.name.clone(),
621                        max,
622                        got: count,
623                    });
624                }
625            }
626        }
627    }
628
629    Ok(out)
630}
631
632#[cfg(feature = "docs")]
633fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
634    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
635}
636
637#[cfg(not(feature = "docs"))]
638fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
639    UsageErr::Help("help".to_string())
640}
641
642fn is_help_arg(spec: &Spec, w: &str) -> bool {
643    spec.disable_help != Some(true)
644        && (w == "--help"
645            || w == "-h"
646            || w == "-?"
647            || (spec.cmd.subcommands.is_empty() && w == "help"))
648}
649
650impl ParseOutput {
651    pub fn as_env(&self) -> BTreeMap<String, String> {
652        let mut env = BTreeMap::new();
653        for (flag, val) in &self.flags {
654            let key = format!("usage_{}", flag.name.to_snake_case());
655            let val = match val {
656                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
657                ParseValue::String(s) => s.clone(),
658                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
659                ParseValue::MultiString(s) => shell_words::join(s),
660            };
661            env.insert(key, val);
662        }
663        for (arg, val) in &self.args {
664            let key = format!("usage_{}", arg.name.to_snake_case());
665            env.insert(key, val.to_string());
666        }
667        env
668    }
669}
670
671impl Display for ParseValue {
672    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
673        match self {
674            ParseValue::Bool(b) => write!(f, "{b}"),
675            ParseValue::String(s) => write!(f, "{s}"),
676            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
677            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
678        }
679    }
680}
681
682impl Debug for ParseOutput {
683    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
684        f.debug_struct("ParseOutput")
685            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
686            .field(
687                "args",
688                &self
689                    .args
690                    .iter()
691                    .map(|(a, w)| format!("{}: {w}", &a.name))
692                    .collect_vec(),
693            )
694            .field(
695                "available_flags",
696                &self
697                    .available_flags
698                    .iter()
699                    .map(|(f, w)| format!("{f}: {w}"))
700                    .collect_vec(),
701            )
702            .field(
703                "flags",
704                &self
705                    .flags
706                    .iter()
707                    .map(|(f, w)| format!("{}: {w}", &f.name))
708                    .collect_vec(),
709            )
710            .field("flag_awaiting_value", &self.flag_awaiting_value)
711            .field("errors", &self.errors)
712            .finish()
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn test_parse() {
722        let cmd = SpecCommand::builder()
723            .name("test")
724            .arg(SpecArg::builder().name("arg").build())
725            .flag(SpecFlag::builder().long("flag").build())
726            .build();
727        let spec = Spec {
728            name: "test".to_string(),
729            bin: "test".to_string(),
730            cmd,
731            ..Default::default()
732        };
733        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
734        let parsed = parse(&spec, &input).unwrap();
735        assert_eq!(parsed.cmds.len(), 1);
736        assert_eq!(parsed.cmds[0].name, "test");
737        assert_eq!(parsed.args.len(), 1);
738        assert_eq!(parsed.flags.len(), 1);
739        assert_eq!(parsed.available_flags.len(), 1);
740    }
741
742    #[test]
743    fn test_as_env() {
744        let cmd = SpecCommand::builder()
745            .name("test")
746            .arg(SpecArg::builder().name("arg").build())
747            .flag(SpecFlag::builder().long("flag").build())
748            .flag(
749                SpecFlag::builder()
750                    .long("force")
751                    .negate("--no-force")
752                    .build(),
753            )
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![
762            "test".to_string(),
763            "--flag".to_string(),
764            "--no-force".to_string(),
765        ];
766        let parsed = parse(&spec, &input).unwrap();
767        let env = parsed.as_env();
768        assert_eq!(env.len(), 2);
769        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
770        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
771    }
772
773    #[test]
774    fn test_arg_env_var() {
775        let cmd = SpecCommand::builder()
776            .name("test")
777            .arg(
778                SpecArg::builder()
779                    .name("input")
780                    .env("TEST_ARG_INPUT")
781                    .required(true)
782                    .build(),
783            )
784            .build();
785        let spec = Spec {
786            name: "test".to_string(),
787            bin: "test".to_string(),
788            cmd,
789            ..Default::default()
790        };
791
792        // Set env var
793        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
794
795        let input = vec!["test".to_string()];
796        let parsed = parse(&spec, &input).unwrap();
797
798        assert_eq!(parsed.args.len(), 1);
799        let arg = parsed.args.keys().next().unwrap();
800        assert_eq!(arg.name, "input");
801        let value = parsed.args.values().next().unwrap();
802        assert_eq!(value.to_string(), "test_file.txt");
803
804        // Clean up
805        std::env::remove_var("TEST_ARG_INPUT");
806    }
807
808    #[test]
809    fn test_flag_env_var_with_arg() {
810        let cmd = SpecCommand::builder()
811            .name("test")
812            .flag(
813                SpecFlag::builder()
814                    .long("output")
815                    .env("TEST_FLAG_OUTPUT")
816                    .arg(SpecArg::builder().name("file").build())
817                    .build(),
818            )
819            .build();
820        let spec = Spec {
821            name: "test".to_string(),
822            bin: "test".to_string(),
823            cmd,
824            ..Default::default()
825        };
826
827        // Set env var
828        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
829
830        let input = vec!["test".to_string()];
831        let parsed = parse(&spec, &input).unwrap();
832
833        assert_eq!(parsed.flags.len(), 1);
834        let flag = parsed.flags.keys().next().unwrap();
835        assert_eq!(flag.name, "output");
836        let value = parsed.flags.values().next().unwrap();
837        assert_eq!(value.to_string(), "output.txt");
838
839        // Clean up
840        std::env::remove_var("TEST_FLAG_OUTPUT");
841    }
842
843    #[test]
844    fn test_flag_env_var_boolean() {
845        let cmd = SpecCommand::builder()
846            .name("test")
847            .flag(
848                SpecFlag::builder()
849                    .long("verbose")
850                    .env("TEST_FLAG_VERBOSE")
851                    .build(),
852            )
853            .build();
854        let spec = Spec {
855            name: "test".to_string(),
856            bin: "test".to_string(),
857            cmd,
858            ..Default::default()
859        };
860
861        // Set env var to true
862        std::env::set_var("TEST_FLAG_VERBOSE", "true");
863
864        let input = vec!["test".to_string()];
865        let parsed = parse(&spec, &input).unwrap();
866
867        assert_eq!(parsed.flags.len(), 1);
868        let flag = parsed.flags.keys().next().unwrap();
869        assert_eq!(flag.name, "verbose");
870        let value = parsed.flags.values().next().unwrap();
871        assert_eq!(value.to_string(), "true");
872
873        // Clean up
874        std::env::remove_var("TEST_FLAG_VERBOSE");
875    }
876
877    #[test]
878    fn test_env_var_precedence() {
879        // CLI args should take precedence over env vars
880        let cmd = SpecCommand::builder()
881            .name("test")
882            .arg(
883                SpecArg::builder()
884                    .name("input")
885                    .env("TEST_PRECEDENCE_INPUT")
886                    .required(true)
887                    .build(),
888            )
889            .build();
890        let spec = Spec {
891            name: "test".to_string(),
892            bin: "test".to_string(),
893            cmd,
894            ..Default::default()
895        };
896
897        // Set env var
898        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
899
900        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
901        let parsed = parse(&spec, &input).unwrap();
902
903        assert_eq!(parsed.args.len(), 1);
904        let value = parsed.args.values().next().unwrap();
905        // CLI arg should take precedence
906        assert_eq!(value.to_string(), "cli_file.txt");
907
908        // Clean up
909        std::env::remove_var("TEST_PRECEDENCE_INPUT");
910    }
911
912    #[test]
913    fn test_flag_var_true_with_single_default() {
914        // When var=true and default="bar", the default should be MultiString(["bar"])
915        let cmd = SpecCommand::builder()
916            .name("test")
917            .flag(
918                SpecFlag::builder()
919                    .long("foo")
920                    .var(true)
921                    .arg(SpecArg::builder().name("foo").build())
922                    .default_value("bar")
923                    .build(),
924            )
925            .build();
926        let spec = Spec {
927            name: "test".to_string(),
928            bin: "test".to_string(),
929            cmd,
930            ..Default::default()
931        };
932
933        // User doesn't provide the flag
934        let input = vec!["test".to_string()];
935        let parsed = parse(&spec, &input).unwrap();
936
937        assert_eq!(parsed.flags.len(), 1);
938        let flag = parsed.flags.keys().next().unwrap();
939        assert_eq!(flag.name, "foo");
940        let value = parsed.flags.values().next().unwrap();
941        // Should be MultiString, not String
942        match value {
943            ParseValue::MultiString(v) => {
944                assert_eq!(v.len(), 1);
945                assert_eq!(v[0], "bar");
946            }
947            _ => panic!("Expected MultiString, got {:?}", value),
948        }
949    }
950
951    #[test]
952    fn test_flag_var_true_with_multiple_defaults() {
953        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
954        let cmd = SpecCommand::builder()
955            .name("test")
956            .flag(
957                SpecFlag::builder()
958                    .long("foo")
959                    .var(true)
960                    .arg(SpecArg::builder().name("foo").build())
961                    .default_values(["xyz", "bar"])
962                    .build(),
963            )
964            .build();
965        let spec = Spec {
966            name: "test".to_string(),
967            bin: "test".to_string(),
968            cmd,
969            ..Default::default()
970        };
971
972        // User doesn't provide the flag
973        let input = vec!["test".to_string()];
974        let parsed = parse(&spec, &input).unwrap();
975
976        assert_eq!(parsed.flags.len(), 1);
977        let value = parsed.flags.values().next().unwrap();
978        // Should be MultiString with both values
979        match value {
980            ParseValue::MultiString(v) => {
981                assert_eq!(v.len(), 2);
982                assert_eq!(v[0], "xyz");
983                assert_eq!(v[1], "bar");
984            }
985            _ => panic!("Expected MultiString, got {:?}", value),
986        }
987    }
988
989    #[test]
990    fn test_flag_var_false_with_default_remains_string() {
991        // When var=false (default), the default should still be String("bar")
992        let cmd = SpecCommand::builder()
993            .name("test")
994            .flag(
995                SpecFlag::builder()
996                    .long("foo")
997                    .var(false) // Default behavior
998                    .arg(SpecArg::builder().name("foo").build())
999                    .default_value("bar")
1000                    .build(),
1001            )
1002            .build();
1003        let spec = Spec {
1004            name: "test".to_string(),
1005            bin: "test".to_string(),
1006            cmd,
1007            ..Default::default()
1008        };
1009
1010        // User doesn't provide the flag
1011        let input = vec!["test".to_string()];
1012        let parsed = parse(&spec, &input).unwrap();
1013
1014        assert_eq!(parsed.flags.len(), 1);
1015        let value = parsed.flags.values().next().unwrap();
1016        // Should be String, not MultiString
1017        match value {
1018            ParseValue::String(s) => {
1019                assert_eq!(s, "bar");
1020            }
1021            _ => panic!("Expected String, got {:?}", value),
1022        }
1023    }
1024
1025    #[test]
1026    fn test_arg_var_true_with_single_default() {
1027        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
1028        let cmd = SpecCommand::builder()
1029            .name("test")
1030            .arg(
1031                SpecArg::builder()
1032                    .name("files")
1033                    .var(true)
1034                    .default_value("default.txt")
1035                    .required(false)
1036                    .build(),
1037            )
1038            .build();
1039        let spec = Spec {
1040            name: "test".to_string(),
1041            bin: "test".to_string(),
1042            cmd,
1043            ..Default::default()
1044        };
1045
1046        // User doesn't provide the arg
1047        let input = vec!["test".to_string()];
1048        let parsed = parse(&spec, &input).unwrap();
1049
1050        assert_eq!(parsed.args.len(), 1);
1051        let value = parsed.args.values().next().unwrap();
1052        // Should be MultiString, not String
1053        match value {
1054            ParseValue::MultiString(v) => {
1055                assert_eq!(v.len(), 1);
1056                assert_eq!(v[0], "default.txt");
1057            }
1058            _ => panic!("Expected MultiString, got {:?}", value),
1059        }
1060    }
1061
1062    #[test]
1063    fn test_arg_var_true_with_multiple_defaults() {
1064        // When arg has var=true and multiple defaults
1065        let cmd = SpecCommand::builder()
1066            .name("test")
1067            .arg(
1068                SpecArg::builder()
1069                    .name("files")
1070                    .var(true)
1071                    .default_values(["file1.txt", "file2.txt"])
1072                    .required(false)
1073                    .build(),
1074            )
1075            .build();
1076        let spec = Spec {
1077            name: "test".to_string(),
1078            bin: "test".to_string(),
1079            cmd,
1080            ..Default::default()
1081        };
1082
1083        // User doesn't provide the arg
1084        let input = vec!["test".to_string()];
1085        let parsed = parse(&spec, &input).unwrap();
1086
1087        assert_eq!(parsed.args.len(), 1);
1088        let value = parsed.args.values().next().unwrap();
1089        // Should be MultiString with both values
1090        match value {
1091            ParseValue::MultiString(v) => {
1092                assert_eq!(v.len(), 2);
1093                assert_eq!(v[0], "file1.txt");
1094                assert_eq!(v[1], "file2.txt");
1095            }
1096            _ => panic!("Expected MultiString, got {:?}", value),
1097        }
1098    }
1099
1100    #[test]
1101    fn test_arg_var_false_with_default_remains_string() {
1102        // When arg has var=false (default), the default should still be String
1103        let cmd = SpecCommand::builder()
1104            .name("test")
1105            .arg(
1106                SpecArg::builder()
1107                    .name("file")
1108                    .var(false)
1109                    .default_value("default.txt")
1110                    .required(false)
1111                    .build(),
1112            )
1113            .build();
1114        let spec = Spec {
1115            name: "test".to_string(),
1116            bin: "test".to_string(),
1117            cmd,
1118            ..Default::default()
1119        };
1120
1121        // User doesn't provide the arg
1122        let input = vec!["test".to_string()];
1123        let parsed = parse(&spec, &input).unwrap();
1124
1125        assert_eq!(parsed.args.len(), 1);
1126        let value = parsed.args.values().next().unwrap();
1127        // Should be String, not MultiString
1128        match value {
1129            ParseValue::String(s) => {
1130                assert_eq!(s, "default.txt");
1131            }
1132            _ => panic!("Expected String, got {:?}", value),
1133        }
1134    }
1135
1136    #[test]
1137    fn test_default_subcommand() {
1138        // Test that default_subcommand routes to the specified subcommand
1139        let run_cmd = SpecCommand::builder()
1140            .name("run")
1141            .arg(SpecArg::builder().name("task").build())
1142            .build();
1143        let mut cmd = SpecCommand::builder().name("test").build();
1144        cmd.subcommands.insert("run".to_string(), run_cmd);
1145
1146        let spec = Spec {
1147            name: "test".to_string(),
1148            bin: "test".to_string(),
1149            cmd,
1150            default_subcommand: Some("run".to_string()),
1151            ..Default::default()
1152        };
1153
1154        // "test mytask" should be parsed as if it were "test run mytask"
1155        let input = vec!["test".to_string(), "mytask".to_string()];
1156        let parsed = parse(&spec, &input).unwrap();
1157
1158        // Should have two commands: root and "run"
1159        assert_eq!(parsed.cmds.len(), 2);
1160        assert_eq!(parsed.cmds[1].name, "run");
1161
1162        // Should have parsed the task argument
1163        assert_eq!(parsed.args.len(), 1);
1164        let arg = parsed.args.keys().next().unwrap();
1165        assert_eq!(arg.name, "task");
1166        let value = parsed.args.values().next().unwrap();
1167        assert_eq!(value.to_string(), "mytask");
1168    }
1169
1170    #[test]
1171    fn test_default_subcommand_explicit_still_works() {
1172        // Test that explicit subcommand takes precedence
1173        let run_cmd = SpecCommand::builder()
1174            .name("run")
1175            .arg(SpecArg::builder().name("task").build())
1176            .build();
1177        let other_cmd = SpecCommand::builder()
1178            .name("other")
1179            .arg(SpecArg::builder().name("other_arg").build())
1180            .build();
1181        let mut cmd = SpecCommand::builder().name("test").build();
1182        cmd.subcommands.insert("run".to_string(), run_cmd);
1183        cmd.subcommands.insert("other".to_string(), other_cmd);
1184
1185        let spec = Spec {
1186            name: "test".to_string(),
1187            bin: "test".to_string(),
1188            cmd,
1189            default_subcommand: Some("run".to_string()),
1190            ..Default::default()
1191        };
1192
1193        // "test other foo" should use "other" subcommand, not default
1194        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1195        let parsed = parse(&spec, &input).unwrap();
1196
1197        // Should have used "other" subcommand
1198        assert_eq!(parsed.cmds.len(), 2);
1199        assert_eq!(parsed.cmds[1].name, "other");
1200    }
1201
1202    #[test]
1203    fn test_default_subcommand_with_nested_subcommands() {
1204        // Test that default_subcommand works when the default subcommand has nested subcommands.
1205        // This is the mise use case: "mise say" should be parsed as "mise run say"
1206        // where "say" is a subcommand of "run" (a task).
1207        let say_cmd = SpecCommand::builder()
1208            .name("say")
1209            .arg(SpecArg::builder().name("name").build())
1210            .build();
1211        let mut run_cmd = SpecCommand::builder().name("run").build();
1212        run_cmd.subcommands.insert("say".to_string(), say_cmd);
1213
1214        let mut cmd = SpecCommand::builder().name("test").build();
1215        cmd.subcommands.insert("run".to_string(), run_cmd);
1216
1217        let spec = Spec {
1218            name: "test".to_string(),
1219            bin: "test".to_string(),
1220            cmd,
1221            default_subcommand: Some("run".to_string()),
1222            ..Default::default()
1223        };
1224
1225        // "test say hello" should be parsed as "test run say hello"
1226        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1227        let parsed = parse(&spec, &input).unwrap();
1228
1229        // Should have three commands: root, "run", and "say"
1230        assert_eq!(parsed.cmds.len(), 3);
1231        assert_eq!(parsed.cmds[0].name, "test");
1232        assert_eq!(parsed.cmds[1].name, "run");
1233        assert_eq!(parsed.cmds[2].name, "say");
1234
1235        // Should have parsed the "name" argument
1236        assert_eq!(parsed.args.len(), 1);
1237        let arg = parsed.args.keys().next().unwrap();
1238        assert_eq!(arg.name, "name");
1239        let value = parsed.args.values().next().unwrap();
1240        assert_eq!(value.to_string(), "hello");
1241    }
1242
1243    #[test]
1244    fn test_default_subcommand_same_name_child() {
1245        // Test that default_subcommand doesn't cause issues when the default subcommand
1246        // has a child with the same name (e.g., "run" has a task named "run").
1247        // This verifies we don't switch multiple times or get stuck in a loop.
1248        let run_task = SpecCommand::builder()
1249            .name("run")
1250            .arg(SpecArg::builder().name("args").build())
1251            .build();
1252        let mut run_cmd = SpecCommand::builder().name("run").build();
1253        run_cmd.subcommands.insert("run".to_string(), run_task);
1254
1255        let mut cmd = SpecCommand::builder().name("test").build();
1256        cmd.subcommands.insert("run".to_string(), run_cmd);
1257
1258        let spec = Spec {
1259            name: "test".to_string(),
1260            bin: "test".to_string(),
1261            cmd,
1262            default_subcommand: Some("run".to_string()),
1263            ..Default::default()
1264        };
1265
1266        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
1267        let input = vec!["test".to_string(), "run".to_string()];
1268        let parsed = parse(&spec, &input).unwrap();
1269
1270        // Should have two commands: root and "run"
1271        assert_eq!(parsed.cmds.len(), 2);
1272        assert_eq!(parsed.cmds[0].name, "test");
1273        assert_eq!(parsed.cmds[1].name, "run");
1274
1275        // "test run run" should descend into the "run" task (child of "run" subcommand)
1276        let input = vec![
1277            "test".to_string(),
1278            "run".to_string(),
1279            "run".to_string(),
1280            "hello".to_string(),
1281        ];
1282        let parsed = parse(&spec, &input).unwrap();
1283
1284        assert_eq!(parsed.cmds.len(), 3);
1285        assert_eq!(parsed.cmds[0].name, "test");
1286        assert_eq!(parsed.cmds[1].name, "run");
1287        assert_eq!(parsed.cmds[2].name, "run");
1288        assert_eq!(parsed.args.len(), 1);
1289        let value = parsed.args.values().next().unwrap();
1290        assert_eq!(value.to_string(), "hello");
1291
1292        // Key test case: "test other" should switch to default subcommand "run"
1293        // and treat "other" as a positional arg (not try to switch again because
1294        // "run" also has a "run" child).
1295        let mut run_cmd = SpecCommand::builder()
1296            .name("run")
1297            .arg(SpecArg::builder().name("task").build())
1298            .build();
1299        let run_task = SpecCommand::builder().name("run").build();
1300        run_cmd.subcommands.insert("run".to_string(), run_task);
1301
1302        let mut cmd = SpecCommand::builder().name("test").build();
1303        cmd.subcommands.insert("run".to_string(), run_cmd);
1304
1305        let spec = Spec {
1306            name: "test".to_string(),
1307            bin: "test".to_string(),
1308            cmd,
1309            default_subcommand: Some("run".to_string()),
1310            ..Default::default()
1311        };
1312
1313        let input = vec!["test".to_string(), "other".to_string()];
1314        let parsed = parse(&spec, &input).unwrap();
1315
1316        // Should have two commands: root and "run" (the default)
1317        // We should NOT have switched again to the "run" task child
1318        assert_eq!(parsed.cmds.len(), 2);
1319        assert_eq!(parsed.cmds[0].name, "test");
1320        assert_eq!(parsed.cmds[1].name, "run");
1321
1322        // "other" should be parsed as a positional arg
1323        assert_eq!(parsed.args.len(), 1);
1324        let value = parsed.args.values().next().unwrap();
1325        assert_eq!(value.to_string(), "other");
1326    }
1327
1328    #[test]
1329    fn test_restart_token() {
1330        // Test that restart_token resets argument parsing
1331        let run_cmd = SpecCommand::builder()
1332            .name("run")
1333            .arg(SpecArg::builder().name("task").build())
1334            .restart_token(":::".to_string())
1335            .build();
1336        let mut cmd = SpecCommand::builder().name("test").build();
1337        cmd.subcommands.insert("run".to_string(), run_cmd);
1338
1339        let spec = Spec {
1340            name: "test".to_string(),
1341            bin: "test".to_string(),
1342            cmd,
1343            ..Default::default()
1344        };
1345
1346        // "test run task1 ::: task2" - should end up with task2 as the arg
1347        let input = vec![
1348            "test".to_string(),
1349            "run".to_string(),
1350            "task1".to_string(),
1351            ":::".to_string(),
1352            "task2".to_string(),
1353        ];
1354        let parsed = parse(&spec, &input).unwrap();
1355
1356        // After restart, args were cleared and task2 was parsed
1357        assert_eq!(parsed.args.len(), 1);
1358        let value = parsed.args.values().next().unwrap();
1359        assert_eq!(value.to_string(), "task2");
1360    }
1361
1362    #[test]
1363    fn test_restart_token_multiple() {
1364        // Test multiple restart tokens
1365        let run_cmd = SpecCommand::builder()
1366            .name("run")
1367            .arg(SpecArg::builder().name("task").build())
1368            .restart_token(":::".to_string())
1369            .build();
1370        let mut cmd = SpecCommand::builder().name("test").build();
1371        cmd.subcommands.insert("run".to_string(), run_cmd);
1372
1373        let spec = Spec {
1374            name: "test".to_string(),
1375            bin: "test".to_string(),
1376            cmd,
1377            ..Default::default()
1378        };
1379
1380        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
1381        let input = vec![
1382            "test".to_string(),
1383            "run".to_string(),
1384            "task1".to_string(),
1385            ":::".to_string(),
1386            "task2".to_string(),
1387            ":::".to_string(),
1388            "task3".to_string(),
1389        ];
1390        let parsed = parse(&spec, &input).unwrap();
1391
1392        // After multiple restarts, args were cleared and task3 was parsed
1393        assert_eq!(parsed.args.len(), 1);
1394        let value = parsed.args.values().next().unwrap();
1395        assert_eq!(value.to_string(), "task3");
1396    }
1397
1398    #[test]
1399    fn test_restart_token_clears_flag_awaiting_value() {
1400        // Test that restart_token clears pending flag values
1401        let run_cmd = SpecCommand::builder()
1402            .name("run")
1403            .arg(SpecArg::builder().name("task").build())
1404            .flag(
1405                SpecFlag::builder()
1406                    .name("jobs")
1407                    .long("jobs")
1408                    .arg(SpecArg::builder().name("count").build())
1409                    .build(),
1410            )
1411            .restart_token(":::".to_string())
1412            .build();
1413        let mut cmd = SpecCommand::builder().name("test").build();
1414        cmd.subcommands.insert("run".to_string(), run_cmd);
1415
1416        let spec = Spec {
1417            name: "test".to_string(),
1418            bin: "test".to_string(),
1419            cmd,
1420            ..Default::default()
1421        };
1422
1423        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
1424        let input = vec![
1425            "test".to_string(),
1426            "run".to_string(),
1427            "task1".to_string(),
1428            "--jobs".to_string(),
1429            ":::".to_string(),
1430            "task2".to_string(),
1431        ];
1432        let parsed = parse(&spec, &input).unwrap();
1433
1434        // task2 should be parsed as the task arg, not as --jobs value
1435        assert_eq!(parsed.args.len(), 1);
1436        let value = parsed.args.values().next().unwrap();
1437        assert_eq!(value.to_string(), "task2");
1438        // --jobs should not have a value
1439        assert!(parsed.flag_awaiting_value.is_empty());
1440    }
1441
1442    #[test]
1443    fn test_restart_token_resets_double_dash() {
1444        // Test that restart_token resets the -- separator effect
1445        let run_cmd = SpecCommand::builder()
1446            .name("run")
1447            .arg(SpecArg::builder().name("task").build())
1448            .arg(SpecArg::builder().name("extra_args").var(true).build())
1449            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1450            .restart_token(":::".to_string())
1451            .build();
1452        let mut cmd = SpecCommand::builder().name("test").build();
1453        cmd.subcommands.insert("run".to_string(), run_cmd);
1454
1455        let spec = Spec {
1456            name: "test".to_string(),
1457            bin: "test".to_string(),
1458            cmd,
1459            ..Default::default()
1460        };
1461
1462        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
1463        let input = vec![
1464            "test".to_string(),
1465            "run".to_string(),
1466            "task1".to_string(),
1467            "--".to_string(),
1468            "extra".to_string(),
1469            ":::".to_string(),
1470            "--verbose".to_string(),
1471            "task2".to_string(),
1472        ];
1473        let parsed = parse(&spec, &input).unwrap();
1474
1475        // --verbose should be parsed as a flag (not an arg) after the restart
1476        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1477        // task2 should be the arg after restart
1478        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1479        let value = parsed.args.get(task_arg).unwrap();
1480        assert_eq!(value.to_string(), "task2");
1481    }
1482
1483    #[test]
1484    fn test_double_dashes_without_preserve() {
1485        // Test that variadic args WITHOUT `preserve` skip "--" tokens (default behavior)
1486        let run_cmd = SpecCommand::builder()
1487            .name("run")
1488            .arg(SpecArg::builder().name("args").var(true).build())
1489            .build();
1490        let mut cmd = SpecCommand::builder().name("test").build();
1491        cmd.subcommands.insert("run".to_string(), run_cmd);
1492
1493        let spec = Spec {
1494            name: "test".to_string(),
1495            bin: "test".to_string(),
1496            cmd,
1497            ..Default::default()
1498        };
1499
1500        // "test run arg1 -- arg2 -- arg3" - all double dashes should be skipped
1501        let input = vec![
1502            "test".to_string(),
1503            "run".to_string(),
1504            "arg1".to_string(),
1505            "--".to_string(),
1506            "arg2".to_string(),
1507            "--".to_string(),
1508            "arg3".to_string(),
1509        ];
1510        let parsed = parse(&spec, &input).unwrap();
1511
1512        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1513        let value = parsed.args.get(args_arg).unwrap();
1514        assert_eq!(value.to_string(), "arg1 arg2 arg3");
1515    }
1516
1517    #[test]
1518    fn test_double_dashes_with_preserve() {
1519        // Test that variadic args WITH `preserve` keep all double dashes
1520        let run_cmd = SpecCommand::builder()
1521            .name("run")
1522            .arg(
1523                SpecArg::builder()
1524                    .name("args")
1525                    .var(true)
1526                    .double_dash(SpecDoubleDashChoices::Preserve)
1527                    .build(),
1528            )
1529            .build();
1530        let mut cmd = SpecCommand::builder().name("test").build();
1531        cmd.subcommands.insert("run".to_string(), run_cmd);
1532
1533        let spec = Spec {
1534            name: "test".to_string(),
1535            bin: "test".to_string(),
1536            cmd,
1537            ..Default::default()
1538        };
1539
1540        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
1541        let input = vec![
1542            "test".to_string(),
1543            "run".to_string(),
1544            "arg1".to_string(),
1545            "--".to_string(),
1546            "arg2".to_string(),
1547            "--".to_string(),
1548            "arg3".to_string(),
1549        ];
1550        let parsed = parse(&spec, &input).unwrap();
1551
1552        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1553        let value = parsed.args.get(args_arg).unwrap();
1554        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1555    }
1556
1557    #[test]
1558    fn test_double_dashes_with_preserve_only_dashes() {
1559        // Test that variadic args WITH `preserve` keep all double dashes even
1560        // if the values are just double dashes
1561        let run_cmd = SpecCommand::builder()
1562            .name("run")
1563            .arg(
1564                SpecArg::builder()
1565                    .name("args")
1566                    .var(true)
1567                    .double_dash(SpecDoubleDashChoices::Preserve)
1568                    .build(),
1569            )
1570            .build();
1571        let mut cmd = SpecCommand::builder().name("test").build();
1572        cmd.subcommands.insert("run".to_string(), run_cmd);
1573
1574        let spec = Spec {
1575            name: "test".to_string(),
1576            bin: "test".to_string(),
1577            cmd,
1578            ..Default::default()
1579        };
1580
1581        // "test run -- --" - all double dashes should be preserved
1582        let input = vec![
1583            "test".to_string(),
1584            "run".to_string(),
1585            "--".to_string(),
1586            "--".to_string(),
1587        ];
1588        let parsed = parse(&spec, &input).unwrap();
1589
1590        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1591        let value = parsed.args.get(args_arg).unwrap();
1592        assert_eq!(value.to_string(), "-- --");
1593    }
1594
1595    #[test]
1596    fn test_double_dashes_with_preserve_multiple_args() {
1597        // Test with multiple args where only the second has has `preserve`
1598        let run_cmd = SpecCommand::builder()
1599            .name("run")
1600            .arg(SpecArg::builder().name("task").build())
1601            .arg(
1602                SpecArg::builder()
1603                    .name("extra_args")
1604                    .var(true)
1605                    .double_dash(SpecDoubleDashChoices::Preserve)
1606                    .build(),
1607            )
1608            .build();
1609        let mut cmd = SpecCommand::builder().name("test").build();
1610        cmd.subcommands.insert("run".to_string(), run_cmd);
1611
1612        let spec = Spec {
1613            name: "test".to_string(),
1614            bin: "test".to_string(),
1615            cmd,
1616            ..Default::default()
1617        };
1618
1619        // The first arg "task1" is captured normally
1620        // Then extra_args with `preserve` captures everything, including the "--" tokens
1621        let input = vec![
1622            "test".to_string(),
1623            "run".to_string(),
1624            "task1".to_string(),
1625            "--".to_string(),
1626            "arg1".to_string(),
1627            "--".to_string(),
1628            "--foo".to_string(),
1629        ];
1630        let parsed = parse(&spec, &input).unwrap();
1631
1632        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1633        let task_value = parsed.args.get(task_arg).unwrap();
1634        assert_eq!(task_value.to_string(), "task1");
1635
1636        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1637        let extra_value = parsed.args.get(extra_arg).unwrap();
1638        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1639    }
1640
1641    #[test]
1642    fn test_parser_with_custom_env_for_required_arg() {
1643        // Test that Parser::with_env works for required args with env vars
1644        // This should NOT fail validation even though the env var is not in std::env
1645        let cmd = SpecCommand::builder()
1646            .name("test")
1647            .arg(
1648                SpecArg::builder()
1649                    .name("name")
1650                    .env("NAME")
1651                    .required(true)
1652                    .build(),
1653            )
1654            .build();
1655        let spec = Spec {
1656            name: "test".to_string(),
1657            bin: "test".to_string(),
1658            cmd,
1659            ..Default::default()
1660        };
1661
1662        // Ensure NAME is not in process env
1663        std::env::remove_var("NAME");
1664
1665        // Provide env through custom env map
1666        let mut env = HashMap::new();
1667        env.insert("NAME".to_string(), "john".to_string());
1668
1669        let input = vec!["test".to_string()];
1670        let result = Parser::new(&spec).with_env(env).parse(&input);
1671
1672        // Should succeed - custom env map should be used for validation
1673        let parsed = result.expect("parse should succeed with custom env");
1674        assert_eq!(parsed.args.len(), 1);
1675        let value = parsed.args.values().next().unwrap();
1676        assert_eq!(value.to_string(), "john");
1677    }
1678
1679    #[test]
1680    fn test_parser_with_custom_env_for_required_flag() {
1681        // Test that Parser::with_env works for required flags with env vars
1682        let cmd = SpecCommand::builder()
1683            .name("test")
1684            .flag(
1685                SpecFlag::builder()
1686                    .long("name")
1687                    .env("NAME")
1688                    .required(true)
1689                    .arg(SpecArg::builder().name("name").build())
1690                    .build(),
1691            )
1692            .build();
1693        let spec = Spec {
1694            name: "test".to_string(),
1695            bin: "test".to_string(),
1696            cmd,
1697            ..Default::default()
1698        };
1699
1700        // Ensure NAME is not in process env
1701        std::env::remove_var("NAME");
1702
1703        // Provide env through custom env map
1704        let mut env = HashMap::new();
1705        env.insert("NAME".to_string(), "jane".to_string());
1706
1707        let input = vec!["test".to_string()];
1708        let result = Parser::new(&spec).with_env(env).parse(&input);
1709
1710        // Should succeed - custom env map should be used for validation
1711        let parsed = result.expect("parse should succeed with custom env");
1712        assert_eq!(parsed.flags.len(), 1);
1713        let value = parsed.flags.values().next().unwrap();
1714        assert_eq!(value.to_string(), "jane");
1715    }
1716
1717    #[test]
1718    fn test_parser_with_custom_env_still_fails_when_missing() {
1719        // Test that validation still fails when env var is missing from both maps
1720        let cmd = SpecCommand::builder()
1721            .name("test")
1722            .arg(
1723                SpecArg::builder()
1724                    .name("name")
1725                    .env("NAME")
1726                    .required(true)
1727                    .build(),
1728            )
1729            .build();
1730        let spec = Spec {
1731            name: "test".to_string(),
1732            bin: "test".to_string(),
1733            cmd,
1734            ..Default::default()
1735        };
1736
1737        // Ensure NAME is not in process env
1738        std::env::remove_var("NAME");
1739
1740        // Provide a custom env map WITHOUT the required env var
1741        let env = HashMap::new();
1742
1743        let input = vec!["test".to_string()];
1744        let result = Parser::new(&spec).with_env(env).parse(&input);
1745
1746        // Should fail - env var is missing from both custom and process env
1747        assert!(result.is_err());
1748    }
1749}