Skip to main content

usage/
parse.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use std::sync::Arc;
9use strum::EnumTryAs;
10
11#[cfg(feature = "docs")]
12use crate::docs;
13use crate::error::UsageErr;
14use crate::spec::arg::SpecDoubleDashChoices;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27fn merge_subcommand_flags(
28    available: &mut BTreeMap<String, Arc<SpecFlag>>,
29    new_flags: BTreeMap<String, Arc<SpecFlag>>,
30) {
31    // Keep only inherited global flags from the parent.
32    available.retain(|_, f| f.global);
33
34    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
35    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
36    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
37
38    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
39    // map's existing intra-subcommand collision resolution: when two flags in the same command
40    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
41    // to its last-declared owner, and we must not change which flag owns it.
42    for (key, flag) in new_flags {
43        if flag.global {
44            // A child that re-declares (or adds) a global flag stays recognized everywhere.
45            available.insert(key, flag);
46            continue;
47        }
48
49        // A non-global re-declaration that shares a LONG name with an inherited global flag is
50        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
51        // global). Keep the global flag (global precedence, so it survives the next descent's
52        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
53        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
54        // deliberate: a re-declaration sharing only a short letter with an unrelated global
55        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
56        // handled by the `contains_key` skip below instead.
57        let inherited_global = flag.long.iter().find_map(|l| {
58            available
59                .get(&format!("--{l}"))
60                .filter(|f| f.global)
61                .cloned()
62        });
63        if let Some(global_flag) = inherited_global {
64            // Never clobber a *different* inherited global's alias. If this re-declaration's
65            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
66            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
67            // precedence dictates, instead of stealing the alias for the merged flag.
68            if available
69                .get(&key)
70                .is_some_and(|existing| existing.global && !Arc::ptr_eq(existing, &global_flag))
71            {
72                continue;
73            }
74            let merged = merged_cache
75                .entry(Arc::as_ptr(&flag) as usize)
76                .or_insert_with(|| {
77                    let mut merged = (*global_flag).clone();
78                    for s in &flag.short {
79                        if !merged.short.contains(s) {
80                            merged.short.push(*s);
81                        }
82                    }
83                    for l in &flag.long {
84                        if !merged.long.contains(l) {
85                            merged.long.push(l.clone());
86                        }
87                    }
88                    Arc::new(merged)
89                })
90                .clone();
91            available.insert(key, merged);
92            continue;
93        }
94
95        // Purely-local flag (shares nothing with an inherited global), or one that collides only
96        // on a short with an unrelated global. Insert this alias but never shadow an inherited
97        // global flag. Such non-global flags are dropped by the next descent's `retain`.
98        if available.contains_key(&key) {
99            continue;
100        }
101        available.insert(key, flag);
102    }
103}
104
105/// Build the lookup keys a flag is registered under in `available_flags`:
106/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
107fn flag_keys(flag: &SpecFlag) -> Vec<String> {
108    let mut keys: Vec<String> = flag
109        .long
110        .iter()
111        .map(|l| format!("--{l}"))
112        .chain(flag.short.iter().map(|s| format!("-{s}")))
113        .collect();
114    if let Some(negate) = &flag.negate {
115        keys.push(negate.clone());
116    }
117    keys
118}
119
120/// Extract the flag key from a flag word for lookup in available_flags map
121/// Handles both long flags (--flag, --flag=value) and short flags (-f)
122fn get_flag_key(word: &str) -> &str {
123    if word.starts_with("--") {
124        // Long flag: strip =value if present
125        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
126    } else if word.len() >= 2 {
127        // Short flag: first two chars (-X)
128        &word[0..2]
129    } else {
130        word
131    }
132}
133
134pub struct ParseOutput {
135    pub cmd: SpecCommand,
136    pub cmds: Vec<SpecCommand>,
137    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
138    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
139    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
140    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
141    pub errors: Vec<UsageErr>,
142}
143
144#[derive(Debug, EnumTryAs, Clone)]
145pub enum ParseValue {
146    Bool(bool),
147    String(String),
148    MultiBool(Vec<bool>),
149    MultiString(Vec<String>),
150}
151
152/// Builder for parsing command-line arguments with custom options.
153///
154/// Use this when you need to customize parsing behavior, such as providing
155/// a custom environment variable map instead of using the process environment.
156///
157/// # Example
158/// ```
159/// use std::collections::HashMap;
160/// use usage::Spec;
161/// use usage::parse::Parser;
162///
163/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
164/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
165///
166/// let result = Parser::new(&spec)
167///     .with_env(env)
168///     .parse(&["cmd".into()])
169///     .unwrap();
170/// ```
171#[non_exhaustive]
172pub struct Parser<'a> {
173    spec: &'a Spec,
174    env: Option<HashMap<String, String>>,
175}
176
177impl<'a> Parser<'a> {
178    /// Create a new parser for the given spec.
179    pub fn new(spec: &'a Spec) -> Self {
180        Self { spec, env: None }
181    }
182
183    /// Use a custom environment variable map instead of the process environment.
184    ///
185    /// This is useful when parsing for tasks in a monorepo where the env vars
186    /// come from a child config file rather than the current process environment.
187    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
188        self.env = Some(env);
189        self
190    }
191
192    /// Parse the input arguments.
193    ///
194    /// Returns the parsed arguments and flags, with defaults and env vars applied.
195    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
196        let custom_env = self.env.as_ref();
197        let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
198        trace!("{out:?}");
199
200        let get_env = |key: &str| -> Option<String> {
201            if let Some(env_map) = custom_env {
202                env_map.get(key).cloned()
203            } else {
204                std::env::var(key).ok()
205            }
206        };
207
208        // Apply env vars and defaults for args
209        for arg in out.cmd.args.iter().skip(out.args.len()) {
210            if let Some(env_var) = arg.env.as_ref() {
211                if let Some(env_value) = get_env(env_var) {
212                    validate_choice_value(
213                        ChoiceTarget::arg(arg),
214                        &env_value,
215                        arg.choices.as_ref(),
216                        custom_env,
217                    )?;
218                    out.args
219                        .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
220                    continue;
221                }
222            }
223            if !arg.default.is_empty() {
224                // Consider var when deciding the type of default return value
225                if arg.var {
226                    validate_choice_values(
227                        ChoiceTarget::arg(arg),
228                        &arg.default,
229                        arg.choices.as_ref(),
230                        custom_env,
231                    )?;
232                    // For var=true, always return a vec (MultiString)
233                    out.args.insert(
234                        Arc::new(arg.clone()),
235                        ParseValue::MultiString(arg.default.clone()),
236                    );
237                } else {
238                    validate_choice_value(
239                        ChoiceTarget::arg(arg),
240                        &arg.default[0],
241                        arg.choices.as_ref(),
242                        custom_env,
243                    )?;
244                    // For var=false, return the first default value as String
245                    out.args.insert(
246                        Arc::new(arg.clone()),
247                        ParseValue::String(arg.default[0].clone()),
248                    );
249                }
250            }
251        }
252
253        // Apply env vars and defaults for flags
254        for flag in out.available_flags.values() {
255            if out.flags.contains_key(flag) {
256                continue;
257            }
258            if let Some(env_var) = flag.env.as_ref() {
259                if let Some(env_value) = get_env(env_var) {
260                    if let Some(arg) = flag.arg.as_ref() {
261                        validate_choice_value(
262                            ChoiceTarget::option(flag),
263                            &env_value,
264                            arg.choices.as_ref(),
265                            custom_env,
266                        )?;
267                        out.flags
268                            .insert(Arc::clone(flag), ParseValue::String(env_value));
269                    } else {
270                        // For boolean flags, check if env value is truthy
271                        let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
272                        out.flags
273                            .insert(Arc::clone(flag), ParseValue::Bool(is_true));
274                    }
275                    continue;
276                }
277            }
278            // Apply flag default
279            if !flag.default.is_empty() {
280                // Consider var when deciding the type of default return value
281                if flag.var {
282                    // For var=true, always return a vec (MultiString for flags with args, MultiBool for boolean flags)
283                    if let Some(arg) = flag.arg.as_ref() {
284                        validate_choice_values(
285                            ChoiceTarget::option(flag),
286                            &flag.default,
287                            arg.choices.as_ref(),
288                            custom_env,
289                        )?;
290                        out.flags.insert(
291                            Arc::clone(flag),
292                            ParseValue::MultiString(flag.default.clone()),
293                        );
294                    } else {
295                        // For boolean flags with var=true, convert default strings to bools
296                        let bools: Vec<bool> = flag
297                            .default
298                            .iter()
299                            .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
300                            .collect();
301                        out.flags
302                            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
303                    }
304                } else {
305                    // For var=false, return the first default value
306                    if let Some(arg) = flag.arg.as_ref() {
307                        validate_choice_value(
308                            ChoiceTarget::option(flag),
309                            &flag.default[0],
310                            arg.choices.as_ref(),
311                            custom_env,
312                        )?;
313                        out.flags.insert(
314                            Arc::clone(flag),
315                            ParseValue::String(flag.default[0].clone()),
316                        );
317                    } else {
318                        // For boolean flags, convert default string to bool
319                        let is_true =
320                            matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
321                        out.flags
322                            .insert(Arc::clone(flag), ParseValue::Bool(is_true));
323                    }
324                }
325            }
326            // Also check nested arg defaults (for flags like --foo <arg> where the arg has a default)
327            if let Some(arg) = flag.arg.as_ref() {
328                if !out.flags.contains_key(flag) && !arg.default.is_empty() {
329                    if flag.var {
330                        validate_choice_values(
331                            ChoiceTarget::option(flag),
332                            &arg.default,
333                            arg.choices.as_ref(),
334                            custom_env,
335                        )?;
336                        out.flags.insert(
337                            Arc::clone(flag),
338                            ParseValue::MultiString(arg.default.clone()),
339                        );
340                    } else {
341                        validate_choice_value(
342                            ChoiceTarget::option(flag),
343                            &arg.default[0],
344                            arg.choices.as_ref(),
345                            custom_env,
346                        )?;
347                        out.flags
348                            .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
349                    }
350                }
351            }
352        }
353        if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
354            bail!("{err}");
355        }
356        if !out.errors.is_empty() {
357            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
358        }
359        Ok(out)
360    }
361}
362
363/// Parse command-line arguments according to a spec.
364///
365/// Returns the parsed arguments and flags, with defaults and env vars applied.
366/// Uses `std::env::var` for environment variable lookups.
367///
368/// For custom environment variable handling, use [`Parser`] instead.
369#[must_use = "parsing result should be used"]
370pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
371    Parser::new(spec).parse(input)
372}
373
374/// Parse command-line arguments without applying defaults.
375///
376/// Use this for help text generation or when you need the raw parsed values.
377#[must_use = "parsing result should be used"]
378pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
379    parse_partial_with_env(spec, input, None)
380}
381
382/// Internal version of parse_partial that accepts an optional custom env map.
383fn parse_partial_with_env(
384    spec: &Spec,
385    input: &[String],
386    custom_env: Option<&HashMap<String, String>>,
387) -> Result<ParseOutput, miette::Error> {
388    trace!("parse_partial: {input:?}");
389    let mut input = input.iter().cloned().collect::<VecDeque<_>>();
390    input.pop_front();
391
392    let gather_flags = |cmd: &SpecCommand| {
393        cmd.flags
394            .iter()
395            .flat_map(|f| {
396                let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
397                flag_keys(&f)
398                    .into_iter()
399                    .map(|key| (key, Arc::clone(&f)))
400                    .collect::<Vec<_>>()
401            })
402            .collect()
403    };
404
405    let mut out = ParseOutput {
406        cmd: spec.cmd.clone(),
407        cmds: vec![spec.cmd.clone()],
408        args: IndexMap::new(),
409        flags: IndexMap::new(),
410        available_flags: gather_flags(&spec.cmd),
411        flag_awaiting_value: vec![],
412        errors: vec![],
413    };
414
415    // Phase 1: Scan for subcommands and collect global flags
416    //
417    // This phase identifies subcommands early because they may have mount points
418    // that need to be executed with the global flags that appeared before them.
419    //
420    // Example: "usage --verbose run task"
421    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
422    //   -> then finds "task" as a subcommand of "run" (if it exists)
423    //
424    // We only collect global flags because:
425    // - Non-global flags are specific to the current command, not subcommands
426    // - Global flags affect all commands and should be passed to mount points
427    let mut prefix_words: Vec<String> = vec![];
428    let mut idx = 0;
429    // Track whether we've already applied the default_subcommand to prevent
430    // multiple switches (e.g., if default is "run" and there's a task named "run")
431    let mut used_default_subcommand = false;
432
433    while idx < input.len() {
434        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
435            let mut subcommand = subcommand.clone();
436            // Pass prefix words (global flags before this subcommand) to mount
437            subcommand.mount(&prefix_words)?;
438            merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
439            // Remove subcommand from input
440            input.remove(idx);
441            out.cmds.push(subcommand.clone());
442            out.cmd = subcommand.clone();
443            prefix_words.clear();
444            // Continue from current position (don't reset to 0)
445            // After remove(), idx now points to the next element
446        } else if input[idx].starts_with('-') {
447            // Check if this is a known flag and if it's global
448            let word = &input[idx];
449            let flag_key = get_flag_key(word);
450
451            if let Some(f) = out.available_flags.get(flag_key) {
452                // Only collect global flags for mount execution.
453                //
454                // We deliberately leave the global flag (and its value) in `input` so that
455                // Phase 2 re-parses it and records it in `out.flags`. That is how global flags
456                // reach `as_env()` for normal execution and for the env passed to mount scripts.
457                // Re-parsing is harmless as long as the flag stays recognized in
458                // `available_flags` (see `merge_subcommand_flags`): Phase 2 consumes it as a
459                // flag instead of mistaking it for a positional argument.
460                if f.global {
461                    prefix_words.push(input[idx].clone());
462                    idx += 1;
463
464                    // Only consume next word if flag takes an argument AND value isn't embedded
465                    // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
466                    if f.arg.is_some()
467                        && !word.contains('=')
468                        && idx < input.len()
469                        && !input[idx].starts_with('-')
470                    {
471                        prefix_words.push(input[idx].clone());
472                        idx += 1;
473                    }
474                } else {
475                    // Non-global flag encountered - stop subcommand search
476                    // This prevents incorrect parsing like: "cmd --local-flag run"
477                    // where "run" might be mistaken for a subcommand
478                    break;
479                }
480            } else {
481                // Unknown flag - stop looking for subcommands
482                // Let the main parsing phase handle the error
483                break;
484            }
485        } else {
486            // Found a word that's not a flag or subcommand
487            // Check if we should use the default_subcommand (only once)
488            if !used_default_subcommand {
489                if let Some(default_name) = &spec.default_subcommand {
490                    if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
491                        let mut subcommand = subcommand.clone();
492                        // Pass prefix words (global flags before this) to mount
493                        subcommand.mount(&prefix_words)?;
494                        merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
495                        out.cmds.push(subcommand.clone());
496                        out.cmd = subcommand.clone();
497                        prefix_words.clear();
498                        used_default_subcommand = true;
499                        // Continue the loop to check if this word is a subcommand of the
500                        // default subcommand (e.g., a task name added via mount).
501                        // If it's not a subcommand, the next iteration will break and
502                        // Phase 2 will handle it as a positional arg.
503                        continue;
504                    }
505                }
506            }
507            // This could be a positional argument, so stop subcommand search
508            break;
509        }
510    }
511
512    // Phase 2: Main argument and flag parsing
513    //
514    // Now that we've identified all subcommands and executed their mounts,
515    // we can parse the remaining arguments, flags, and their values.
516    let mut next_arg = out.cmd.args.first();
517    let mut enable_flags = true;
518    let mut grouped_flag = false;
519
520    while !input.is_empty() {
521        let mut w = input.pop_front().unwrap();
522
523        // Check for restart_token - resets argument parsing for multiple command invocations
524        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
525        if let Some(ref restart_token) = out.cmd.restart_token {
526            if w == *restart_token {
527                // Reset argument parsing state for a fresh command invocation
528                out.args.clear();
529                next_arg = out.cmd.args.first();
530                out.flag_awaiting_value.clear(); // Clear any pending flag values
531                enable_flags = true; // Reset -- separator effect
532                                     // Keep flags and continue parsing
533                continue;
534            }
535        }
536
537        if w == "--" {
538            // Always disable flag parsing after seeing a "--" token
539            enable_flags = false;
540
541            // Only preserve the double dash token if we're collecting values for a variadic arg
542            // in double_dash == `preserve` mode
543            let should_preserve = next_arg
544                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
545                .unwrap_or(false);
546
547            if should_preserve {
548                // Fall through to arg parsing
549            } else {
550                // Default behavior, skip the token
551                continue;
552            }
553        }
554
555        // long flags
556        if enable_flags && w.starts_with("--") {
557            grouped_flag = false;
558            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
559            if !val.is_empty() {
560                input.push_front(val.to_string());
561            }
562            if let Some(f) = out.available_flags.get(word) {
563                if f.arg.is_some() {
564                    out.flag_awaiting_value.push(Arc::clone(f));
565                } else if f.count {
566                    let arr = out
567                        .flags
568                        .entry(Arc::clone(f))
569                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
570                        .try_as_multi_bool_mut()
571                        .unwrap();
572                    arr.push(true);
573                } else {
574                    let negate = f.negate.clone().unwrap_or_default();
575                    out.flags
576                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
577                }
578                continue;
579            }
580            if is_help_arg(spec, &w) {
581                out.errors
582                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
583                return Ok(out);
584            }
585        }
586
587        // short flags
588        if enable_flags && w.starts_with('-') && w.len() > 1 {
589            let short = w.chars().nth(1).unwrap();
590            if let Some(f) = out.available_flags.get(&format!("-{short}")) {
591                if w.len() > 2 {
592                    input.push_front(format!("-{}", &w[2..]));
593                    grouped_flag = true;
594                }
595                if f.arg.is_some() {
596                    out.flag_awaiting_value.push(Arc::clone(f));
597                } else if f.count {
598                    let arr = out
599                        .flags
600                        .entry(Arc::clone(f))
601                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
602                        .try_as_multi_bool_mut()
603                        .unwrap();
604                    arr.push(true);
605                } else {
606                    let negate = f.negate.clone().unwrap_or_default();
607                    out.flags
608                        .insert(Arc::clone(f), ParseValue::Bool(w != negate));
609                }
610                continue;
611            }
612            if is_help_arg(spec, &w) {
613                out.errors
614                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
615                return Ok(out);
616            }
617            if grouped_flag {
618                grouped_flag = false;
619                w.remove(0);
620            }
621        }
622
623        if !out.flag_awaiting_value.is_empty() {
624            while let Some(flag) = out.flag_awaiting_value.pop() {
625                let arg = flag.arg.as_ref().unwrap();
626                if validate_choices(
627                    spec,
628                    &out.cmd,
629                    &mut out.errors,
630                    ChoiceTarget::option(&flag),
631                    &w,
632                    arg.choices.as_ref(),
633                    custom_env,
634                )? {
635                    return Ok(out);
636                }
637                if flag.var {
638                    let arr = out
639                        .flags
640                        .entry(flag)
641                        .or_insert_with(|| ParseValue::MultiString(vec![]))
642                        .try_as_multi_string_mut()
643                        .unwrap();
644                    arr.push(w);
645                } else {
646                    out.flags.insert(flag, ParseValue::String(w));
647                }
648                w = "".to_string();
649            }
650            continue;
651        }
652
653        if let Some(arg) = next_arg {
654            if validate_choices(
655                spec,
656                &out.cmd,
657                &mut out.errors,
658                ChoiceTarget::arg(arg),
659                &w,
660                arg.choices.as_ref(),
661                custom_env,
662            )? {
663                return Ok(out);
664            }
665            if arg.var {
666                let arr = out
667                    .args
668                    .entry(Arc::new(arg.clone()))
669                    .or_insert_with(|| ParseValue::MultiString(vec![]))
670                    .try_as_multi_string_mut()
671                    .unwrap();
672                arr.push(w);
673                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
674                    next_arg = out.cmd.args.get(out.args.len());
675                }
676            } else {
677                out.args
678                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
679                next_arg = out.cmd.args.get(out.args.len());
680            }
681            continue;
682        }
683        if is_help_arg(spec, &w) {
684            out.errors
685                .push(render_help_err(spec, &out.cmd, w.len() > 2));
686            return Ok(out);
687        }
688        bail!("unexpected word: {w}");
689    }
690
691    for arg in out.cmd.args.iter().skip(out.args.len()) {
692        if arg.required && arg.default.is_empty() {
693            // Check if there's an env var available (custom env map takes precedence)
694            let has_env = arg.env.as_ref().is_some_and(|e| {
695                custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
696                    || std::env::var(e).is_ok()
697            });
698            if !has_env {
699                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
700            }
701        }
702    }
703
704    for flag in out.available_flags.values() {
705        if out.flags.contains_key(flag) {
706            continue;
707        }
708        let has_default =
709            !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
710        // Check if there's an env var available (custom env map takes precedence)
711        let has_env = flag.env.as_ref().is_some_and(|e| {
712            custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
713        });
714        if flag.required && !has_default && !has_env {
715            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
716        }
717    }
718
719    // Validate var_min/var_max constraints for variadic args
720    for (arg, value) in &out.args {
721        if arg.var {
722            if let ParseValue::MultiString(values) = value {
723                if let Some(min) = arg.var_min {
724                    if values.len() < min {
725                        out.errors.push(UsageErr::VarArgTooFew {
726                            name: arg.name.clone(),
727                            min,
728                            got: values.len(),
729                        });
730                    }
731                }
732                if let Some(max) = arg.var_max {
733                    if values.len() > max {
734                        out.errors.push(UsageErr::VarArgTooMany {
735                            name: arg.name.clone(),
736                            max,
737                            got: values.len(),
738                        });
739                    }
740                }
741            }
742        }
743    }
744
745    // Validate var_min/var_max constraints for variadic flags
746    for (flag, value) in &out.flags {
747        if flag.var {
748            let count = match value {
749                ParseValue::MultiString(values) => values.len(),
750                ParseValue::MultiBool(values) => values.len(),
751                _ => continue,
752            };
753            if let Some(min) = flag.var_min {
754                if count < min {
755                    out.errors.push(UsageErr::VarFlagTooFew {
756                        name: flag.name.clone(),
757                        min,
758                        got: count,
759                    });
760                }
761            }
762            if let Some(max) = flag.var_max {
763                if count > max {
764                    out.errors.push(UsageErr::VarFlagTooMany {
765                        name: flag.name.clone(),
766                        max,
767                        got: count,
768                    });
769                }
770            }
771        }
772    }
773
774    Ok(out)
775}
776
777#[cfg(feature = "docs")]
778fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
779    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
780}
781
782#[cfg(not(feature = "docs"))]
783fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
784    UsageErr::Help("help".to_string())
785}
786
787#[derive(Copy, Clone)]
788struct ChoiceTarget<'a> {
789    kind: &'a str,
790    name: &'a str,
791}
792
793impl<'a> ChoiceTarget<'a> {
794    fn arg(arg: &'a SpecArg) -> Self {
795        Self {
796            kind: "arg",
797            name: &arg.name,
798        }
799    }
800
801    fn option(flag: &'a SpecFlag) -> Self {
802        Self {
803            kind: "option",
804            name: &flag.name,
805        }
806    }
807}
808
809fn choice_error(
810    target: ChoiceTarget<'_>,
811    value: &str,
812    choices: Option<&SpecChoices>,
813    custom_env: Option<&HashMap<String, String>>,
814) -> Option<String> {
815    let choices = choices?;
816    let values = choices.values_with_env(custom_env);
817    if values.iter().any(|choice| choice == value) {
818        return None;
819    }
820    if let Some(env) = choices.env() {
821        if values.is_empty() {
822            return Some(format!(
823                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
824                target.kind, target.name,
825            ));
826        }
827    }
828    Some(format!(
829        "Invalid choice for {} {}: {value}, expected one of {}",
830        target.kind,
831        target.name,
832        values.join(", ")
833    ))
834}
835
836fn validate_choices(
837    spec: &Spec,
838    cmd: &SpecCommand,
839    errors: &mut Vec<UsageErr>,
840    target: ChoiceTarget<'_>,
841    value: &str,
842    choices: Option<&SpecChoices>,
843    custom_env: Option<&HashMap<String, String>>,
844) -> miette::Result<bool> {
845    if is_help_arg(spec, value)
846        && choices.is_some_and(|choices| {
847            !choices
848                .values_with_env(custom_env)
849                .iter()
850                .any(|choice| choice == value)
851        })
852    {
853        errors.push(render_help_err(spec, cmd, value.len() > 2));
854        return Ok(true);
855    }
856
857    if let Some(err) = choice_error(target, value, choices, custom_env) {
858        bail!("{err}");
859    }
860    Ok(false)
861}
862
863fn validate_choice_value(
864    target: ChoiceTarget<'_>,
865    value: &str,
866    choices: Option<&SpecChoices>,
867    custom_env: Option<&HashMap<String, String>>,
868) -> miette::Result<()> {
869    if let Some(err) = choice_error(target, value, choices, custom_env) {
870        bail!("{err}");
871    }
872    Ok(())
873}
874
875fn validate_choice_values(
876    target: ChoiceTarget<'_>,
877    values: &[String],
878    choices: Option<&SpecChoices>,
879    custom_env: Option<&HashMap<String, String>>,
880) -> miette::Result<()> {
881    for value in values {
882        validate_choice_value(target, value, choices, custom_env)?;
883    }
884    Ok(())
885}
886
887fn is_help_arg(spec: &Spec, w: &str) -> bool {
888    spec.disable_help != Some(true)
889        && (w == "--help"
890            || w == "-h"
891            || w == "-?"
892            || (spec.cmd.subcommands.is_empty() && w == "help"))
893}
894
895impl ParseOutput {
896    pub fn as_env(&self) -> BTreeMap<String, String> {
897        let mut env = BTreeMap::new();
898        for (flag, val) in &self.flags {
899            let key = format!("usage_{}", flag.name.to_snake_case());
900            let val = match val {
901                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
902                ParseValue::String(s) => s.clone(),
903                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
904                ParseValue::MultiString(s) => shell_words::join(s),
905            };
906            env.insert(key, val);
907        }
908        for (arg, val) in &self.args {
909            let key = format!("usage_{}", arg.name.to_snake_case());
910            env.insert(key, val.to_string());
911        }
912        env
913    }
914}
915
916impl Display for ParseValue {
917    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
918        match self {
919            ParseValue::Bool(b) => write!(f, "{b}"),
920            ParseValue::String(s) => write!(f, "{s}"),
921            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
922            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
923        }
924    }
925}
926
927impl Debug for ParseOutput {
928    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
929        f.debug_struct("ParseOutput")
930            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
931            .field(
932                "args",
933                &self
934                    .args
935                    .iter()
936                    .map(|(a, w)| format!("{}: {w}", &a.name))
937                    .collect_vec(),
938            )
939            .field(
940                "available_flags",
941                &self
942                    .available_flags
943                    .iter()
944                    .map(|(f, w)| format!("{f}: {w}"))
945                    .collect_vec(),
946            )
947            .field(
948                "flags",
949                &self
950                    .flags
951                    .iter()
952                    .map(|(f, w)| format!("{}: {w}", &f.name))
953                    .collect_vec(),
954            )
955            .field("flag_awaiting_value", &self.flag_awaiting_value)
956            .field("errors", &self.errors)
957            .finish()
958    }
959}
960
961#[cfg(test)]
962mod tests {
963    use super::*;
964
965    fn input(words: &[&str]) -> Vec<String> {
966        words.iter().map(|word| (*word).to_string()).collect()
967    }
968
969    fn spec_with_arg(arg: SpecArg) -> Spec {
970        let cmd = SpecCommand::builder().name("test").arg(arg).build();
971        Spec {
972            name: "test".to_string(),
973            bin: "test".to_string(),
974            cmd,
975            ..Default::default()
976        }
977    }
978
979    fn spec_with_flag(flag: SpecFlag) -> Spec {
980        let cmd = SpecCommand::builder().name("test").flag(flag).build();
981        Spec {
982            name: "test".to_string(),
983            bin: "test".to_string(),
984            cmd,
985            ..Default::default()
986        }
987    }
988
989    fn parse_with_env(
990        spec: &Spec,
991        words: &[&str],
992        env: &[(&str, &str)],
993    ) -> Result<ParseOutput, miette::Error> {
994        let env = env
995            .iter()
996            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
997            .collect();
998        Parser::new(spec).with_env(env).parse(&input(words))
999    }
1000
1001    fn first_string_value(parsed: &ParseOutput) -> &str {
1002        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1003            return value;
1004        }
1005        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1006            return value;
1007        }
1008        panic!("expected first parsed value to be ParseValue::String");
1009    }
1010
1011    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1012        let err = result.expect_err("expected parser error");
1013        assert_eq!(format!("{err}"), expected);
1014    }
1015
1016    #[cfg(feature = "unstable_choices_env")]
1017    fn spec_arg_choices_env(key: &str) -> Spec {
1018        spec_with_arg(
1019            SpecArg::builder()
1020                .name("env")
1021                .choices_env(key)
1022                .required(false)
1023                .build(),
1024        )
1025    }
1026
1027    #[cfg(feature = "unstable_choices_env")]
1028    fn spec_flag_choices_env(key: &str) -> Spec {
1029        spec_with_flag(
1030            SpecFlag::builder()
1031                .long("env")
1032                .arg(SpecArg::builder().name("env").choices_env(key).build())
1033                .build(),
1034        )
1035    }
1036
1037    #[test]
1038    fn test_parse() {
1039        let cmd = SpecCommand::builder()
1040            .name("test")
1041            .arg(SpecArg::builder().name("arg").build())
1042            .flag(SpecFlag::builder().long("flag").build())
1043            .build();
1044        let spec = Spec {
1045            name: "test".to_string(),
1046            bin: "test".to_string(),
1047            cmd,
1048            ..Default::default()
1049        };
1050        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1051        let parsed = parse(&spec, &input).unwrap();
1052        assert_eq!(parsed.cmds.len(), 1);
1053        assert_eq!(parsed.cmds[0].name, "test");
1054        assert_eq!(parsed.args.len(), 1);
1055        assert_eq!(parsed.flags.len(), 1);
1056        assert_eq!(parsed.available_flags.len(), 1);
1057    }
1058
1059    #[test]
1060    fn test_as_env() {
1061        let cmd = SpecCommand::builder()
1062            .name("test")
1063            .arg(SpecArg::builder().name("arg").build())
1064            .flag(SpecFlag::builder().long("flag").build())
1065            .flag(
1066                SpecFlag::builder()
1067                    .long("force")
1068                    .negate("--no-force")
1069                    .build(),
1070            )
1071            .build();
1072        let spec = Spec {
1073            name: "test".to_string(),
1074            bin: "test".to_string(),
1075            cmd,
1076            ..Default::default()
1077        };
1078        let input = vec![
1079            "test".to_string(),
1080            "--flag".to_string(),
1081            "--no-force".to_string(),
1082        ];
1083        let parsed = parse(&spec, &input).unwrap();
1084        let env = parsed.as_env();
1085        assert_eq!(env.len(), 2);
1086        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1087        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1088    }
1089
1090    #[test]
1091    fn test_arg_env_var() {
1092        let cmd = SpecCommand::builder()
1093            .name("test")
1094            .arg(
1095                SpecArg::builder()
1096                    .name("input")
1097                    .env("TEST_ARG_INPUT")
1098                    .required(true)
1099                    .build(),
1100            )
1101            .build();
1102        let spec = Spec {
1103            name: "test".to_string(),
1104            bin: "test".to_string(),
1105            cmd,
1106            ..Default::default()
1107        };
1108
1109        // Set env var
1110        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1111
1112        let input = vec!["test".to_string()];
1113        let parsed = parse(&spec, &input).unwrap();
1114
1115        assert_eq!(parsed.args.len(), 1);
1116        let arg = parsed.args.keys().next().unwrap();
1117        assert_eq!(arg.name, "input");
1118        let value = parsed.args.values().next().unwrap();
1119        assert_eq!(value.to_string(), "test_file.txt");
1120
1121        // Clean up
1122        std::env::remove_var("TEST_ARG_INPUT");
1123    }
1124
1125    #[test]
1126    fn test_flag_env_var_with_arg() {
1127        let cmd = SpecCommand::builder()
1128            .name("test")
1129            .flag(
1130                SpecFlag::builder()
1131                    .long("output")
1132                    .env("TEST_FLAG_OUTPUT")
1133                    .arg(SpecArg::builder().name("file").build())
1134                    .build(),
1135            )
1136            .build();
1137        let spec = Spec {
1138            name: "test".to_string(),
1139            bin: "test".to_string(),
1140            cmd,
1141            ..Default::default()
1142        };
1143
1144        // Set env var
1145        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1146
1147        let input = vec!["test".to_string()];
1148        let parsed = parse(&spec, &input).unwrap();
1149
1150        assert_eq!(parsed.flags.len(), 1);
1151        let flag = parsed.flags.keys().next().unwrap();
1152        assert_eq!(flag.name, "output");
1153        let value = parsed.flags.values().next().unwrap();
1154        assert_eq!(value.to_string(), "output.txt");
1155
1156        // Clean up
1157        std::env::remove_var("TEST_FLAG_OUTPUT");
1158    }
1159
1160    #[test]
1161    fn test_flag_env_var_boolean() {
1162        let cmd = SpecCommand::builder()
1163            .name("test")
1164            .flag(
1165                SpecFlag::builder()
1166                    .long("verbose")
1167                    .env("TEST_FLAG_VERBOSE")
1168                    .build(),
1169            )
1170            .build();
1171        let spec = Spec {
1172            name: "test".to_string(),
1173            bin: "test".to_string(),
1174            cmd,
1175            ..Default::default()
1176        };
1177
1178        // Set env var to true
1179        std::env::set_var("TEST_FLAG_VERBOSE", "true");
1180
1181        let input = vec!["test".to_string()];
1182        let parsed = parse(&spec, &input).unwrap();
1183
1184        assert_eq!(parsed.flags.len(), 1);
1185        let flag = parsed.flags.keys().next().unwrap();
1186        assert_eq!(flag.name, "verbose");
1187        let value = parsed.flags.values().next().unwrap();
1188        assert_eq!(value.to_string(), "true");
1189
1190        // Clean up
1191        std::env::remove_var("TEST_FLAG_VERBOSE");
1192    }
1193
1194    #[test]
1195    fn test_env_var_precedence() {
1196        // CLI args should take precedence over env vars
1197        let cmd = SpecCommand::builder()
1198            .name("test")
1199            .arg(
1200                SpecArg::builder()
1201                    .name("input")
1202                    .env("TEST_PRECEDENCE_INPUT")
1203                    .required(true)
1204                    .build(),
1205            )
1206            .build();
1207        let spec = Spec {
1208            name: "test".to_string(),
1209            bin: "test".to_string(),
1210            cmd,
1211            ..Default::default()
1212        };
1213
1214        // Set env var
1215        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1216
1217        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1218        let parsed = parse(&spec, &input).unwrap();
1219
1220        assert_eq!(parsed.args.len(), 1);
1221        let value = parsed.args.values().next().unwrap();
1222        // CLI arg should take precedence
1223        assert_eq!(value.to_string(), "cli_file.txt");
1224
1225        // Clean up
1226        std::env::remove_var("TEST_PRECEDENCE_INPUT");
1227    }
1228
1229    #[test]
1230    fn test_flag_var_true_with_single_default() {
1231        // When var=true and default="bar", the default should be MultiString(["bar"])
1232        let cmd = SpecCommand::builder()
1233            .name("test")
1234            .flag(
1235                SpecFlag::builder()
1236                    .long("foo")
1237                    .var(true)
1238                    .arg(SpecArg::builder().name("foo").build())
1239                    .default_value("bar")
1240                    .build(),
1241            )
1242            .build();
1243        let spec = Spec {
1244            name: "test".to_string(),
1245            bin: "test".to_string(),
1246            cmd,
1247            ..Default::default()
1248        };
1249
1250        // User doesn't provide the flag
1251        let input = vec!["test".to_string()];
1252        let parsed = parse(&spec, &input).unwrap();
1253
1254        assert_eq!(parsed.flags.len(), 1);
1255        let flag = parsed.flags.keys().next().unwrap();
1256        assert_eq!(flag.name, "foo");
1257        let value = parsed.flags.values().next().unwrap();
1258        // Should be MultiString, not String
1259        match value {
1260            ParseValue::MultiString(v) => {
1261                assert_eq!(v.len(), 1);
1262                assert_eq!(v[0], "bar");
1263            }
1264            _ => panic!("Expected MultiString, got {:?}", value),
1265        }
1266    }
1267
1268    #[test]
1269    fn test_flag_var_true_with_multiple_defaults() {
1270        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
1271        let cmd = SpecCommand::builder()
1272            .name("test")
1273            .flag(
1274                SpecFlag::builder()
1275                    .long("foo")
1276                    .var(true)
1277                    .arg(SpecArg::builder().name("foo").build())
1278                    .default_values(["xyz", "bar"])
1279                    .build(),
1280            )
1281            .build();
1282        let spec = Spec {
1283            name: "test".to_string(),
1284            bin: "test".to_string(),
1285            cmd,
1286            ..Default::default()
1287        };
1288
1289        // User doesn't provide the flag
1290        let input = vec!["test".to_string()];
1291        let parsed = parse(&spec, &input).unwrap();
1292
1293        assert_eq!(parsed.flags.len(), 1);
1294        let value = parsed.flags.values().next().unwrap();
1295        // Should be MultiString with both values
1296        match value {
1297            ParseValue::MultiString(v) => {
1298                assert_eq!(v.len(), 2);
1299                assert_eq!(v[0], "xyz");
1300                assert_eq!(v[1], "bar");
1301            }
1302            _ => panic!("Expected MultiString, got {:?}", value),
1303        }
1304    }
1305
1306    #[test]
1307    fn test_flag_var_false_with_default_remains_string() {
1308        // When var=false (default), the default should still be String("bar")
1309        let cmd = SpecCommand::builder()
1310            .name("test")
1311            .flag(
1312                SpecFlag::builder()
1313                    .long("foo")
1314                    .var(false) // Default behavior
1315                    .arg(SpecArg::builder().name("foo").build())
1316                    .default_value("bar")
1317                    .build(),
1318            )
1319            .build();
1320        let spec = Spec {
1321            name: "test".to_string(),
1322            bin: "test".to_string(),
1323            cmd,
1324            ..Default::default()
1325        };
1326
1327        // User doesn't provide the flag
1328        let input = vec!["test".to_string()];
1329        let parsed = parse(&spec, &input).unwrap();
1330
1331        assert_eq!(parsed.flags.len(), 1);
1332        let value = parsed.flags.values().next().unwrap();
1333        // Should be String, not MultiString
1334        match value {
1335            ParseValue::String(s) => {
1336                assert_eq!(s, "bar");
1337            }
1338            _ => panic!("Expected String, got {:?}", value),
1339        }
1340    }
1341
1342    #[test]
1343    fn test_arg_var_true_with_single_default() {
1344        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
1345        let cmd = SpecCommand::builder()
1346            .name("test")
1347            .arg(
1348                SpecArg::builder()
1349                    .name("files")
1350                    .var(true)
1351                    .default_value("default.txt")
1352                    .required(false)
1353                    .build(),
1354            )
1355            .build();
1356        let spec = Spec {
1357            name: "test".to_string(),
1358            bin: "test".to_string(),
1359            cmd,
1360            ..Default::default()
1361        };
1362
1363        // User doesn't provide the arg
1364        let input = vec!["test".to_string()];
1365        let parsed = parse(&spec, &input).unwrap();
1366
1367        assert_eq!(parsed.args.len(), 1);
1368        let value = parsed.args.values().next().unwrap();
1369        // Should be MultiString, not String
1370        match value {
1371            ParseValue::MultiString(v) => {
1372                assert_eq!(v.len(), 1);
1373                assert_eq!(v[0], "default.txt");
1374            }
1375            _ => panic!("Expected MultiString, got {:?}", value),
1376        }
1377    }
1378
1379    #[test]
1380    fn test_arg_var_true_with_multiple_defaults() {
1381        // When arg has var=true and multiple defaults
1382        let cmd = SpecCommand::builder()
1383            .name("test")
1384            .arg(
1385                SpecArg::builder()
1386                    .name("files")
1387                    .var(true)
1388                    .default_values(["file1.txt", "file2.txt"])
1389                    .required(false)
1390                    .build(),
1391            )
1392            .build();
1393        let spec = Spec {
1394            name: "test".to_string(),
1395            bin: "test".to_string(),
1396            cmd,
1397            ..Default::default()
1398        };
1399
1400        // User doesn't provide the arg
1401        let input = vec!["test".to_string()];
1402        let parsed = parse(&spec, &input).unwrap();
1403
1404        assert_eq!(parsed.args.len(), 1);
1405        let value = parsed.args.values().next().unwrap();
1406        // Should be MultiString with both values
1407        match value {
1408            ParseValue::MultiString(v) => {
1409                assert_eq!(v.len(), 2);
1410                assert_eq!(v[0], "file1.txt");
1411                assert_eq!(v[1], "file2.txt");
1412            }
1413            _ => panic!("Expected MultiString, got {:?}", value),
1414        }
1415    }
1416
1417    #[test]
1418    fn test_arg_var_false_with_default_remains_string() {
1419        // When arg has var=false (default), the default should still be String
1420        let cmd = SpecCommand::builder()
1421            .name("test")
1422            .arg(
1423                SpecArg::builder()
1424                    .name("file")
1425                    .var(false)
1426                    .default_value("default.txt")
1427                    .required(false)
1428                    .build(),
1429            )
1430            .build();
1431        let spec = Spec {
1432            name: "test".to_string(),
1433            bin: "test".to_string(),
1434            cmd,
1435            ..Default::default()
1436        };
1437
1438        // User doesn't provide the arg
1439        let input = vec!["test".to_string()];
1440        let parsed = parse(&spec, &input).unwrap();
1441
1442        assert_eq!(parsed.args.len(), 1);
1443        let value = parsed.args.values().next().unwrap();
1444        // Should be String, not MultiString
1445        match value {
1446            ParseValue::String(s) => {
1447                assert_eq!(s, "default.txt");
1448            }
1449            _ => panic!("Expected String, got {:?}", value),
1450        }
1451    }
1452
1453    #[test]
1454    fn test_scalar_defaults_validate_only_first_default_choice() {
1455        let specs = [
1456            spec_with_arg(
1457                SpecArg::builder()
1458                    .name("env")
1459                    .var(false)
1460                    .default_values(["dev", "prod"])
1461                    .choices(["dev"])
1462                    .required(false)
1463                    .build(),
1464            ),
1465            spec_with_flag(
1466                SpecFlag::builder()
1467                    .long("env")
1468                    .arg(
1469                        SpecArg::builder()
1470                            .name("env")
1471                            .default_values(["dev", "prod"])
1472                            .choices(["dev"])
1473                            .build(),
1474                    )
1475                    .build(),
1476            ),
1477        ];
1478
1479        for spec in specs {
1480            let parsed = parse(&spec, &input(&["test"])).unwrap();
1481            assert_eq!(first_string_value(&parsed), "dev");
1482        }
1483    }
1484
1485    #[test]
1486    fn test_default_subcommand() {
1487        // Test that default_subcommand routes to the specified subcommand
1488        let run_cmd = SpecCommand::builder()
1489            .name("run")
1490            .arg(SpecArg::builder().name("task").build())
1491            .build();
1492        let mut cmd = SpecCommand::builder().name("test").build();
1493        cmd.subcommands.insert("run".to_string(), run_cmd);
1494
1495        let spec = Spec {
1496            name: "test".to_string(),
1497            bin: "test".to_string(),
1498            cmd,
1499            default_subcommand: Some("run".to_string()),
1500            ..Default::default()
1501        };
1502
1503        // "test mytask" should be parsed as if it were "test run mytask"
1504        let input = vec!["test".to_string(), "mytask".to_string()];
1505        let parsed = parse(&spec, &input).unwrap();
1506
1507        // Should have two commands: root and "run"
1508        assert_eq!(parsed.cmds.len(), 2);
1509        assert_eq!(parsed.cmds[1].name, "run");
1510
1511        // Should have parsed the task argument
1512        assert_eq!(parsed.args.len(), 1);
1513        let arg = parsed.args.keys().next().unwrap();
1514        assert_eq!(arg.name, "task");
1515        let value = parsed.args.values().next().unwrap();
1516        assert_eq!(value.to_string(), "mytask");
1517    }
1518
1519    #[test]
1520    fn test_default_subcommand_explicit_still_works() {
1521        // Test that explicit subcommand takes precedence
1522        let run_cmd = SpecCommand::builder()
1523            .name("run")
1524            .arg(SpecArg::builder().name("task").build())
1525            .build();
1526        let other_cmd = SpecCommand::builder()
1527            .name("other")
1528            .arg(SpecArg::builder().name("other_arg").build())
1529            .build();
1530        let mut cmd = SpecCommand::builder().name("test").build();
1531        cmd.subcommands.insert("run".to_string(), run_cmd);
1532        cmd.subcommands.insert("other".to_string(), other_cmd);
1533
1534        let spec = Spec {
1535            name: "test".to_string(),
1536            bin: "test".to_string(),
1537            cmd,
1538            default_subcommand: Some("run".to_string()),
1539            ..Default::default()
1540        };
1541
1542        // "test other foo" should use "other" subcommand, not default
1543        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1544        let parsed = parse(&spec, &input).unwrap();
1545
1546        // Should have used "other" subcommand
1547        assert_eq!(parsed.cmds.len(), 2);
1548        assert_eq!(parsed.cmds[1].name, "other");
1549    }
1550
1551    #[test]
1552    fn test_default_subcommand_with_nested_subcommands() {
1553        // Test that default_subcommand works when the default subcommand has nested subcommands.
1554        // This is the mise use case: "mise say" should be parsed as "mise run say"
1555        // where "say" is a subcommand of "run" (a task).
1556        let say_cmd = SpecCommand::builder()
1557            .name("say")
1558            .arg(SpecArg::builder().name("name").build())
1559            .build();
1560        let mut run_cmd = SpecCommand::builder().name("run").build();
1561        run_cmd.subcommands.insert("say".to_string(), say_cmd);
1562
1563        let mut cmd = SpecCommand::builder().name("test").build();
1564        cmd.subcommands.insert("run".to_string(), run_cmd);
1565
1566        let spec = Spec {
1567            name: "test".to_string(),
1568            bin: "test".to_string(),
1569            cmd,
1570            default_subcommand: Some("run".to_string()),
1571            ..Default::default()
1572        };
1573
1574        // "test say hello" should be parsed as "test run say hello"
1575        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1576        let parsed = parse(&spec, &input).unwrap();
1577
1578        // Should have three commands: root, "run", and "say"
1579        assert_eq!(parsed.cmds.len(), 3);
1580        assert_eq!(parsed.cmds[0].name, "test");
1581        assert_eq!(parsed.cmds[1].name, "run");
1582        assert_eq!(parsed.cmds[2].name, "say");
1583
1584        // Should have parsed the "name" argument
1585        assert_eq!(parsed.args.len(), 1);
1586        let arg = parsed.args.keys().next().unwrap();
1587        assert_eq!(arg.name, "name");
1588        let value = parsed.args.values().next().unwrap();
1589        assert_eq!(value.to_string(), "hello");
1590    }
1591
1592    /// Build a spec equivalent to the post-mount structure produced by mise's
1593    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
1594    /// subcommand that re-declares the same flag as NON-global, and a mounted task
1595    /// (`sample:run`) carrying a positional arg with `choices`.
1596    ///
1597    /// We construct the merged structure directly instead of executing a real mount so the
1598    /// test stays hermetic and cross-platform while still exercising the parser defect.
1599    fn mounted_global_flag_spec() -> Spec {
1600        let task_cmd = SpecCommand::builder()
1601            .name("sample:run")
1602            .arg(
1603                SpecArg::builder()
1604                    .name("profile")
1605                    .choices(["alpha", "beta", "gamma"])
1606                    .build(),
1607            )
1608            .build();
1609        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
1610        let mut run_cmd = SpecCommand::builder()
1611            .name("run")
1612            .flag(
1613                SpecFlag::builder()
1614                    .name("cd")
1615                    .short('C')
1616                    .long("cd")
1617                    .arg(SpecArg::builder().name("dir").build())
1618                    .global(false)
1619                    .build(),
1620            )
1621            .build();
1622        run_cmd
1623            .subcommands
1624            .insert("sample:run".to_string(), task_cmd);
1625
1626        let mut cmd = SpecCommand::builder()
1627            .name("test")
1628            .flag(
1629                SpecFlag::builder()
1630                    .name("cd")
1631                    .short('C')
1632                    .long("cd")
1633                    .arg(SpecArg::builder().name("dir").build())
1634                    .global(true)
1635                    .build(),
1636            )
1637            .build();
1638        cmd.subcommands.insert("run".to_string(), run_cmd);
1639
1640        Spec {
1641            name: "test".to_string(),
1642            bin: "test".to_string(),
1643            cmd,
1644            ..Default::default()
1645        }
1646    }
1647
1648    #[test]
1649    fn test_prefix_global_flag_does_not_pollute_choices() {
1650        // Regression for the parser-side root cause referenced by jdx/mise#10069.
1651        //
1652        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
1653        // then into the mounted `sample:run`) used to drop the inherited global flag from
1654        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
1655        // mis-validated against the task's `choices` positional arg.
1656        let spec = mounted_global_flag_spec();
1657
1658        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
1659        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
1660        for words in [
1661            &["test", "-C", "/tmp", "run", "sample:run"][..],
1662            // Embedded-value form must behave identically.
1663            &["test", "--cd=/tmp", "run", "sample:run"][..],
1664        ] {
1665            let parsed = parse_partial(&spec, &input(words)).unwrap();
1666            assert_eq!(
1667                parsed
1668                    .cmds
1669                    .iter()
1670                    .map(|c| c.name.as_str())
1671                    .collect::<Vec<_>>(),
1672                vec!["test", "run", "sample:run"],
1673            );
1674            // No positional arg should have been consumed by the leftover global-flag tokens.
1675            assert!(
1676                parsed.args.is_empty(),
1677                "args should be empty, got {:?}",
1678                parsed.args
1679            );
1680
1681            // Fix (B): the inherited global flag survives the descent even though `run`
1682            // re-declares `-C/--cd` as non-global.
1683            let cd = parsed
1684                .available_flags
1685                .get("--cd")
1686                .expect("--cd should remain available after descending into the subcommand");
1687            assert!(cd.global, "--cd must stay global after descent");
1688            assert!(
1689                parsed.available_flags.get("-C").is_some_and(|f| f.global),
1690                "-C must stay global after descent",
1691            );
1692
1693            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
1694            // for normal execution and for the env passed to mount scripts. (Removing the
1695            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
1696            assert_eq!(
1697                parsed.as_env().get("usage_cd").map(String::as_str),
1698                Some("/tmp"),
1699                "global flag value must survive in as_env(), got {:?}",
1700                parsed.as_env(),
1701            );
1702        }
1703
1704        // A real, valid choice still parses through the global flag prefix.
1705        let parsed = parse_partial(
1706            &spec,
1707            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1708        )
1709        .unwrap();
1710        assert_eq!(parsed.args.len(), 1);
1711        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1712
1713        // And genuinely invalid choices are still rejected (we didn't disable validation).
1714        assert_parse_err(
1715            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1716            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1717        );
1718    }
1719
1720    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
1721    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
1722    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
1723    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
1724    fn mounted_orphan_short_spec() -> Spec {
1725        let task_cmd = SpecCommand::builder()
1726            .name("sample:run")
1727            .arg(
1728                SpecArg::builder()
1729                    .name("profile")
1730                    .choices(["alpha", "beta", "gamma"])
1731                    .build(),
1732            )
1733            .build();
1734        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
1735        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
1736        let mut run_cmd = SpecCommand::builder()
1737            .name("run")
1738            .flag(
1739                SpecFlag::builder()
1740                    .name("raw")
1741                    .short('r')
1742                    .long("raw")
1743                    .global(false)
1744                    .build(),
1745            )
1746            .flag(
1747                SpecFlag::builder()
1748                    .name("force")
1749                    .short('f')
1750                    .long("force")
1751                    .global(false)
1752                    .build(),
1753            )
1754            .build();
1755        run_cmd
1756            .subcommands
1757            .insert("sample:run".to_string(), task_cmd);
1758
1759        // Root global is LONG-ONLY: `--raw` with no short.
1760        let mut cmd = SpecCommand::builder()
1761            .name("test")
1762            .flag(
1763                SpecFlag::builder()
1764                    .name("raw")
1765                    .long("raw")
1766                    .global(true)
1767                    .build(),
1768            )
1769            .build();
1770        cmd.subcommands.insert("run".to_string(), run_cmd);
1771
1772        Spec {
1773            name: "test".to_string(),
1774            bin: "test".to_string(),
1775            cmd,
1776            ..Default::default()
1777        }
1778    }
1779
1780    #[test]
1781    fn test_orphan_short_alias_survives_merge() {
1782        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
1783        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
1784        // added short `-r` must be unioned onto the surviving inherited global flag instead of
1785        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
1786        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
1787        let spec = mounted_orphan_short_spec();
1788
1789        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
1790        assert_eq!(
1791            parsed
1792                .cmds
1793                .iter()
1794                .map(|c| c.name.as_str())
1795                .collect::<Vec<_>>(),
1796            vec!["test", "run", "sample:run"],
1797        );
1798
1799        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
1800        // and the original long `--raw` is still global too.
1801        assert!(
1802            parsed.available_flags.get("-r").is_some_and(|f| f.global),
1803            "-r must be merged onto the inherited global flag and stay global after descent",
1804        );
1805        assert!(
1806            parsed
1807                .available_flags
1808                .get("--raw")
1809                .is_some_and(|f| f.global),
1810            "--raw must stay global after descent",
1811        );
1812
1813        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
1814        assert!(
1815            parsed.args.is_empty(),
1816            "args should be empty, got {:?}",
1817            parsed.args
1818        );
1819
1820        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
1821        assert_eq!(
1822            parsed.as_env().get("usage_raw").map(String::as_str),
1823            Some("true"),
1824            "merged short's value must survive in as_env(), got {:?}",
1825            parsed.as_env(),
1826        );
1827
1828        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
1829        // promoted/merged — it is correctly dropped when descending into the mount.
1830        assert!(
1831            parsed.available_flags.get("-f").is_none(),
1832            "purely-local -f must not be promoted onto a global",
1833        );
1834        assert!(
1835            parsed.available_flags.get("--force").is_none(),
1836            "purely-local --force must not be promoted onto a global",
1837        );
1838
1839        // A real, valid choice still parses through the merged short prefix.
1840        let parsed =
1841            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
1842        assert_eq!(parsed.args.len(), 1);
1843        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1844
1845        // And genuinely invalid choices are still rejected.
1846        assert_parse_err(
1847            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
1848            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1849        );
1850    }
1851
1852    #[test]
1853    fn test_orphan_short_does_not_clobber_unrelated_global() {
1854        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
1855        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
1856        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
1857        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
1858        let run_cmd = SpecCommand::builder()
1859            .name("run")
1860            .flag(
1861                SpecFlag::builder()
1862                    .name("raw")
1863                    .short('r')
1864                    .long("raw")
1865                    .global(false)
1866                    .build(),
1867            )
1868            .build();
1869        let mut cmd = SpecCommand::builder()
1870            .name("test")
1871            .flag(
1872                SpecFlag::builder()
1873                    .name("raw")
1874                    .long("raw")
1875                    .global(true)
1876                    .build(),
1877            )
1878            .flag(
1879                SpecFlag::builder()
1880                    .name("restrict")
1881                    .short('r')
1882                    .long("restrict")
1883                    .global(true)
1884                    .build(),
1885            )
1886            .build();
1887        cmd.subcommands.insert("run".to_string(), run_cmd);
1888        let spec = Spec {
1889            name: "test".to_string(),
1890            bin: "test".to_string(),
1891            cmd,
1892            ..Default::default()
1893        };
1894
1895        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
1896        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
1897        assert_eq!(
1898            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
1899            Some("restrict"),
1900            "-r must remain owned by the unrelated global it already belonged to",
1901        );
1902        // Both globals are still recognized and global after the descent.
1903        assert!(parsed
1904            .available_flags
1905            .get("--raw")
1906            .is_some_and(|f| f.global));
1907        assert!(parsed
1908            .available_flags
1909            .get("--restrict")
1910            .is_some_and(|f| f.global));
1911    }
1912
1913    #[test]
1914    fn test_subcommand_alias_collision_keeps_last_owner() {
1915        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
1916        // share an alias are resolved. Historically the flattened flag map gave the shared
1917        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
1918        let run_cmd = SpecCommand::builder()
1919            .name("run")
1920            .flag(
1921                SpecFlag::builder()
1922                    .name("alpha")
1923                    .short('x')
1924                    .long("alpha")
1925                    .global(false)
1926                    .build(),
1927            )
1928            .flag(
1929                SpecFlag::builder()
1930                    .name("beta")
1931                    .short('x')
1932                    .long("beta")
1933                    .global(false)
1934                    .build(),
1935            )
1936            .build();
1937        let mut cmd = SpecCommand::builder().name("test").build();
1938        cmd.subcommands.insert("run".to_string(), run_cmd);
1939        let spec = Spec {
1940            name: "test".to_string(),
1941            bin: "test".to_string(),
1942            cmd,
1943            ..Default::default()
1944        };
1945
1946        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
1947        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
1948        assert_eq!(
1949            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
1950            Some("beta"),
1951            "the last-declared flag must keep a shared short alias",
1952        );
1953        // Both distinct long aliases remain recognized and point to their own flag.
1954        assert_eq!(
1955            parsed
1956                .available_flags
1957                .get("--alpha")
1958                .map(|f| f.name.as_str()),
1959            Some("alpha"),
1960        );
1961        assert_eq!(
1962            parsed
1963                .available_flags
1964                .get("--beta")
1965                .map(|f| f.name.as_str()),
1966            Some("beta"),
1967        );
1968    }
1969
1970    #[test]
1971    fn test_default_subcommand_same_name_child() {
1972        // Test that default_subcommand doesn't cause issues when the default subcommand
1973        // has a child with the same name (e.g., "run" has a task named "run").
1974        // This verifies we don't switch multiple times or get stuck in a loop.
1975        let run_task = SpecCommand::builder()
1976            .name("run")
1977            .arg(SpecArg::builder().name("args").build())
1978            .build();
1979        let mut run_cmd = SpecCommand::builder().name("run").build();
1980        run_cmd.subcommands.insert("run".to_string(), run_task);
1981
1982        let mut cmd = SpecCommand::builder().name("test").build();
1983        cmd.subcommands.insert("run".to_string(), run_cmd);
1984
1985        let spec = Spec {
1986            name: "test".to_string(),
1987            bin: "test".to_string(),
1988            cmd,
1989            default_subcommand: Some("run".to_string()),
1990            ..Default::default()
1991        };
1992
1993        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
1994        let input = vec!["test".to_string(), "run".to_string()];
1995        let parsed = parse(&spec, &input).unwrap();
1996
1997        // Should have two commands: root and "run"
1998        assert_eq!(parsed.cmds.len(), 2);
1999        assert_eq!(parsed.cmds[0].name, "test");
2000        assert_eq!(parsed.cmds[1].name, "run");
2001
2002        // "test run run" should descend into the "run" task (child of "run" subcommand)
2003        let input = vec![
2004            "test".to_string(),
2005            "run".to_string(),
2006            "run".to_string(),
2007            "hello".to_string(),
2008        ];
2009        let parsed = parse(&spec, &input).unwrap();
2010
2011        assert_eq!(parsed.cmds.len(), 3);
2012        assert_eq!(parsed.cmds[0].name, "test");
2013        assert_eq!(parsed.cmds[1].name, "run");
2014        assert_eq!(parsed.cmds[2].name, "run");
2015        assert_eq!(parsed.args.len(), 1);
2016        let value = parsed.args.values().next().unwrap();
2017        assert_eq!(value.to_string(), "hello");
2018
2019        // Key test case: "test other" should switch to default subcommand "run"
2020        // and treat "other" as a positional arg (not try to switch again because
2021        // "run" also has a "run" child).
2022        let mut run_cmd = SpecCommand::builder()
2023            .name("run")
2024            .arg(SpecArg::builder().name("task").build())
2025            .build();
2026        let run_task = SpecCommand::builder().name("run").build();
2027        run_cmd.subcommands.insert("run".to_string(), run_task);
2028
2029        let mut cmd = SpecCommand::builder().name("test").build();
2030        cmd.subcommands.insert("run".to_string(), run_cmd);
2031
2032        let spec = Spec {
2033            name: "test".to_string(),
2034            bin: "test".to_string(),
2035            cmd,
2036            default_subcommand: Some("run".to_string()),
2037            ..Default::default()
2038        };
2039
2040        let input = vec!["test".to_string(), "other".to_string()];
2041        let parsed = parse(&spec, &input).unwrap();
2042
2043        // Should have two commands: root and "run" (the default)
2044        // We should NOT have switched again to the "run" task child
2045        assert_eq!(parsed.cmds.len(), 2);
2046        assert_eq!(parsed.cmds[0].name, "test");
2047        assert_eq!(parsed.cmds[1].name, "run");
2048
2049        // "other" should be parsed as a positional arg
2050        assert_eq!(parsed.args.len(), 1);
2051        let value = parsed.args.values().next().unwrap();
2052        assert_eq!(value.to_string(), "other");
2053    }
2054
2055    #[test]
2056    fn test_restart_token() {
2057        // Test that restart_token resets argument parsing
2058        let run_cmd = SpecCommand::builder()
2059            .name("run")
2060            .arg(SpecArg::builder().name("task").build())
2061            .restart_token(":::".to_string())
2062            .build();
2063        let mut cmd = SpecCommand::builder().name("test").build();
2064        cmd.subcommands.insert("run".to_string(), run_cmd);
2065
2066        let spec = Spec {
2067            name: "test".to_string(),
2068            bin: "test".to_string(),
2069            cmd,
2070            ..Default::default()
2071        };
2072
2073        // "test run task1 ::: task2" - should end up with task2 as the arg
2074        let input = vec![
2075            "test".to_string(),
2076            "run".to_string(),
2077            "task1".to_string(),
2078            ":::".to_string(),
2079            "task2".to_string(),
2080        ];
2081        let parsed = parse(&spec, &input).unwrap();
2082
2083        // After restart, args were cleared and task2 was parsed
2084        assert_eq!(parsed.args.len(), 1);
2085        let value = parsed.args.values().next().unwrap();
2086        assert_eq!(value.to_string(), "task2");
2087    }
2088
2089    #[test]
2090    fn test_restart_token_multiple() {
2091        // Test multiple restart tokens
2092        let run_cmd = SpecCommand::builder()
2093            .name("run")
2094            .arg(SpecArg::builder().name("task").build())
2095            .restart_token(":::".to_string())
2096            .build();
2097        let mut cmd = SpecCommand::builder().name("test").build();
2098        cmd.subcommands.insert("run".to_string(), run_cmd);
2099
2100        let spec = Spec {
2101            name: "test".to_string(),
2102            bin: "test".to_string(),
2103            cmd,
2104            ..Default::default()
2105        };
2106
2107        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
2108        let input = vec![
2109            "test".to_string(),
2110            "run".to_string(),
2111            "task1".to_string(),
2112            ":::".to_string(),
2113            "task2".to_string(),
2114            ":::".to_string(),
2115            "task3".to_string(),
2116        ];
2117        let parsed = parse(&spec, &input).unwrap();
2118
2119        // After multiple restarts, args were cleared and task3 was parsed
2120        assert_eq!(parsed.args.len(), 1);
2121        let value = parsed.args.values().next().unwrap();
2122        assert_eq!(value.to_string(), "task3");
2123    }
2124
2125    #[test]
2126    fn test_restart_token_clears_flag_awaiting_value() {
2127        // Test that restart_token clears pending flag values
2128        let run_cmd = SpecCommand::builder()
2129            .name("run")
2130            .arg(SpecArg::builder().name("task").build())
2131            .flag(
2132                SpecFlag::builder()
2133                    .name("jobs")
2134                    .long("jobs")
2135                    .arg(SpecArg::builder().name("count").build())
2136                    .build(),
2137            )
2138            .restart_token(":::".to_string())
2139            .build();
2140        let mut cmd = SpecCommand::builder().name("test").build();
2141        cmd.subcommands.insert("run".to_string(), run_cmd);
2142
2143        let spec = Spec {
2144            name: "test".to_string(),
2145            bin: "test".to_string(),
2146            cmd,
2147            ..Default::default()
2148        };
2149
2150        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
2151        let input = vec![
2152            "test".to_string(),
2153            "run".to_string(),
2154            "task1".to_string(),
2155            "--jobs".to_string(),
2156            ":::".to_string(),
2157            "task2".to_string(),
2158        ];
2159        let parsed = parse(&spec, &input).unwrap();
2160
2161        // task2 should be parsed as the task arg, not as --jobs value
2162        assert_eq!(parsed.args.len(), 1);
2163        let value = parsed.args.values().next().unwrap();
2164        assert_eq!(value.to_string(), "task2");
2165        // --jobs should not have a value
2166        assert!(parsed.flag_awaiting_value.is_empty());
2167    }
2168
2169    #[test]
2170    fn test_restart_token_resets_double_dash() {
2171        // Test that restart_token resets the -- separator effect
2172        let run_cmd = SpecCommand::builder()
2173            .name("run")
2174            .arg(SpecArg::builder().name("task").build())
2175            .arg(SpecArg::builder().name("extra_args").var(true).build())
2176            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
2177            .restart_token(":::".to_string())
2178            .build();
2179        let mut cmd = SpecCommand::builder().name("test").build();
2180        cmd.subcommands.insert("run".to_string(), run_cmd);
2181
2182        let spec = Spec {
2183            name: "test".to_string(),
2184            bin: "test".to_string(),
2185            cmd,
2186            ..Default::default()
2187        };
2188
2189        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
2190        let input = vec![
2191            "test".to_string(),
2192            "run".to_string(),
2193            "task1".to_string(),
2194            "--".to_string(),
2195            "extra".to_string(),
2196            ":::".to_string(),
2197            "--verbose".to_string(),
2198            "task2".to_string(),
2199        ];
2200        let parsed = parse(&spec, &input).unwrap();
2201
2202        // --verbose should be parsed as a flag (not an arg) after the restart
2203        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
2204        // task2 should be the arg after restart
2205        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2206        let value = parsed.args.get(task_arg).unwrap();
2207        assert_eq!(value.to_string(), "task2");
2208    }
2209
2210    #[test]
2211    fn test_double_dashes_without_preserve() {
2212        // Test that variadic args WITHOUT `preserve` skip "--" tokens (default behavior)
2213        let run_cmd = SpecCommand::builder()
2214            .name("run")
2215            .arg(SpecArg::builder().name("args").var(true).build())
2216            .build();
2217        let mut cmd = SpecCommand::builder().name("test").build();
2218        cmd.subcommands.insert("run".to_string(), run_cmd);
2219
2220        let spec = Spec {
2221            name: "test".to_string(),
2222            bin: "test".to_string(),
2223            cmd,
2224            ..Default::default()
2225        };
2226
2227        // "test run arg1 -- arg2 -- arg3" - all double dashes should be skipped
2228        let input = vec![
2229            "test".to_string(),
2230            "run".to_string(),
2231            "arg1".to_string(),
2232            "--".to_string(),
2233            "arg2".to_string(),
2234            "--".to_string(),
2235            "arg3".to_string(),
2236        ];
2237        let parsed = parse(&spec, &input).unwrap();
2238
2239        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2240        let value = parsed.args.get(args_arg).unwrap();
2241        assert_eq!(value.to_string(), "arg1 arg2 arg3");
2242    }
2243
2244    #[test]
2245    fn test_double_dashes_with_preserve() {
2246        // Test that variadic args WITH `preserve` keep all double dashes
2247        let run_cmd = SpecCommand::builder()
2248            .name("run")
2249            .arg(
2250                SpecArg::builder()
2251                    .name("args")
2252                    .var(true)
2253                    .double_dash(SpecDoubleDashChoices::Preserve)
2254                    .build(),
2255            )
2256            .build();
2257        let mut cmd = SpecCommand::builder().name("test").build();
2258        cmd.subcommands.insert("run".to_string(), run_cmd);
2259
2260        let spec = Spec {
2261            name: "test".to_string(),
2262            bin: "test".to_string(),
2263            cmd,
2264            ..Default::default()
2265        };
2266
2267        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
2268        let input = vec![
2269            "test".to_string(),
2270            "run".to_string(),
2271            "arg1".to_string(),
2272            "--".to_string(),
2273            "arg2".to_string(),
2274            "--".to_string(),
2275            "arg3".to_string(),
2276        ];
2277        let parsed = parse(&spec, &input).unwrap();
2278
2279        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2280        let value = parsed.args.get(args_arg).unwrap();
2281        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
2282    }
2283
2284    #[test]
2285    fn test_double_dashes_with_preserve_only_dashes() {
2286        // Test that variadic args WITH `preserve` keep all double dashes even
2287        // if the values are just double dashes
2288        let run_cmd = SpecCommand::builder()
2289            .name("run")
2290            .arg(
2291                SpecArg::builder()
2292                    .name("args")
2293                    .var(true)
2294                    .double_dash(SpecDoubleDashChoices::Preserve)
2295                    .build(),
2296            )
2297            .build();
2298        let mut cmd = SpecCommand::builder().name("test").build();
2299        cmd.subcommands.insert("run".to_string(), run_cmd);
2300
2301        let spec = Spec {
2302            name: "test".to_string(),
2303            bin: "test".to_string(),
2304            cmd,
2305            ..Default::default()
2306        };
2307
2308        // "test run -- --" - all double dashes should be preserved
2309        let input = vec![
2310            "test".to_string(),
2311            "run".to_string(),
2312            "--".to_string(),
2313            "--".to_string(),
2314        ];
2315        let parsed = parse(&spec, &input).unwrap();
2316
2317        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2318        let value = parsed.args.get(args_arg).unwrap();
2319        assert_eq!(value.to_string(), "-- --");
2320    }
2321
2322    #[test]
2323    fn test_double_dashes_with_preserve_multiple_args() {
2324        // Test with multiple args where only the second has has `preserve`
2325        let run_cmd = SpecCommand::builder()
2326            .name("run")
2327            .arg(SpecArg::builder().name("task").build())
2328            .arg(
2329                SpecArg::builder()
2330                    .name("extra_args")
2331                    .var(true)
2332                    .double_dash(SpecDoubleDashChoices::Preserve)
2333                    .build(),
2334            )
2335            .build();
2336        let mut cmd = SpecCommand::builder().name("test").build();
2337        cmd.subcommands.insert("run".to_string(), run_cmd);
2338
2339        let spec = Spec {
2340            name: "test".to_string(),
2341            bin: "test".to_string(),
2342            cmd,
2343            ..Default::default()
2344        };
2345
2346        // The first arg "task1" is captured normally
2347        // Then extra_args with `preserve` captures everything, including the "--" tokens
2348        let input = vec![
2349            "test".to_string(),
2350            "run".to_string(),
2351            "task1".to_string(),
2352            "--".to_string(),
2353            "arg1".to_string(),
2354            "--".to_string(),
2355            "--foo".to_string(),
2356        ];
2357        let parsed = parse(&spec, &input).unwrap();
2358
2359        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2360        let task_value = parsed.args.get(task_arg).unwrap();
2361        assert_eq!(task_value.to_string(), "task1");
2362
2363        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
2364        let extra_value = parsed.args.get(extra_arg).unwrap();
2365        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
2366    }
2367
2368    #[test]
2369    fn test_parser_with_custom_env_for_required_arg() {
2370        let spec = spec_with_arg(
2371            SpecArg::builder()
2372                .name("name")
2373                .env("NAME")
2374                .required(true)
2375                .build(),
2376        );
2377        std::env::remove_var("NAME");
2378
2379        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
2380            .expect("parse should succeed with custom env");
2381        assert_eq!(parsed.args.len(), 1);
2382        assert_eq!(first_string_value(&parsed), "john");
2383    }
2384
2385    #[test]
2386    fn test_parser_with_custom_env_for_required_flag() {
2387        let spec = spec_with_flag(
2388            SpecFlag::builder()
2389                .long("name")
2390                .env("NAME")
2391                .required(true)
2392                .arg(SpecArg::builder().name("name").build())
2393                .build(),
2394        );
2395        std::env::remove_var("NAME");
2396
2397        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
2398            .expect("parse should succeed with custom env");
2399        assert_eq!(parsed.flags.len(), 1);
2400        assert_eq!(first_string_value(&parsed), "jane");
2401    }
2402
2403    #[test]
2404    fn test_parser_with_custom_env_still_fails_when_missing() {
2405        let spec = spec_with_arg(
2406            SpecArg::builder()
2407                .name("name")
2408                .env("NAME")
2409                .required(true)
2410                .build(),
2411        );
2412        std::env::remove_var("NAME");
2413        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
2414    }
2415
2416    #[test]
2417    fn test_parser_does_not_treat_env_choice_value_as_help() {
2418        let spec = spec_with_arg(
2419            SpecArg::builder()
2420                .name("env")
2421                .env("CURRENT_ENV")
2422                .choices(["dev", "staging"])
2423                .required(false)
2424                .build(),
2425        );
2426
2427        assert_parse_err(
2428            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
2429            "Invalid choice for arg env: --help, expected one of dev, staging",
2430        );
2431    }
2432
2433    #[test]
2434    fn test_parser_does_not_treat_default_choice_value_as_help() {
2435        let spec = spec_with_flag(
2436            SpecFlag::builder()
2437                .long("env")
2438                .arg(
2439                    SpecArg::builder()
2440                        .name("env")
2441                        .choices(["dev", "staging"])
2442                        .build(),
2443                )
2444                .default_value("--help")
2445                .build(),
2446        );
2447
2448        assert_parse_err(
2449            parse_with_env(&spec, &["test"], &[]),
2450            "Invalid choice for option env: --help, expected one of dev, staging",
2451        );
2452    }
2453
2454    #[cfg(feature = "unstable_choices_env")]
2455    #[test]
2456    fn test_parser_arg_choices_from_custom_env() {
2457        let spec = spec_arg_choices_env("DEPLOY_ENVS");
2458
2459        let parsed =
2460            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
2461        assert_eq!(first_string_value(&parsed), "bar");
2462
2463        assert_parse_err(
2464            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
2465            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
2466        );
2467        assert_parse_err(
2468            parse_with_env(&spec, &["test", "prod"], &[]),
2469            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
2470        );
2471    }
2472
2473    #[cfg(feature = "unstable_choices_env")]
2474    #[test]
2475    fn test_parser_validates_flag_choices_from_custom_env() {
2476        let spec = spec_flag_choices_env("DEPLOY_ENVS");
2477        let parsed = parse_with_env(
2478            &spec,
2479            &["test", "--env", "baz"],
2480            &[("DEPLOY_ENVS", "foo,bar baz")],
2481        )
2482        .unwrap();
2483        assert_eq!(first_string_value(&parsed), "baz");
2484    }
2485
2486    #[cfg(feature = "unstable_choices_env")]
2487    #[test]
2488    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
2489        let arg_env_spec = spec_with_arg(
2490            SpecArg::builder()
2491                .name("env")
2492                .env("CURRENT_ENV")
2493                .choices_env("DEPLOY_ENVS")
2494                .build(),
2495        );
2496        assert_parse_err(
2497            parse_with_env(
2498                &arg_env_spec,
2499                &["test"],
2500                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
2501            ),
2502            "Invalid choice for arg env: prod, expected one of dev, staging",
2503        );
2504
2505        let flag_default_spec = spec_with_flag(
2506            SpecFlag::builder()
2507                .long("env")
2508                .arg(
2509                    SpecArg::builder()
2510                        .name("env")
2511                        .choices_env("DEPLOY_ENVS")
2512                        .build(),
2513                )
2514                .default_value("prod")
2515                .build(),
2516        );
2517        assert_parse_err(
2518            parse_with_env(
2519                &flag_default_spec,
2520                &["test"],
2521                &[("DEPLOY_ENVS", "dev,staging")],
2522            ),
2523            "Invalid choice for option env: prod, expected one of dev, staging",
2524        );
2525    }
2526
2527    #[test]
2528    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
2529        let spec: Spec = r#"
2530            flag "-v --verbose" var=#true
2531            arg "[database]" default="myapp_dev"
2532            arg "[args...]"
2533        "#
2534        .parse()
2535        .unwrap();
2536        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2537            .into_iter()
2538            .map(String::from)
2539            .collect();
2540        let parsed = parse(&spec, &input).unwrap();
2541        let env = parsed.as_env();
2542        assert_eq!(env.get("usage_database").unwrap(), "mydb");
2543        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
2544    }
2545
2546    #[test]
2547    fn test_variadic_arg_captures_unknown_flags() {
2548        let cmd = SpecCommand::builder()
2549            .name("test")
2550            .flag(SpecFlag::builder().short('v').long("verbose").build())
2551            .arg(SpecArg::builder().name("database").required(false).build())
2552            .arg(
2553                SpecArg::builder()
2554                    .name("args")
2555                    .required(false)
2556                    .var(true)
2557                    .build(),
2558            )
2559            .build();
2560        let spec = Spec {
2561            name: "test".to_string(),
2562            bin: "test".to_string(),
2563            cmd,
2564            ..Default::default()
2565        };
2566
2567        // Unknown --host flag and its value should be captured by [args...]
2568        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2569            .into_iter()
2570            .map(String::from)
2571            .collect();
2572        let parsed = parse(&spec, &input).unwrap();
2573        assert_eq!(parsed.args.len(), 2);
2574        let args_val = parsed
2575            .args
2576            .iter()
2577            .find(|(a, _)| a.name == "args")
2578            .unwrap()
2579            .1;
2580        match args_val {
2581            ParseValue::MultiString(v) => {
2582                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2583            }
2584            _ => panic!("Expected MultiString, got {:?}", args_val),
2585        }
2586    }
2587
2588    #[test]
2589    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
2590        let cmd = SpecCommand::builder()
2591            .name("test")
2592            .flag(SpecFlag::builder().short('v').long("verbose").build())
2593            .arg(SpecArg::builder().name("database").required(false).build())
2594            .arg(
2595                SpecArg::builder()
2596                    .name("args")
2597                    .required(false)
2598                    .var(true)
2599                    .build(),
2600            )
2601            .build();
2602        let spec = Spec {
2603            name: "test".to_string(),
2604            bin: "test".to_string(),
2605            cmd,
2606            ..Default::default()
2607        };
2608
2609        // With explicit -- separator
2610        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
2611            .into_iter()
2612            .map(String::from)
2613            .collect();
2614        let parsed = parse(&spec, &input).unwrap();
2615        assert_eq!(parsed.args.len(), 2);
2616        let args_val = parsed
2617            .args
2618            .iter()
2619            .find(|(a, _)| a.name == "args")
2620            .unwrap()
2621            .1;
2622        match args_val {
2623            ParseValue::MultiString(v) => {
2624                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2625            }
2626            _ => panic!("Expected MultiString, got {:?}", args_val),
2627        }
2628    }
2629}