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