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