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            // This could be a positional argument, so stop subcommand search
240            break;
241        }
242    }
243
244    // Phase 2: Main argument and flag parsing
245    //
246    // Now that we've identified all subcommands and executed their mounts,
247    // we can parse the remaining arguments, flags, and their values.
248    let mut next_arg = out.cmd.args.first();
249    let mut enable_flags = true;
250    let mut grouped_flag = false;
251
252    while !input.is_empty() {
253        let mut w = input.pop_front().unwrap();
254
255        if w == "--" {
256            enable_flags = false;
257            continue;
258        }
259
260        // long flags
261        if enable_flags && w.starts_with("--") {
262            grouped_flag = false;
263            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
264            if !val.is_empty() {
265                input.push_front(val.to_string());
266            }
267            if let Some(f) = out.available_flags.get(word) {
268                if f.arg.is_some() {
269                    out.flag_awaiting_value.push(f.clone());
270                } else if f.count {
271                    let arr = out
272                        .flags
273                        .entry(f.clone())
274                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
275                        .try_as_multi_bool_mut()
276                        .unwrap();
277                    arr.push(true);
278                } else {
279                    let negate = f.negate.clone().unwrap_or_default();
280                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
281                }
282                continue;
283            }
284            if is_help_arg(spec, &w) {
285                out.errors
286                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
287                return Ok(out);
288            }
289        }
290
291        // short flags
292        if enable_flags && w.starts_with('-') && w.len() > 1 {
293            let short = w.chars().nth(1).unwrap();
294            if let Some(f) = out.available_flags.get(&format!("-{short}")) {
295                if w.len() > 2 {
296                    input.push_front(format!("-{}", &w[2..]));
297                    grouped_flag = true;
298                }
299                if f.arg.is_some() {
300                    out.flag_awaiting_value.push(f.clone());
301                } else if f.count {
302                    let arr = out
303                        .flags
304                        .entry(f.clone())
305                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
306                        .try_as_multi_bool_mut()
307                        .unwrap();
308                    arr.push(true);
309                } else {
310                    let negate = f.negate.clone().unwrap_or_default();
311                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
312                }
313                continue;
314            }
315            if is_help_arg(spec, &w) {
316                out.errors
317                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
318                return Ok(out);
319            }
320            if grouped_flag {
321                grouped_flag = false;
322                w.remove(0);
323            }
324        }
325
326        if !out.flag_awaiting_value.is_empty() {
327            while let Some(flag) = out.flag_awaiting_value.pop() {
328                let arg = flag.arg.as_ref().unwrap();
329                if flag.var {
330                    let arr = out
331                        .flags
332                        .entry(flag)
333                        .or_insert_with(|| ParseValue::MultiString(vec![]))
334                        .try_as_multi_string_mut()
335                        .unwrap();
336                    arr.push(w);
337                } else {
338                    if let Some(choices) = &arg.choices {
339                        if !choices.choices.contains(&w) {
340                            if is_help_arg(spec, &w) {
341                                out.errors
342                                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
343                                return Ok(out);
344                            }
345                            bail!(
346                                "Invalid choice for option {}: {w}, expected one of {}",
347                                flag.name,
348                                choices.choices.join(", ")
349                            );
350                        }
351                    }
352                    out.flags.insert(flag, ParseValue::String(w));
353                }
354                w = "".to_string();
355            }
356            continue;
357        }
358
359        if let Some(arg) = next_arg {
360            if arg.var {
361                let arr = out
362                    .args
363                    .entry(arg.clone())
364                    .or_insert_with(|| ParseValue::MultiString(vec![]))
365                    .try_as_multi_string_mut()
366                    .unwrap();
367                arr.push(w);
368                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
369                    next_arg = out.cmd.args.get(out.args.len());
370                }
371            } else {
372                if let Some(choices) = &arg.choices {
373                    if !choices.choices.contains(&w) {
374                        if is_help_arg(spec, &w) {
375                            out.errors
376                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
377                            return Ok(out);
378                        }
379                        bail!(
380                            "Invalid choice for arg {}: {w}, expected one of {}",
381                            arg.name,
382                            choices.choices.join(", ")
383                        );
384                    }
385                }
386                out.args.insert(arg.clone(), ParseValue::String(w));
387                next_arg = out.cmd.args.get(out.args.len());
388            }
389            continue;
390        }
391        if is_help_arg(spec, &w) {
392            out.errors
393                .push(render_help_err(spec, &out.cmd, w.len() > 2));
394            return Ok(out);
395        }
396        bail!("unexpected word: {w}");
397    }
398
399    for arg in out.cmd.args.iter().skip(out.args.len()) {
400        if arg.required && arg.default.is_empty() {
401            // Check if there's an env var available
402            let has_env = arg
403                .env
404                .as_ref()
405                .map(|e| std::env::var(e).is_ok())
406                .unwrap_or(false);
407            if !has_env {
408                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
409            }
410        }
411    }
412
413    for flag in out.available_flags.values() {
414        if out.flags.contains_key(flag) {
415            continue;
416        }
417        let has_default =
418            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
419        let has_env = flag
420            .env
421            .as_ref()
422            .map(|e| std::env::var(e).is_ok())
423            .unwrap_or(false);
424        if flag.required && !has_default && !has_env {
425            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
426        }
427    }
428
429    // Validate var_min/var_max constraints for variadic args
430    for (arg, value) in &out.args {
431        if arg.var {
432            if let ParseValue::MultiString(values) = value {
433                if let Some(min) = arg.var_min {
434                    if values.len() < min {
435                        out.errors.push(UsageErr::VarArgTooFew {
436                            name: arg.name.clone(),
437                            min,
438                            got: values.len(),
439                        });
440                    }
441                }
442                if let Some(max) = arg.var_max {
443                    if values.len() > max {
444                        out.errors.push(UsageErr::VarArgTooMany {
445                            name: arg.name.clone(),
446                            max,
447                            got: values.len(),
448                        });
449                    }
450                }
451            }
452        }
453    }
454
455    // Validate var_min/var_max constraints for variadic flags
456    for (flag, value) in &out.flags {
457        if flag.var {
458            let count = match value {
459                ParseValue::MultiString(values) => values.len(),
460                ParseValue::MultiBool(values) => values.len(),
461                _ => continue,
462            };
463            if let Some(min) = flag.var_min {
464                if count < min {
465                    out.errors.push(UsageErr::VarFlagTooFew {
466                        name: flag.name.clone(),
467                        min,
468                        got: count,
469                    });
470                }
471            }
472            if let Some(max) = flag.var_max {
473                if count > max {
474                    out.errors.push(UsageErr::VarFlagTooMany {
475                        name: flag.name.clone(),
476                        max,
477                        got: count,
478                    });
479                }
480            }
481        }
482    }
483
484    Ok(out)
485}
486
487#[cfg(feature = "docs")]
488fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
489    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
490}
491
492#[cfg(not(feature = "docs"))]
493fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
494    UsageErr::Help("help".to_string())
495}
496
497fn is_help_arg(spec: &Spec, w: &str) -> bool {
498    spec.disable_help != Some(true)
499        && (w == "--help"
500            || w == "-h"
501            || w == "-?"
502            || (spec.cmd.subcommands.is_empty() && w == "help"))
503}
504
505impl ParseOutput {
506    pub fn as_env(&self) -> BTreeMap<String, String> {
507        let mut env = BTreeMap::new();
508        for (flag, val) in &self.flags {
509            let key = format!("usage_{}", flag.name.to_snake_case());
510            let val = match val {
511                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
512                ParseValue::String(s) => s.clone(),
513                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
514                ParseValue::MultiString(s) => shell_words::join(s),
515            };
516            env.insert(key, val);
517        }
518        for (arg, val) in &self.args {
519            let key = format!("usage_{}", arg.name.to_snake_case());
520            env.insert(key, val.to_string());
521        }
522        env
523    }
524}
525
526impl Display for ParseValue {
527    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
528        match self {
529            ParseValue::Bool(b) => write!(f, "{b}"),
530            ParseValue::String(s) => write!(f, "{s}"),
531            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
532            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
533        }
534    }
535}
536
537impl Debug for ParseOutput {
538    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
539        f.debug_struct("ParseOutput")
540            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
541            .field(
542                "args",
543                &self
544                    .args
545                    .iter()
546                    .map(|(a, w)| format!("{}: {w}", &a.name))
547                    .collect_vec(),
548            )
549            .field(
550                "available_flags",
551                &self
552                    .available_flags
553                    .iter()
554                    .map(|(f, w)| format!("{f}: {w}"))
555                    .collect_vec(),
556            )
557            .field(
558                "flags",
559                &self
560                    .flags
561                    .iter()
562                    .map(|(f, w)| format!("{}: {w}", &f.name))
563                    .collect_vec(),
564            )
565            .field("flag_awaiting_value", &self.flag_awaiting_value)
566            .field("errors", &self.errors)
567            .finish()
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn test_parse() {
577        let cmd = SpecCommand::builder()
578            .name("test")
579            .arg(SpecArg::builder().name("arg").build())
580            .flag(SpecFlag::builder().long("flag").build())
581            .build();
582        let spec = Spec {
583            name: "test".to_string(),
584            bin: "test".to_string(),
585            cmd,
586            ..Default::default()
587        };
588        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
589        let parsed = parse(&spec, &input).unwrap();
590        assert_eq!(parsed.cmds.len(), 1);
591        assert_eq!(parsed.cmds[0].name, "test");
592        assert_eq!(parsed.args.len(), 1);
593        assert_eq!(parsed.flags.len(), 1);
594        assert_eq!(parsed.available_flags.len(), 1);
595    }
596
597    #[test]
598    fn test_as_env() {
599        let cmd = SpecCommand::builder()
600            .name("test")
601            .arg(SpecArg::builder().name("arg").build())
602            .flag(SpecFlag::builder().long("flag").build())
603            .flag(
604                SpecFlag::builder()
605                    .long("force")
606                    .negate("--no-force")
607                    .build(),
608            )
609            .build();
610        let spec = Spec {
611            name: "test".to_string(),
612            bin: "test".to_string(),
613            cmd,
614            ..Default::default()
615        };
616        let input = vec![
617            "test".to_string(),
618            "--flag".to_string(),
619            "--no-force".to_string(),
620        ];
621        let parsed = parse(&spec, &input).unwrap();
622        let env = parsed.as_env();
623        assert_eq!(env.len(), 2);
624        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
625        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
626    }
627
628    #[test]
629    fn test_arg_env_var() {
630        let cmd = SpecCommand::builder()
631            .name("test")
632            .arg(
633                SpecArg::builder()
634                    .name("input")
635                    .env("TEST_ARG_INPUT")
636                    .required(true)
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
647        // Set env var
648        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
649
650        let input = vec!["test".to_string()];
651        let parsed = parse(&spec, &input).unwrap();
652
653        assert_eq!(parsed.args.len(), 1);
654        let arg = parsed.args.keys().next().unwrap();
655        assert_eq!(arg.name, "input");
656        let value = parsed.args.values().next().unwrap();
657        assert_eq!(value.to_string(), "test_file.txt");
658
659        // Clean up
660        std::env::remove_var("TEST_ARG_INPUT");
661    }
662
663    #[test]
664    fn test_flag_env_var_with_arg() {
665        let cmd = SpecCommand::builder()
666            .name("test")
667            .flag(
668                SpecFlag::builder()
669                    .long("output")
670                    .env("TEST_FLAG_OUTPUT")
671                    .arg(SpecArg::builder().name("file").build())
672                    .build(),
673            )
674            .build();
675        let spec = Spec {
676            name: "test".to_string(),
677            bin: "test".to_string(),
678            cmd,
679            ..Default::default()
680        };
681
682        // Set env var
683        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
684
685        let input = vec!["test".to_string()];
686        let parsed = parse(&spec, &input).unwrap();
687
688        assert_eq!(parsed.flags.len(), 1);
689        let flag = parsed.flags.keys().next().unwrap();
690        assert_eq!(flag.name, "output");
691        let value = parsed.flags.values().next().unwrap();
692        assert_eq!(value.to_string(), "output.txt");
693
694        // Clean up
695        std::env::remove_var("TEST_FLAG_OUTPUT");
696    }
697
698    #[test]
699    fn test_flag_env_var_boolean() {
700        let cmd = SpecCommand::builder()
701            .name("test")
702            .flag(
703                SpecFlag::builder()
704                    .long("verbose")
705                    .env("TEST_FLAG_VERBOSE")
706                    .build(),
707            )
708            .build();
709        let spec = Spec {
710            name: "test".to_string(),
711            bin: "test".to_string(),
712            cmd,
713            ..Default::default()
714        };
715
716        // Set env var to true
717        std::env::set_var("TEST_FLAG_VERBOSE", "true");
718
719        let input = vec!["test".to_string()];
720        let parsed = parse(&spec, &input).unwrap();
721
722        assert_eq!(parsed.flags.len(), 1);
723        let flag = parsed.flags.keys().next().unwrap();
724        assert_eq!(flag.name, "verbose");
725        let value = parsed.flags.values().next().unwrap();
726        assert_eq!(value.to_string(), "true");
727
728        // Clean up
729        std::env::remove_var("TEST_FLAG_VERBOSE");
730    }
731
732    #[test]
733    fn test_env_var_precedence() {
734        // CLI args should take precedence over env vars
735        let cmd = SpecCommand::builder()
736            .name("test")
737            .arg(
738                SpecArg::builder()
739                    .name("input")
740                    .env("TEST_PRECEDENCE_INPUT")
741                    .required(true)
742                    .build(),
743            )
744            .build();
745        let spec = Spec {
746            name: "test".to_string(),
747            bin: "test".to_string(),
748            cmd,
749            ..Default::default()
750        };
751
752        // Set env var
753        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
754
755        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
756        let parsed = parse(&spec, &input).unwrap();
757
758        assert_eq!(parsed.args.len(), 1);
759        let value = parsed.args.values().next().unwrap();
760        // CLI arg should take precedence
761        assert_eq!(value.to_string(), "cli_file.txt");
762
763        // Clean up
764        std::env::remove_var("TEST_PRECEDENCE_INPUT");
765    }
766
767    #[test]
768    fn test_flag_var_true_with_single_default() {
769        // When var=true and default="bar", the default should be MultiString(["bar"])
770        let cmd = SpecCommand::builder()
771            .name("test")
772            .flag(
773                SpecFlag::builder()
774                    .long("foo")
775                    .var(true)
776                    .arg(SpecArg::builder().name("foo").build())
777                    .default_value("bar")
778                    .build(),
779            )
780            .build();
781        let spec = Spec {
782            name: "test".to_string(),
783            bin: "test".to_string(),
784            cmd,
785            ..Default::default()
786        };
787
788        // User doesn't provide the flag
789        let input = vec!["test".to_string()];
790        let parsed = parse(&spec, &input).unwrap();
791
792        assert_eq!(parsed.flags.len(), 1);
793        let flag = parsed.flags.keys().next().unwrap();
794        assert_eq!(flag.name, "foo");
795        let value = parsed.flags.values().next().unwrap();
796        // Should be MultiString, not String
797        match value {
798            ParseValue::MultiString(v) => {
799                assert_eq!(v.len(), 1);
800                assert_eq!(v[0], "bar");
801            }
802            _ => panic!("Expected MultiString, got {:?}", value),
803        }
804    }
805
806    #[test]
807    fn test_flag_var_true_with_multiple_defaults() {
808        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
809        let cmd = SpecCommand::builder()
810            .name("test")
811            .flag(
812                SpecFlag::builder()
813                    .long("foo")
814                    .var(true)
815                    .arg(SpecArg::builder().name("foo").build())
816                    .default_values(["xyz", "bar"])
817                    .build(),
818            )
819            .build();
820        let spec = Spec {
821            name: "test".to_string(),
822            bin: "test".to_string(),
823            cmd,
824            ..Default::default()
825        };
826
827        // User doesn't provide the flag
828        let input = vec!["test".to_string()];
829        let parsed = parse(&spec, &input).unwrap();
830
831        assert_eq!(parsed.flags.len(), 1);
832        let value = parsed.flags.values().next().unwrap();
833        // Should be MultiString with both values
834        match value {
835            ParseValue::MultiString(v) => {
836                assert_eq!(v.len(), 2);
837                assert_eq!(v[0], "xyz");
838                assert_eq!(v[1], "bar");
839            }
840            _ => panic!("Expected MultiString, got {:?}", value),
841        }
842    }
843
844    #[test]
845    fn test_flag_var_false_with_default_remains_string() {
846        // When var=false (default), the default should still be String("bar")
847        let cmd = SpecCommand::builder()
848            .name("test")
849            .flag(
850                SpecFlag::builder()
851                    .long("foo")
852                    .var(false) // Default behavior
853                    .arg(SpecArg::builder().name("foo").build())
854                    .default_value("bar")
855                    .build(),
856            )
857            .build();
858        let spec = Spec {
859            name: "test".to_string(),
860            bin: "test".to_string(),
861            cmd,
862            ..Default::default()
863        };
864
865        // User doesn't provide the flag
866        let input = vec!["test".to_string()];
867        let parsed = parse(&spec, &input).unwrap();
868
869        assert_eq!(parsed.flags.len(), 1);
870        let value = parsed.flags.values().next().unwrap();
871        // Should be String, not MultiString
872        match value {
873            ParseValue::String(s) => {
874                assert_eq!(s, "bar");
875            }
876            _ => panic!("Expected String, got {:?}", value),
877        }
878    }
879
880    #[test]
881    fn test_arg_var_true_with_single_default() {
882        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
883        let cmd = SpecCommand::builder()
884            .name("test")
885            .arg(
886                SpecArg::builder()
887                    .name("files")
888                    .var(true)
889                    .default_value("default.txt")
890                    .required(false)
891                    .build(),
892            )
893            .build();
894        let spec = Spec {
895            name: "test".to_string(),
896            bin: "test".to_string(),
897            cmd,
898            ..Default::default()
899        };
900
901        // User doesn't provide the arg
902        let input = vec!["test".to_string()];
903        let parsed = parse(&spec, &input).unwrap();
904
905        assert_eq!(parsed.args.len(), 1);
906        let value = parsed.args.values().next().unwrap();
907        // Should be MultiString, not String
908        match value {
909            ParseValue::MultiString(v) => {
910                assert_eq!(v.len(), 1);
911                assert_eq!(v[0], "default.txt");
912            }
913            _ => panic!("Expected MultiString, got {:?}", value),
914        }
915    }
916
917    #[test]
918    fn test_arg_var_true_with_multiple_defaults() {
919        // When arg has var=true and multiple defaults
920        let cmd = SpecCommand::builder()
921            .name("test")
922            .arg(
923                SpecArg::builder()
924                    .name("files")
925                    .var(true)
926                    .default_values(["file1.txt", "file2.txt"])
927                    .required(false)
928                    .build(),
929            )
930            .build();
931        let spec = Spec {
932            name: "test".to_string(),
933            bin: "test".to_string(),
934            cmd,
935            ..Default::default()
936        };
937
938        // User doesn't provide the arg
939        let input = vec!["test".to_string()];
940        let parsed = parse(&spec, &input).unwrap();
941
942        assert_eq!(parsed.args.len(), 1);
943        let value = parsed.args.values().next().unwrap();
944        // Should be MultiString with both values
945        match value {
946            ParseValue::MultiString(v) => {
947                assert_eq!(v.len(), 2);
948                assert_eq!(v[0], "file1.txt");
949                assert_eq!(v[1], "file2.txt");
950            }
951            _ => panic!("Expected MultiString, got {:?}", value),
952        }
953    }
954
955    #[test]
956    fn test_arg_var_false_with_default_remains_string() {
957        // When arg has var=false (default), the default should still be String
958        let cmd = SpecCommand::builder()
959            .name("test")
960            .arg(
961                SpecArg::builder()
962                    .name("file")
963                    .var(false)
964                    .default_value("default.txt")
965                    .required(false)
966                    .build(),
967            )
968            .build();
969        let spec = Spec {
970            name: "test".to_string(),
971            bin: "test".to_string(),
972            cmd,
973            ..Default::default()
974        };
975
976        // User doesn't provide the arg
977        let input = vec!["test".to_string()];
978        let parsed = parse(&spec, &input).unwrap();
979
980        assert_eq!(parsed.args.len(), 1);
981        let value = parsed.args.values().next().unwrap();
982        // Should be String, not MultiString
983        match value {
984            ParseValue::String(s) => {
985                assert_eq!(s, "default.txt");
986            }
987            _ => panic!("Expected String, got {:?}", value),
988        }
989    }
990}