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