usage/
parse.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, 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/// Parse command-line arguments according to a spec.
50///
51/// Returns the parsed arguments and flags, with defaults and env vars applied.
52#[must_use = "parsing result should be used"]
53pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
54    let mut out = parse_partial(spec, input)?;
55    trace!("{out:?}");
56
57    // Apply env vars and defaults for args
58    for arg in out.cmd.args.iter().skip(out.args.len()) {
59        if let Some(env_var) = arg.env.as_ref() {
60            if let Ok(env_value) = std::env::var(env_var) {
61                out.args
62                    .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
63                continue;
64            }
65        }
66        if !arg.default.is_empty() {
67            // Consider var when deciding the type of default return value
68            if arg.var {
69                // For var=true, always return a vec (MultiString)
70                out.args.insert(
71                    Arc::new(arg.clone()),
72                    ParseValue::MultiString(arg.default.clone()),
73                );
74            } else {
75                // For var=false, return the first default value as String
76                out.args.insert(
77                    Arc::new(arg.clone()),
78                    ParseValue::String(arg.default[0].clone()),
79                );
80            }
81        }
82    }
83
84    // Apply env vars and defaults for flags
85    for flag in out.available_flags.values() {
86        if out.flags.contains_key(flag) {
87            continue;
88        }
89        if let Some(env_var) = flag.env.as_ref() {
90            if let Ok(env_value) = std::env::var(env_var) {
91                if flag.arg.is_some() {
92                    out.flags
93                        .insert(Arc::clone(flag), ParseValue::String(env_value));
94                } else {
95                    // For boolean flags, check if env value is truthy
96                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
97                    out.flags
98                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
99                }
100                continue;
101            }
102        }
103        // Apply flag default
104        if !flag.default.is_empty() {
105            // Consider var when deciding the type of default return value
106            if flag.var {
107                // For var=true, always return a vec (MultiString for flags with args, MultiBool for boolean flags)
108                if flag.arg.is_some() {
109                    out.flags.insert(
110                        Arc::clone(flag),
111                        ParseValue::MultiString(flag.default.clone()),
112                    );
113                } else {
114                    // For boolean flags with var=true, convert default strings to bools
115                    let bools: Vec<bool> = flag
116                        .default
117                        .iter()
118                        .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
119                        .collect();
120                    out.flags
121                        .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
122                }
123            } else {
124                // For var=false, return the first default value
125                if flag.arg.is_some() {
126                    out.flags.insert(
127                        Arc::clone(flag),
128                        ParseValue::String(flag.default[0].clone()),
129                    );
130                } else {
131                    // For boolean flags, convert default string to bool
132                    let is_true =
133                        matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
134                    out.flags
135                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
136                }
137            }
138        }
139        // Also check nested arg defaults (for flags like --foo <arg> where the arg has a default)
140        if let Some(arg) = flag.arg.as_ref() {
141            if !out.flags.contains_key(flag) && !arg.default.is_empty() {
142                if flag.var {
143                    out.flags.insert(
144                        Arc::clone(flag),
145                        ParseValue::MultiString(arg.default.clone()),
146                    );
147                } else {
148                    out.flags
149                        .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
150                }
151            }
152        }
153    }
154    if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
155        bail!("{err}");
156    }
157    if !out.errors.is_empty() {
158        bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
159    }
160    Ok(out)
161}
162
163/// Parse command-line arguments without applying defaults.
164///
165/// Use this for help text generation or when you need the raw parsed values.
166#[must_use = "parsing result should be used"]
167pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
168    trace!("parse_partial: {input:?}");
169    let mut input = input.iter().cloned().collect::<VecDeque<_>>();
170    input.pop_front();
171
172    let gather_flags = |cmd: &SpecCommand| {
173        cmd.flags
174            .iter()
175            .flat_map(|f| {
176                let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
177                let mut flags = f
178                    .long
179                    .iter()
180                    .map(|l| (format!("--{l}"), Arc::clone(&f)))
181                    .chain(f.short.iter().map(|s| (format!("-{s}"), Arc::clone(&f))))
182                    .collect::<Vec<_>>();
183                if let Some(negate) = &f.negate {
184                    flags.push((negate.clone(), Arc::clone(&f)));
185                }
186                flags
187            })
188            .collect()
189    };
190
191    let mut out = ParseOutput {
192        cmd: spec.cmd.clone(),
193        cmds: vec![spec.cmd.clone()],
194        args: IndexMap::new(),
195        flags: IndexMap::new(),
196        available_flags: gather_flags(&spec.cmd),
197        flag_awaiting_value: vec![],
198        errors: vec![],
199    };
200
201    // Phase 1: Scan for subcommands and collect global flags
202    //
203    // This phase identifies subcommands early because they may have mount points
204    // that need to be executed with the global flags that appeared before them.
205    //
206    // Example: "usage --verbose run task"
207    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
208    //   -> then finds "task" as a subcommand of "run" (if it exists)
209    //
210    // We only collect global flags because:
211    // - Non-global flags are specific to the current command, not subcommands
212    // - Global flags affect all commands and should be passed to mount points
213    let mut prefix_words: Vec<String> = vec![];
214    let mut idx = 0;
215
216    while idx < input.len() {
217        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
218            let mut subcommand = subcommand.clone();
219            // Pass prefix words (global flags before this subcommand) to mount
220            subcommand.mount(&prefix_words)?;
221            out.available_flags.retain(|_, f| f.global);
222            out.available_flags.extend(gather_flags(&subcommand));
223            // Remove subcommand from input
224            input.remove(idx);
225            out.cmds.push(subcommand.clone());
226            out.cmd = subcommand.clone();
227            prefix_words.clear();
228            // Continue from current position (don't reset to 0)
229            // After remove(), idx now points to the next element
230        } else if input[idx].starts_with('-') {
231            // Check if this is a known flag and if it's global
232            let word = &input[idx];
233            let flag_key = get_flag_key(word);
234
235            if let Some(f) = out.available_flags.get(flag_key) {
236                // Only collect global flags for mount execution
237                if f.global {
238                    prefix_words.push(input[idx].clone());
239                    idx += 1;
240
241                    // Only consume next word if flag takes an argument AND value isn't embedded
242                    // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
243                    if f.arg.is_some()
244                        && !word.contains('=')
245                        && idx < input.len()
246                        && !input[idx].starts_with('-')
247                    {
248                        prefix_words.push(input[idx].clone());
249                        idx += 1;
250                    }
251                } else {
252                    // Non-global flag encountered - stop subcommand search
253                    // This prevents incorrect parsing like: "cmd --local-flag run"
254                    // where "run" might be mistaken for a subcommand
255                    break;
256                }
257            } else {
258                // Unknown flag - stop looking for subcommands
259                // Let the main parsing phase handle the error
260                break;
261            }
262        } else {
263            // Found a word that's not a flag or subcommand
264            // Check if we should use the default_subcommand
265            if let Some(default_name) = &spec.default_subcommand {
266                if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
267                    let mut subcommand = subcommand.clone();
268                    // Pass prefix words (global flags before this) to mount
269                    subcommand.mount(&prefix_words)?;
270                    out.available_flags.retain(|_, f| f.global);
271                    out.available_flags.extend(gather_flags(&subcommand));
272                    out.cmds.push(subcommand.clone());
273                    out.cmd = subcommand.clone();
274                    prefix_words.clear();
275                    // Don't remove the current word - it's an argument to the default subcommand
276                    // Don't increment idx - let Phase 2 handle this word as a positional arg
277                    break;
278                }
279            }
280            // This could be a positional argument, so stop subcommand search
281            break;
282        }
283    }
284
285    // Phase 2: Main argument and flag parsing
286    //
287    // Now that we've identified all subcommands and executed their mounts,
288    // we can parse the remaining arguments, flags, and their values.
289    let mut next_arg = out.cmd.args.first();
290    let mut enable_flags = true;
291    let mut grouped_flag = false;
292
293    while !input.is_empty() {
294        let mut w = input.pop_front().unwrap();
295
296        // Check for restart_token - resets argument parsing for multiple command invocations
297        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
298        if let Some(ref restart_token) = out.cmd.restart_token {
299            if w == *restart_token {
300                // Reset argument parsing state for a fresh command invocation
301                out.args.clear();
302                next_arg = out.cmd.args.first();
303                out.flag_awaiting_value.clear(); // Clear any pending flag values
304                enable_flags = true; // Reset -- separator effect
305                                     // Keep flags and continue parsing
306                continue;
307            }
308        }
309
310        if w == "--" {
311            // Always disable flag parsing after seeing a "--" token
312            enable_flags = false;
313
314            // Only preserve the double dash token if we're collecting values for a variadic arg
315            // in double_dash == `preserve` mode
316            let should_preserve = next_arg
317                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
318                .unwrap_or(false);
319
320            if should_preserve {
321                // Fall through to arg parsing
322            } else {
323                // Default behavior, skip the token
324                continue;
325            }
326        }
327
328        // long flags
329        if enable_flags && w.starts_with("--") {
330            grouped_flag = false;
331            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
332            if !val.is_empty() {
333                input.push_front(val.to_string());
334            }
335            if let Some(f) = out.available_flags.get(word) {
336                if f.arg.is_some() {
337                    out.flag_awaiting_value.push(Arc::clone(f));
338                } else if f.count {
339                    let arr = out
340                        .flags
341                        .entry(Arc::clone(f))
342                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
343                        .try_as_multi_bool_mut()
344                        .unwrap();
345                    arr.push(true);
346                } else {
347                    let negate = f.negate.clone().unwrap_or_default();
348                    out.flags
349                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
350                }
351                continue;
352            }
353            if is_help_arg(spec, &w) {
354                out.errors
355                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
356                return Ok(out);
357            }
358        }
359
360        // short flags
361        if enable_flags && w.starts_with('-') && w.len() > 1 {
362            let short = w.chars().nth(1).unwrap();
363            if let Some(f) = out.available_flags.get(&format!("-{short}")) {
364                if w.len() > 2 {
365                    input.push_front(format!("-{}", &w[2..]));
366                    grouped_flag = true;
367                }
368                if f.arg.is_some() {
369                    out.flag_awaiting_value.push(Arc::clone(f));
370                } else if f.count {
371                    let arr = out
372                        .flags
373                        .entry(Arc::clone(f))
374                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
375                        .try_as_multi_bool_mut()
376                        .unwrap();
377                    arr.push(true);
378                } else {
379                    let negate = f.negate.clone().unwrap_or_default();
380                    out.flags
381                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
382                }
383                continue;
384            }
385            if is_help_arg(spec, &w) {
386                out.errors
387                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
388                return Ok(out);
389            }
390            if grouped_flag {
391                grouped_flag = false;
392                w.remove(0);
393            }
394        }
395
396        if !out.flag_awaiting_value.is_empty() {
397            while let Some(flag) = out.flag_awaiting_value.pop() {
398                let arg = flag.arg.as_ref().unwrap();
399                if flag.var {
400                    let arr = out
401                        .flags
402                        .entry(flag)
403                        .or_insert_with(|| ParseValue::MultiString(vec![]))
404                        .try_as_multi_string_mut()
405                        .unwrap();
406                    arr.push(w);
407                } else {
408                    if let Some(choices) = &arg.choices {
409                        if !choices.choices.contains(&w) {
410                            if is_help_arg(spec, &w) {
411                                out.errors
412                                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
413                                return Ok(out);
414                            }
415                            bail!(
416                                "Invalid choice for option {}: {w}, expected one of {}",
417                                flag.name,
418                                choices.choices.join(", ")
419                            );
420                        }
421                    }
422                    out.flags.insert(flag, ParseValue::String(w));
423                }
424                w = "".to_string();
425            }
426            continue;
427        }
428
429        if let Some(arg) = next_arg {
430            if arg.var {
431                let arr = out
432                    .args
433                    .entry(Arc::new(arg.clone()))
434                    .or_insert_with(|| ParseValue::MultiString(vec![]))
435                    .try_as_multi_string_mut()
436                    .unwrap();
437                arr.push(w);
438                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
439                    next_arg = out.cmd.args.get(out.args.len());
440                }
441            } else {
442                if let Some(choices) = &arg.choices {
443                    if !choices.choices.contains(&w) {
444                        if is_help_arg(spec, &w) {
445                            out.errors
446                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
447                            return Ok(out);
448                        }
449                        bail!(
450                            "Invalid choice for arg {}: {w}, expected one of {}",
451                            arg.name,
452                            choices.choices.join(", ")
453                        );
454                    }
455                }
456                out.args
457                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
458                next_arg = out.cmd.args.get(out.args.len());
459            }
460            continue;
461        }
462        if is_help_arg(spec, &w) {
463            out.errors
464                .push(render_help_err(spec, &out.cmd, w.len() > 2));
465            return Ok(out);
466        }
467        bail!("unexpected word: {w}");
468    }
469
470    for arg in out.cmd.args.iter().skip(out.args.len()) {
471        if arg.required && arg.default.is_empty() {
472            // Check if there's an env var available
473            let has_env = arg
474                .env
475                .as_ref()
476                .map(|e| std::env::var(e).is_ok())
477                .unwrap_or(false);
478            if !has_env {
479                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
480            }
481        }
482    }
483
484    for flag in out.available_flags.values() {
485        if out.flags.contains_key(flag) {
486            continue;
487        }
488        let has_default =
489            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
490        let has_env = flag
491            .env
492            .as_ref()
493            .map(|e| std::env::var(e).is_ok())
494            .unwrap_or(false);
495        if flag.required && !has_default && !has_env {
496            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
497        }
498    }
499
500    // Validate var_min/var_max constraints for variadic args
501    for (arg, value) in &out.args {
502        if arg.var {
503            if let ParseValue::MultiString(values) = value {
504                if let Some(min) = arg.var_min {
505                    if values.len() < min {
506                        out.errors.push(UsageErr::VarArgTooFew {
507                            name: arg.name.clone(),
508                            min,
509                            got: values.len(),
510                        });
511                    }
512                }
513                if let Some(max) = arg.var_max {
514                    if values.len() > max {
515                        out.errors.push(UsageErr::VarArgTooMany {
516                            name: arg.name.clone(),
517                            max,
518                            got: values.len(),
519                        });
520                    }
521                }
522            }
523        }
524    }
525
526    // Validate var_min/var_max constraints for variadic flags
527    for (flag, value) in &out.flags {
528        if flag.var {
529            let count = match value {
530                ParseValue::MultiString(values) => values.len(),
531                ParseValue::MultiBool(values) => values.len(),
532                _ => continue,
533            };
534            if let Some(min) = flag.var_min {
535                if count < min {
536                    out.errors.push(UsageErr::VarFlagTooFew {
537                        name: flag.name.clone(),
538                        min,
539                        got: count,
540                    });
541                }
542            }
543            if let Some(max) = flag.var_max {
544                if count > max {
545                    out.errors.push(UsageErr::VarFlagTooMany {
546                        name: flag.name.clone(),
547                        max,
548                        got: count,
549                    });
550                }
551            }
552        }
553    }
554
555    Ok(out)
556}
557
558#[cfg(feature = "docs")]
559fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
560    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
561}
562
563#[cfg(not(feature = "docs"))]
564fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
565    UsageErr::Help("help".to_string())
566}
567
568fn is_help_arg(spec: &Spec, w: &str) -> bool {
569    spec.disable_help != Some(true)
570        && (w == "--help"
571            || w == "-h"
572            || w == "-?"
573            || (spec.cmd.subcommands.is_empty() && w == "help"))
574}
575
576impl ParseOutput {
577    pub fn as_env(&self) -> BTreeMap<String, String> {
578        let mut env = BTreeMap::new();
579        for (flag, val) in &self.flags {
580            let key = format!("usage_{}", flag.name.to_snake_case());
581            let val = match val {
582                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
583                ParseValue::String(s) => s.clone(),
584                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
585                ParseValue::MultiString(s) => shell_words::join(s),
586            };
587            env.insert(key, val);
588        }
589        for (arg, val) in &self.args {
590            let key = format!("usage_{}", arg.name.to_snake_case());
591            env.insert(key, val.to_string());
592        }
593        env
594    }
595}
596
597impl Display for ParseValue {
598    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
599        match self {
600            ParseValue::Bool(b) => write!(f, "{b}"),
601            ParseValue::String(s) => write!(f, "{s}"),
602            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
603            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
604        }
605    }
606}
607
608impl Debug for ParseOutput {
609    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
610        f.debug_struct("ParseOutput")
611            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
612            .field(
613                "args",
614                &self
615                    .args
616                    .iter()
617                    .map(|(a, w)| format!("{}: {w}", &a.name))
618                    .collect_vec(),
619            )
620            .field(
621                "available_flags",
622                &self
623                    .available_flags
624                    .iter()
625                    .map(|(f, w)| format!("{f}: {w}"))
626                    .collect_vec(),
627            )
628            .field(
629                "flags",
630                &self
631                    .flags
632                    .iter()
633                    .map(|(f, w)| format!("{}: {w}", &f.name))
634                    .collect_vec(),
635            )
636            .field("flag_awaiting_value", &self.flag_awaiting_value)
637            .field("errors", &self.errors)
638            .finish()
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    #[test]
647    fn test_parse() {
648        let cmd = SpecCommand::builder()
649            .name("test")
650            .arg(SpecArg::builder().name("arg").build())
651            .flag(SpecFlag::builder().long("flag").build())
652            .build();
653        let spec = Spec {
654            name: "test".to_string(),
655            bin: "test".to_string(),
656            cmd,
657            ..Default::default()
658        };
659        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
660        let parsed = parse(&spec, &input).unwrap();
661        assert_eq!(parsed.cmds.len(), 1);
662        assert_eq!(parsed.cmds[0].name, "test");
663        assert_eq!(parsed.args.len(), 1);
664        assert_eq!(parsed.flags.len(), 1);
665        assert_eq!(parsed.available_flags.len(), 1);
666    }
667
668    #[test]
669    fn test_as_env() {
670        let cmd = SpecCommand::builder()
671            .name("test")
672            .arg(SpecArg::builder().name("arg").build())
673            .flag(SpecFlag::builder().long("flag").build())
674            .flag(
675                SpecFlag::builder()
676                    .long("force")
677                    .negate("--no-force")
678                    .build(),
679            )
680            .build();
681        let spec = Spec {
682            name: "test".to_string(),
683            bin: "test".to_string(),
684            cmd,
685            ..Default::default()
686        };
687        let input = vec![
688            "test".to_string(),
689            "--flag".to_string(),
690            "--no-force".to_string(),
691        ];
692        let parsed = parse(&spec, &input).unwrap();
693        let env = parsed.as_env();
694        assert_eq!(env.len(), 2);
695        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
696        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
697    }
698
699    #[test]
700    fn test_arg_env_var() {
701        let cmd = SpecCommand::builder()
702            .name("test")
703            .arg(
704                SpecArg::builder()
705                    .name("input")
706                    .env("TEST_ARG_INPUT")
707                    .required(true)
708                    .build(),
709            )
710            .build();
711        let spec = Spec {
712            name: "test".to_string(),
713            bin: "test".to_string(),
714            cmd,
715            ..Default::default()
716        };
717
718        // Set env var
719        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
720
721        let input = vec!["test".to_string()];
722        let parsed = parse(&spec, &input).unwrap();
723
724        assert_eq!(parsed.args.len(), 1);
725        let arg = parsed.args.keys().next().unwrap();
726        assert_eq!(arg.name, "input");
727        let value = parsed.args.values().next().unwrap();
728        assert_eq!(value.to_string(), "test_file.txt");
729
730        // Clean up
731        std::env::remove_var("TEST_ARG_INPUT");
732    }
733
734    #[test]
735    fn test_flag_env_var_with_arg() {
736        let cmd = SpecCommand::builder()
737            .name("test")
738            .flag(
739                SpecFlag::builder()
740                    .long("output")
741                    .env("TEST_FLAG_OUTPUT")
742                    .arg(SpecArg::builder().name("file").build())
743                    .build(),
744            )
745            .build();
746        let spec = Spec {
747            name: "test".to_string(),
748            bin: "test".to_string(),
749            cmd,
750            ..Default::default()
751        };
752
753        // Set env var
754        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
755
756        let input = vec!["test".to_string()];
757        let parsed = parse(&spec, &input).unwrap();
758
759        assert_eq!(parsed.flags.len(), 1);
760        let flag = parsed.flags.keys().next().unwrap();
761        assert_eq!(flag.name, "output");
762        let value = parsed.flags.values().next().unwrap();
763        assert_eq!(value.to_string(), "output.txt");
764
765        // Clean up
766        std::env::remove_var("TEST_FLAG_OUTPUT");
767    }
768
769    #[test]
770    fn test_flag_env_var_boolean() {
771        let cmd = SpecCommand::builder()
772            .name("test")
773            .flag(
774                SpecFlag::builder()
775                    .long("verbose")
776                    .env("TEST_FLAG_VERBOSE")
777                    .build(),
778            )
779            .build();
780        let spec = Spec {
781            name: "test".to_string(),
782            bin: "test".to_string(),
783            cmd,
784            ..Default::default()
785        };
786
787        // Set env var to true
788        std::env::set_var("TEST_FLAG_VERBOSE", "true");
789
790        let input = vec!["test".to_string()];
791        let parsed = parse(&spec, &input).unwrap();
792
793        assert_eq!(parsed.flags.len(), 1);
794        let flag = parsed.flags.keys().next().unwrap();
795        assert_eq!(flag.name, "verbose");
796        let value = parsed.flags.values().next().unwrap();
797        assert_eq!(value.to_string(), "true");
798
799        // Clean up
800        std::env::remove_var("TEST_FLAG_VERBOSE");
801    }
802
803    #[test]
804    fn test_env_var_precedence() {
805        // CLI args should take precedence over env vars
806        let cmd = SpecCommand::builder()
807            .name("test")
808            .arg(
809                SpecArg::builder()
810                    .name("input")
811                    .env("TEST_PRECEDENCE_INPUT")
812                    .required(true)
813                    .build(),
814            )
815            .build();
816        let spec = Spec {
817            name: "test".to_string(),
818            bin: "test".to_string(),
819            cmd,
820            ..Default::default()
821        };
822
823        // Set env var
824        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
825
826        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
827        let parsed = parse(&spec, &input).unwrap();
828
829        assert_eq!(parsed.args.len(), 1);
830        let value = parsed.args.values().next().unwrap();
831        // CLI arg should take precedence
832        assert_eq!(value.to_string(), "cli_file.txt");
833
834        // Clean up
835        std::env::remove_var("TEST_PRECEDENCE_INPUT");
836    }
837
838    #[test]
839    fn test_flag_var_true_with_single_default() {
840        // When var=true and default="bar", the default should be MultiString(["bar"])
841        let cmd = SpecCommand::builder()
842            .name("test")
843            .flag(
844                SpecFlag::builder()
845                    .long("foo")
846                    .var(true)
847                    .arg(SpecArg::builder().name("foo").build())
848                    .default_value("bar")
849                    .build(),
850            )
851            .build();
852        let spec = Spec {
853            name: "test".to_string(),
854            bin: "test".to_string(),
855            cmd,
856            ..Default::default()
857        };
858
859        // User doesn't provide the flag
860        let input = vec!["test".to_string()];
861        let parsed = parse(&spec, &input).unwrap();
862
863        assert_eq!(parsed.flags.len(), 1);
864        let flag = parsed.flags.keys().next().unwrap();
865        assert_eq!(flag.name, "foo");
866        let value = parsed.flags.values().next().unwrap();
867        // Should be MultiString, not String
868        match value {
869            ParseValue::MultiString(v) => {
870                assert_eq!(v.len(), 1);
871                assert_eq!(v[0], "bar");
872            }
873            _ => panic!("Expected MultiString, got {:?}", value),
874        }
875    }
876
877    #[test]
878    fn test_flag_var_true_with_multiple_defaults() {
879        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
880        let cmd = SpecCommand::builder()
881            .name("test")
882            .flag(
883                SpecFlag::builder()
884                    .long("foo")
885                    .var(true)
886                    .arg(SpecArg::builder().name("foo").build())
887                    .default_values(["xyz", "bar"])
888                    .build(),
889            )
890            .build();
891        let spec = Spec {
892            name: "test".to_string(),
893            bin: "test".to_string(),
894            cmd,
895            ..Default::default()
896        };
897
898        // User doesn't provide the flag
899        let input = vec!["test".to_string()];
900        let parsed = parse(&spec, &input).unwrap();
901
902        assert_eq!(parsed.flags.len(), 1);
903        let value = parsed.flags.values().next().unwrap();
904        // Should be MultiString with both values
905        match value {
906            ParseValue::MultiString(v) => {
907                assert_eq!(v.len(), 2);
908                assert_eq!(v[0], "xyz");
909                assert_eq!(v[1], "bar");
910            }
911            _ => panic!("Expected MultiString, got {:?}", value),
912        }
913    }
914
915    #[test]
916    fn test_flag_var_false_with_default_remains_string() {
917        // When var=false (default), the default should still be String("bar")
918        let cmd = SpecCommand::builder()
919            .name("test")
920            .flag(
921                SpecFlag::builder()
922                    .long("foo")
923                    .var(false) // Default behavior
924                    .arg(SpecArg::builder().name("foo").build())
925                    .default_value("bar")
926                    .build(),
927            )
928            .build();
929        let spec = Spec {
930            name: "test".to_string(),
931            bin: "test".to_string(),
932            cmd,
933            ..Default::default()
934        };
935
936        // User doesn't provide the flag
937        let input = vec!["test".to_string()];
938        let parsed = parse(&spec, &input).unwrap();
939
940        assert_eq!(parsed.flags.len(), 1);
941        let value = parsed.flags.values().next().unwrap();
942        // Should be String, not MultiString
943        match value {
944            ParseValue::String(s) => {
945                assert_eq!(s, "bar");
946            }
947            _ => panic!("Expected String, got {:?}", value),
948        }
949    }
950
951    #[test]
952    fn test_arg_var_true_with_single_default() {
953        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
954        let cmd = SpecCommand::builder()
955            .name("test")
956            .arg(
957                SpecArg::builder()
958                    .name("files")
959                    .var(true)
960                    .default_value("default.txt")
961                    .required(false)
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 arg
973        let input = vec!["test".to_string()];
974        let parsed = parse(&spec, &input).unwrap();
975
976        assert_eq!(parsed.args.len(), 1);
977        let value = parsed.args.values().next().unwrap();
978        // Should be MultiString, not String
979        match value {
980            ParseValue::MultiString(v) => {
981                assert_eq!(v.len(), 1);
982                assert_eq!(v[0], "default.txt");
983            }
984            _ => panic!("Expected MultiString, got {:?}", value),
985        }
986    }
987
988    #[test]
989    fn test_arg_var_true_with_multiple_defaults() {
990        // When arg has var=true and multiple defaults
991        let cmd = SpecCommand::builder()
992            .name("test")
993            .arg(
994                SpecArg::builder()
995                    .name("files")
996                    .var(true)
997                    .default_values(["file1.txt", "file2.txt"])
998                    .required(false)
999                    .build(),
1000            )
1001            .build();
1002        let spec = Spec {
1003            name: "test".to_string(),
1004            bin: "test".to_string(),
1005            cmd,
1006            ..Default::default()
1007        };
1008
1009        // User doesn't provide the arg
1010        let input = vec!["test".to_string()];
1011        let parsed = parse(&spec, &input).unwrap();
1012
1013        assert_eq!(parsed.args.len(), 1);
1014        let value = parsed.args.values().next().unwrap();
1015        // Should be MultiString with both values
1016        match value {
1017            ParseValue::MultiString(v) => {
1018                assert_eq!(v.len(), 2);
1019                assert_eq!(v[0], "file1.txt");
1020                assert_eq!(v[1], "file2.txt");
1021            }
1022            _ => panic!("Expected MultiString, got {:?}", value),
1023        }
1024    }
1025
1026    #[test]
1027    fn test_arg_var_false_with_default_remains_string() {
1028        // When arg has var=false (default), the default should still be String
1029        let cmd = SpecCommand::builder()
1030            .name("test")
1031            .arg(
1032                SpecArg::builder()
1033                    .name("file")
1034                    .var(false)
1035                    .default_value("default.txt")
1036                    .required(false)
1037                    .build(),
1038            )
1039            .build();
1040        let spec = Spec {
1041            name: "test".to_string(),
1042            bin: "test".to_string(),
1043            cmd,
1044            ..Default::default()
1045        };
1046
1047        // User doesn't provide the arg
1048        let input = vec!["test".to_string()];
1049        let parsed = parse(&spec, &input).unwrap();
1050
1051        assert_eq!(parsed.args.len(), 1);
1052        let value = parsed.args.values().next().unwrap();
1053        // Should be String, not MultiString
1054        match value {
1055            ParseValue::String(s) => {
1056                assert_eq!(s, "default.txt");
1057            }
1058            _ => panic!("Expected String, got {:?}", value),
1059        }
1060    }
1061
1062    #[test]
1063    fn test_default_subcommand() {
1064        // Test that default_subcommand routes to the specified subcommand
1065        let run_cmd = SpecCommand::builder()
1066            .name("run")
1067            .arg(SpecArg::builder().name("task").build())
1068            .build();
1069        let mut cmd = SpecCommand::builder().name("test").build();
1070        cmd.subcommands.insert("run".to_string(), run_cmd);
1071
1072        let spec = Spec {
1073            name: "test".to_string(),
1074            bin: "test".to_string(),
1075            cmd,
1076            default_subcommand: Some("run".to_string()),
1077            ..Default::default()
1078        };
1079
1080        // "test mytask" should be parsed as if it were "test run mytask"
1081        let input = vec!["test".to_string(), "mytask".to_string()];
1082        let parsed = parse(&spec, &input).unwrap();
1083
1084        // Should have two commands: root and "run"
1085        assert_eq!(parsed.cmds.len(), 2);
1086        assert_eq!(parsed.cmds[1].name, "run");
1087
1088        // Should have parsed the task argument
1089        assert_eq!(parsed.args.len(), 1);
1090        let arg = parsed.args.keys().next().unwrap();
1091        assert_eq!(arg.name, "task");
1092        let value = parsed.args.values().next().unwrap();
1093        assert_eq!(value.to_string(), "mytask");
1094    }
1095
1096    #[test]
1097    fn test_default_subcommand_explicit_still_works() {
1098        // Test that explicit subcommand takes precedence
1099        let run_cmd = SpecCommand::builder()
1100            .name("run")
1101            .arg(SpecArg::builder().name("task").build())
1102            .build();
1103        let other_cmd = SpecCommand::builder()
1104            .name("other")
1105            .arg(SpecArg::builder().name("other_arg").build())
1106            .build();
1107        let mut cmd = SpecCommand::builder().name("test").build();
1108        cmd.subcommands.insert("run".to_string(), run_cmd);
1109        cmd.subcommands.insert("other".to_string(), other_cmd);
1110
1111        let spec = Spec {
1112            name: "test".to_string(),
1113            bin: "test".to_string(),
1114            cmd,
1115            default_subcommand: Some("run".to_string()),
1116            ..Default::default()
1117        };
1118
1119        // "test other foo" should use "other" subcommand, not default
1120        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1121        let parsed = parse(&spec, &input).unwrap();
1122
1123        // Should have used "other" subcommand
1124        assert_eq!(parsed.cmds.len(), 2);
1125        assert_eq!(parsed.cmds[1].name, "other");
1126    }
1127
1128    #[test]
1129    fn test_restart_token() {
1130        // Test that restart_token resets argument parsing
1131        let run_cmd = SpecCommand::builder()
1132            .name("run")
1133            .arg(SpecArg::builder().name("task").build())
1134            .restart_token(":::".to_string())
1135            .build();
1136        let mut cmd = SpecCommand::builder().name("test").build();
1137        cmd.subcommands.insert("run".to_string(), run_cmd);
1138
1139        let spec = Spec {
1140            name: "test".to_string(),
1141            bin: "test".to_string(),
1142            cmd,
1143            ..Default::default()
1144        };
1145
1146        // "test run task1 ::: task2" - should end up with task2 as the arg
1147        let input = vec![
1148            "test".to_string(),
1149            "run".to_string(),
1150            "task1".to_string(),
1151            ":::".to_string(),
1152            "task2".to_string(),
1153        ];
1154        let parsed = parse(&spec, &input).unwrap();
1155
1156        // After restart, args were cleared and task2 was parsed
1157        assert_eq!(parsed.args.len(), 1);
1158        let value = parsed.args.values().next().unwrap();
1159        assert_eq!(value.to_string(), "task2");
1160    }
1161
1162    #[test]
1163    fn test_restart_token_multiple() {
1164        // Test multiple restart tokens
1165        let run_cmd = SpecCommand::builder()
1166            .name("run")
1167            .arg(SpecArg::builder().name("task").build())
1168            .restart_token(":::".to_string())
1169            .build();
1170        let mut cmd = SpecCommand::builder().name("test").build();
1171        cmd.subcommands.insert("run".to_string(), run_cmd);
1172
1173        let spec = Spec {
1174            name: "test".to_string(),
1175            bin: "test".to_string(),
1176            cmd,
1177            ..Default::default()
1178        };
1179
1180        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
1181        let input = vec![
1182            "test".to_string(),
1183            "run".to_string(),
1184            "task1".to_string(),
1185            ":::".to_string(),
1186            "task2".to_string(),
1187            ":::".to_string(),
1188            "task3".to_string(),
1189        ];
1190        let parsed = parse(&spec, &input).unwrap();
1191
1192        // After multiple restarts, args were cleared and task3 was parsed
1193        assert_eq!(parsed.args.len(), 1);
1194        let value = parsed.args.values().next().unwrap();
1195        assert_eq!(value.to_string(), "task3");
1196    }
1197
1198    #[test]
1199    fn test_restart_token_clears_flag_awaiting_value() {
1200        // Test that restart_token clears pending flag values
1201        let run_cmd = SpecCommand::builder()
1202            .name("run")
1203            .arg(SpecArg::builder().name("task").build())
1204            .flag(
1205                SpecFlag::builder()
1206                    .name("jobs")
1207                    .long("jobs")
1208                    .arg(SpecArg::builder().name("count").build())
1209                    .build(),
1210            )
1211            .restart_token(":::".to_string())
1212            .build();
1213        let mut cmd = SpecCommand::builder().name("test").build();
1214        cmd.subcommands.insert("run".to_string(), run_cmd);
1215
1216        let spec = Spec {
1217            name: "test".to_string(),
1218            bin: "test".to_string(),
1219            cmd,
1220            ..Default::default()
1221        };
1222
1223        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
1224        let input = vec![
1225            "test".to_string(),
1226            "run".to_string(),
1227            "task1".to_string(),
1228            "--jobs".to_string(),
1229            ":::".to_string(),
1230            "task2".to_string(),
1231        ];
1232        let parsed = parse(&spec, &input).unwrap();
1233
1234        // task2 should be parsed as the task arg, not as --jobs value
1235        assert_eq!(parsed.args.len(), 1);
1236        let value = parsed.args.values().next().unwrap();
1237        assert_eq!(value.to_string(), "task2");
1238        // --jobs should not have a value
1239        assert!(parsed.flag_awaiting_value.is_empty());
1240    }
1241
1242    #[test]
1243    fn test_restart_token_resets_double_dash() {
1244        // Test that restart_token resets the -- separator effect
1245        let run_cmd = SpecCommand::builder()
1246            .name("run")
1247            .arg(SpecArg::builder().name("task").build())
1248            .arg(SpecArg::builder().name("extra_args").var(true).build())
1249            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1250            .restart_token(":::".to_string())
1251            .build();
1252        let mut cmd = SpecCommand::builder().name("test").build();
1253        cmd.subcommands.insert("run".to_string(), run_cmd);
1254
1255        let spec = Spec {
1256            name: "test".to_string(),
1257            bin: "test".to_string(),
1258            cmd,
1259            ..Default::default()
1260        };
1261
1262        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
1263        let input = vec![
1264            "test".to_string(),
1265            "run".to_string(),
1266            "task1".to_string(),
1267            "--".to_string(),
1268            "extra".to_string(),
1269            ":::".to_string(),
1270            "--verbose".to_string(),
1271            "task2".to_string(),
1272        ];
1273        let parsed = parse(&spec, &input).unwrap();
1274
1275        // --verbose should be parsed as a flag (not an arg) after the restart
1276        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1277        // task2 should be the arg after restart
1278        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1279        let value = parsed.args.get(task_arg).unwrap();
1280        assert_eq!(value.to_string(), "task2");
1281    }
1282
1283    #[test]
1284    fn test_double_dashes_without_preserve() {
1285        // Test that variadic args WITHOUT `preserve` skip "--" tokens (default behavior)
1286        let run_cmd = SpecCommand::builder()
1287            .name("run")
1288            .arg(SpecArg::builder().name("args").var(true).build())
1289            .build();
1290        let mut cmd = SpecCommand::builder().name("test").build();
1291        cmd.subcommands.insert("run".to_string(), run_cmd);
1292
1293        let spec = Spec {
1294            name: "test".to_string(),
1295            bin: "test".to_string(),
1296            cmd,
1297            ..Default::default()
1298        };
1299
1300        // "test run arg1 -- arg2 -- arg3" - all double dashes should be skipped
1301        let input = vec![
1302            "test".to_string(),
1303            "run".to_string(),
1304            "arg1".to_string(),
1305            "--".to_string(),
1306            "arg2".to_string(),
1307            "--".to_string(),
1308            "arg3".to_string(),
1309        ];
1310        let parsed = parse(&spec, &input).unwrap();
1311
1312        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1313        let value = parsed.args.get(args_arg).unwrap();
1314        assert_eq!(value.to_string(), "arg1 arg2 arg3");
1315    }
1316
1317    #[test]
1318    fn test_double_dashes_with_preserve() {
1319        // Test that variadic args WITH `preserve` keep all double dashes
1320        let run_cmd = SpecCommand::builder()
1321            .name("run")
1322            .arg(
1323                SpecArg::builder()
1324                    .name("args")
1325                    .var(true)
1326                    .double_dash(SpecDoubleDashChoices::Preserve)
1327                    .build(),
1328            )
1329            .build();
1330        let mut cmd = SpecCommand::builder().name("test").build();
1331        cmd.subcommands.insert("run".to_string(), run_cmd);
1332
1333        let spec = Spec {
1334            name: "test".to_string(),
1335            bin: "test".to_string(),
1336            cmd,
1337            ..Default::default()
1338        };
1339
1340        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
1341        let input = vec![
1342            "test".to_string(),
1343            "run".to_string(),
1344            "arg1".to_string(),
1345            "--".to_string(),
1346            "arg2".to_string(),
1347            "--".to_string(),
1348            "arg3".to_string(),
1349        ];
1350        let parsed = parse(&spec, &input).unwrap();
1351
1352        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1353        let value = parsed.args.get(args_arg).unwrap();
1354        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1355    }
1356
1357    #[test]
1358    fn test_double_dashes_with_preserve_only_dashes() {
1359        // Test that variadic args WITH `preserve` keep all double dashes even
1360        // if the values are just double dashes
1361        let run_cmd = SpecCommand::builder()
1362            .name("run")
1363            .arg(
1364                SpecArg::builder()
1365                    .name("args")
1366                    .var(true)
1367                    .double_dash(SpecDoubleDashChoices::Preserve)
1368                    .build(),
1369            )
1370            .build();
1371        let mut cmd = SpecCommand::builder().name("test").build();
1372        cmd.subcommands.insert("run".to_string(), run_cmd);
1373
1374        let spec = Spec {
1375            name: "test".to_string(),
1376            bin: "test".to_string(),
1377            cmd,
1378            ..Default::default()
1379        };
1380
1381        // "test run -- --" - all double dashes should be preserved
1382        let input = vec![
1383            "test".to_string(),
1384            "run".to_string(),
1385            "--".to_string(),
1386            "--".to_string(),
1387        ];
1388        let parsed = parse(&spec, &input).unwrap();
1389
1390        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1391        let value = parsed.args.get(args_arg).unwrap();
1392        assert_eq!(value.to_string(), "-- --");
1393    }
1394
1395    #[test]
1396    fn test_double_dashes_with_preserve_multiple_args() {
1397        // Test with multiple args where only the second has has `preserve`
1398        let run_cmd = SpecCommand::builder()
1399            .name("run")
1400            .arg(SpecArg::builder().name("task").build())
1401            .arg(
1402                SpecArg::builder()
1403                    .name("extra_args")
1404                    .var(true)
1405                    .double_dash(SpecDoubleDashChoices::Preserve)
1406                    .build(),
1407            )
1408            .build();
1409        let mut cmd = SpecCommand::builder().name("test").build();
1410        cmd.subcommands.insert("run".to_string(), run_cmd);
1411
1412        let spec = Spec {
1413            name: "test".to_string(),
1414            bin: "test".to_string(),
1415            cmd,
1416            ..Default::default()
1417        };
1418
1419        // The first arg "task1" is captured normally
1420        // Then extra_args with `preserve` captures everything, including the "--" tokens
1421        let input = vec![
1422            "test".to_string(),
1423            "run".to_string(),
1424            "task1".to_string(),
1425            "--".to_string(),
1426            "arg1".to_string(),
1427            "--".to_string(),
1428            "--foo".to_string(),
1429        ];
1430        let parsed = parse(&spec, &input).unwrap();
1431
1432        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1433        let task_value = parsed.args.get(task_arg).unwrap();
1434        assert_eq!(task_value.to_string(), "task1");
1435
1436        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1437        let extra_value = parsed.args.get(extra_arg).unwrap();
1438        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1439    }
1440}