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    Ok(out)
430}
431
432#[cfg(feature = "docs")]
433fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
434    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
435}
436
437#[cfg(not(feature = "docs"))]
438fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
439    UsageErr::Help("help".to_string())
440}
441
442fn is_help_arg(spec: &Spec, w: &str) -> bool {
443    spec.disable_help != Some(true)
444        && (w == "--help"
445            || w == "-h"
446            || w == "-?"
447            || (spec.cmd.subcommands.is_empty() && w == "help"))
448}
449
450impl ParseOutput {
451    pub fn as_env(&self) -> BTreeMap<String, String> {
452        let mut env = BTreeMap::new();
453        for (flag, val) in &self.flags {
454            let key = format!("usage_{}", flag.name.to_snake_case());
455            let val = match val {
456                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
457                ParseValue::String(s) => s.clone(),
458                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
459                ParseValue::MultiString(s) => shell_words::join(s),
460            };
461            env.insert(key, val);
462        }
463        for (arg, val) in &self.args {
464            let key = format!("usage_{}", arg.name.to_snake_case());
465            env.insert(key, val.to_string());
466        }
467        env
468    }
469}
470
471impl Display for ParseValue {
472    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
473        match self {
474            ParseValue::Bool(b) => write!(f, "{b}"),
475            ParseValue::String(s) => write!(f, "{s}"),
476            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
477            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
478        }
479    }
480}
481
482impl Debug for ParseOutput {
483    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
484        f.debug_struct("ParseOutput")
485            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
486            .field(
487                "args",
488                &self
489                    .args
490                    .iter()
491                    .map(|(a, w)| format!("{}: {w}", &a.name))
492                    .collect_vec(),
493            )
494            .field(
495                "available_flags",
496                &self
497                    .available_flags
498                    .iter()
499                    .map(|(f, w)| format!("{f}: {w}"))
500                    .collect_vec(),
501            )
502            .field(
503                "flags",
504                &self
505                    .flags
506                    .iter()
507                    .map(|(f, w)| format!("{}: {w}", &f.name))
508                    .collect_vec(),
509            )
510            .field("flag_awaiting_value", &self.flag_awaiting_value)
511            .field("errors", &self.errors)
512            .finish()
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn test_parse() {
522        let mut cmd = SpecCommand::default();
523        cmd.name = "test".to_string();
524        cmd.args = vec![SpecArg {
525            name: "arg".to_string(),
526            ..Default::default()
527        }];
528        cmd.flags = vec![SpecFlag {
529            name: "flag".to_string(),
530            long: vec!["flag".to_string()],
531            ..Default::default()
532        }];
533        let spec = Spec {
534            name: "test".to_string(),
535            bin: "test".to_string(),
536            cmd,
537            ..Default::default()
538        };
539        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
540        let parsed = parse(&spec, &input).unwrap();
541        assert_eq!(parsed.cmds.len(), 1);
542        assert_eq!(parsed.cmds[0].name, "test");
543        assert_eq!(parsed.args.len(), 1);
544        assert_eq!(parsed.flags.len(), 1);
545        assert_eq!(parsed.available_flags.len(), 1);
546    }
547
548    #[test]
549    fn test_as_env() {
550        let mut cmd = SpecCommand::default();
551        cmd.name = "test".to_string();
552        cmd.args = vec![SpecArg {
553            name: "arg".to_string(),
554            ..Default::default()
555        }];
556        cmd.flags = vec![
557            SpecFlag {
558                name: "flag".to_string(),
559                long: vec!["flag".to_string()],
560                ..Default::default()
561            },
562            SpecFlag {
563                name: "force".to_string(),
564                long: vec!["force".to_string()],
565                negate: Some("--no-force".to_string()),
566                ..Default::default()
567            },
568        ];
569        let spec = Spec {
570            name: "test".to_string(),
571            bin: "test".to_string(),
572            cmd,
573            ..Default::default()
574        };
575        let input = vec![
576            "test".to_string(),
577            "--flag".to_string(),
578            "--no-force".to_string(),
579        ];
580        let parsed = parse(&spec, &input).unwrap();
581        let env = parsed.as_env();
582        assert_eq!(env.len(), 2);
583        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
584        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
585    }
586
587    #[test]
588    fn test_arg_env_var() {
589        let mut cmd = SpecCommand::default();
590        cmd.name = "test".to_string();
591        cmd.args = vec![SpecArg {
592            name: "input".to_string(),
593            env: Some("TEST_ARG_INPUT".to_string()),
594            required: true,
595            ..Default::default()
596        }];
597        let spec = Spec {
598            name: "test".to_string(),
599            bin: "test".to_string(),
600            cmd,
601            ..Default::default()
602        };
603
604        // Set env var
605        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
606
607        let input = vec!["test".to_string()];
608        let parsed = parse(&spec, &input).unwrap();
609
610        assert_eq!(parsed.args.len(), 1);
611        let arg = parsed.args.keys().next().unwrap();
612        assert_eq!(arg.name, "input");
613        let value = parsed.args.values().next().unwrap();
614        assert_eq!(value.to_string(), "test_file.txt");
615
616        // Clean up
617        std::env::remove_var("TEST_ARG_INPUT");
618    }
619
620    #[test]
621    fn test_flag_env_var_with_arg() {
622        let mut cmd = SpecCommand::default();
623        cmd.name = "test".to_string();
624        cmd.flags = vec![SpecFlag {
625            name: "output".to_string(),
626            long: vec!["output".to_string()],
627            env: Some("TEST_FLAG_OUTPUT".to_string()),
628            arg: Some(SpecArg {
629                name: "file".to_string(),
630                ..Default::default()
631            }),
632            ..Default::default()
633        }];
634        let spec = Spec {
635            name: "test".to_string(),
636            bin: "test".to_string(),
637            cmd,
638            ..Default::default()
639        };
640
641        // Set env var
642        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
643
644        let input = vec!["test".to_string()];
645        let parsed = parse(&spec, &input).unwrap();
646
647        assert_eq!(parsed.flags.len(), 1);
648        let flag = parsed.flags.keys().next().unwrap();
649        assert_eq!(flag.name, "output");
650        let value = parsed.flags.values().next().unwrap();
651        assert_eq!(value.to_string(), "output.txt");
652
653        // Clean up
654        std::env::remove_var("TEST_FLAG_OUTPUT");
655    }
656
657    #[test]
658    fn test_flag_env_var_boolean() {
659        let mut cmd = SpecCommand::default();
660        cmd.name = "test".to_string();
661        cmd.flags = vec![SpecFlag {
662            name: "verbose".to_string(),
663            long: vec!["verbose".to_string()],
664            env: Some("TEST_FLAG_VERBOSE".to_string()),
665            ..Default::default()
666        }];
667        let spec = Spec {
668            name: "test".to_string(),
669            bin: "test".to_string(),
670            cmd,
671            ..Default::default()
672        };
673
674        // Set env var to true
675        std::env::set_var("TEST_FLAG_VERBOSE", "true");
676
677        let input = vec!["test".to_string()];
678        let parsed = parse(&spec, &input).unwrap();
679
680        assert_eq!(parsed.flags.len(), 1);
681        let flag = parsed.flags.keys().next().unwrap();
682        assert_eq!(flag.name, "verbose");
683        let value = parsed.flags.values().next().unwrap();
684        assert_eq!(value.to_string(), "true");
685
686        // Clean up
687        std::env::remove_var("TEST_FLAG_VERBOSE");
688    }
689
690    #[test]
691    fn test_env_var_precedence() {
692        // CLI args should take precedence over env vars
693        let mut cmd = SpecCommand::default();
694        cmd.name = "test".to_string();
695        cmd.args = vec![SpecArg {
696            name: "input".to_string(),
697            env: Some("TEST_PRECEDENCE_INPUT".to_string()),
698            required: true,
699            ..Default::default()
700        }];
701        let spec = Spec {
702            name: "test".to_string(),
703            bin: "test".to_string(),
704            cmd,
705            ..Default::default()
706        };
707
708        // Set env var
709        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
710
711        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
712        let parsed = parse(&spec, &input).unwrap();
713
714        assert_eq!(parsed.args.len(), 1);
715        let value = parsed.args.values().next().unwrap();
716        // CLI arg should take precedence
717        assert_eq!(value.to_string(), "cli_file.txt");
718
719        // Clean up
720        std::env::remove_var("TEST_PRECEDENCE_INPUT");
721    }
722
723    #[test]
724    fn test_flag_var_true_with_single_default() {
725        // When var=true and default="bar", the default should be MultiString(["bar"])
726        let mut cmd = SpecCommand::default();
727        cmd.name = "test".to_string();
728        cmd.flags = vec![SpecFlag {
729            name: "foo".to_string(),
730            long: vec!["foo".to_string()],
731            var: true,
732            arg: Some(SpecArg {
733                name: "foo".to_string(),
734                ..Default::default()
735            }),
736            default: vec!["bar".to_string()],
737            ..Default::default()
738        }];
739        let spec = Spec {
740            name: "test".to_string(),
741            bin: "test".to_string(),
742            cmd,
743            ..Default::default()
744        };
745
746        // User doesn't provide the flag
747        let input = vec!["test".to_string()];
748        let parsed = parse(&spec, &input).unwrap();
749
750        assert_eq!(parsed.flags.len(), 1);
751        let flag = parsed.flags.keys().next().unwrap();
752        assert_eq!(flag.name, "foo");
753        let value = parsed.flags.values().next().unwrap();
754        // Should be MultiString, not String
755        match value {
756            ParseValue::MultiString(v) => {
757                assert_eq!(v.len(), 1);
758                assert_eq!(v[0], "bar");
759            }
760            _ => panic!("Expected MultiString, got {:?}", value),
761        }
762    }
763
764    #[test]
765    fn test_flag_var_true_with_multiple_defaults() {
766        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
767        let mut cmd = SpecCommand::default();
768        cmd.name = "test".to_string();
769        cmd.flags = vec![SpecFlag {
770            name: "foo".to_string(),
771            long: vec!["foo".to_string()],
772            var: true,
773            arg: Some(SpecArg {
774                name: "foo".to_string(),
775                ..Default::default()
776            }),
777            default: vec!["xyz".to_string(), "bar".to_string()],
778            ..Default::default()
779        }];
780        let spec = Spec {
781            name: "test".to_string(),
782            bin: "test".to_string(),
783            cmd,
784            ..Default::default()
785        };
786
787        // User doesn't provide the flag
788        let input = vec!["test".to_string()];
789        let parsed = parse(&spec, &input).unwrap();
790
791        assert_eq!(parsed.flags.len(), 1);
792        let value = parsed.flags.values().next().unwrap();
793        // Should be MultiString with both values
794        match value {
795            ParseValue::MultiString(v) => {
796                assert_eq!(v.len(), 2);
797                assert_eq!(v[0], "xyz");
798                assert_eq!(v[1], "bar");
799            }
800            _ => panic!("Expected MultiString, got {:?}", value),
801        }
802    }
803
804    #[test]
805    fn test_flag_var_false_with_default_remains_string() {
806        // When var=false (default), the default should still be String("bar")
807        let mut cmd = SpecCommand::default();
808        cmd.name = "test".to_string();
809        cmd.flags = vec![SpecFlag {
810            name: "foo".to_string(),
811            long: vec!["foo".to_string()],
812            var: false, // Default behavior
813            arg: Some(SpecArg {
814                name: "foo".to_string(),
815                ..Default::default()
816            }),
817            default: vec!["bar".to_string()],
818            ..Default::default()
819        }];
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 String, not MultiString
834        match value {
835            ParseValue::String(s) => {
836                assert_eq!(s, "bar");
837            }
838            _ => panic!("Expected String, got {:?}", value),
839        }
840    }
841
842    #[test]
843    fn test_arg_var_true_with_single_default() {
844        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
845        let mut cmd = SpecCommand::default();
846        cmd.name = "test".to_string();
847        cmd.args = vec![SpecArg {
848            name: "files".to_string(),
849            var: true,
850            default: vec!["default.txt".to_string()],
851            required: false,
852            ..Default::default()
853        }];
854        let spec = Spec {
855            name: "test".to_string(),
856            bin: "test".to_string(),
857            cmd,
858            ..Default::default()
859        };
860
861        // User doesn't provide the arg
862        let input = vec!["test".to_string()];
863        let parsed = parse(&spec, &input).unwrap();
864
865        assert_eq!(parsed.args.len(), 1);
866        let value = parsed.args.values().next().unwrap();
867        // Should be MultiString, not String
868        match value {
869            ParseValue::MultiString(v) => {
870                assert_eq!(v.len(), 1);
871                assert_eq!(v[0], "default.txt");
872            }
873            _ => panic!("Expected MultiString, got {:?}", value),
874        }
875    }
876
877    #[test]
878    fn test_arg_var_true_with_multiple_defaults() {
879        // When arg has var=true and multiple defaults
880        let mut cmd = SpecCommand::default();
881        cmd.name = "test".to_string();
882        cmd.args = vec![SpecArg {
883            name: "files".to_string(),
884            var: true,
885            default: vec!["file1.txt".to_string(), "file2.txt".to_string()],
886            required: false,
887            ..Default::default()
888        }];
889        let spec = Spec {
890            name: "test".to_string(),
891            bin: "test".to_string(),
892            cmd,
893            ..Default::default()
894        };
895
896        // User doesn't provide the arg
897        let input = vec!["test".to_string()];
898        let parsed = parse(&spec, &input).unwrap();
899
900        assert_eq!(parsed.args.len(), 1);
901        let value = parsed.args.values().next().unwrap();
902        // Should be MultiString with both values
903        match value {
904            ParseValue::MultiString(v) => {
905                assert_eq!(v.len(), 2);
906                assert_eq!(v[0], "file1.txt");
907                assert_eq!(v[1], "file2.txt");
908            }
909            _ => panic!("Expected MultiString, got {:?}", value),
910        }
911    }
912
913    #[test]
914    fn test_arg_var_false_with_default_remains_string() {
915        // When arg has var=false (default), the default should still be String
916        let mut cmd = SpecCommand::default();
917        cmd.name = "test".to_string();
918        cmd.args = vec![SpecArg {
919            name: "file".to_string(),
920            var: false,
921            default: vec!["default.txt".to_string()],
922            required: false,
923            ..Default::default()
924        }];
925        let spec = Spec {
926            name: "test".to_string(),
927            bin: "test".to_string(),
928            cmd,
929            ..Default::default()
930        };
931
932        // User doesn't provide the arg
933        let input = vec!["test".to_string()];
934        let parsed = parse(&spec, &input).unwrap();
935
936        assert_eq!(parsed.args.len(), 1);
937        let value = parsed.args.values().next().unwrap();
938        // Should be String, not MultiString
939        match value {
940            ParseValue::String(s) => {
941                assert_eq!(s, "default.txt");
942            }
943            _ => panic!("Expected String, got {:?}", value),
944        }
945    }
946}