Skip to main content

argot_cmd/parser/
mod.rs

1//! Tokenization and argument parsing for raw `argv` slices.
2//!
3//! The parser operates in three stages:
4//!
5//! 1. **Tokenize** — the raw `&[&str]` argv is converted to a typed
6//!    token stream by the internal `tokenizer` module.
7//!    Each token is one of: a plain word, a long flag (`--name` / `--name=val`),
8//!    a short flag (`-f` / `-fval`), or the `--` separator.
9//!
10//! 2. **Subcommand tree walk** — the first word token is resolved to a
11//!    top-level [`Command`] using the [`Resolver`]. While the
12//!    resolved command has subcommands and the next token is a word that
13//!    resolves to one of them, the parser descends into the subcommand tree.
14//!    A word that fails to resolve is treated as the start of positional
15//!    arguments rather than an error.
16//!
17//! 3. **Flag and argument binding** — remaining tokens are bound using a
18//!    queue-based loop: long and short flag tokens are matched against the
19//!    resolved command's flag definitions; plain word tokens are accumulated
20//!    as positional arguments. Adjacent short flags (`-abc`) are expanded
21//!    inline: each boolean flag in the run registers `"true"` and the
22//!    remaining characters are re-queued as a new `ShortFlag` token.
23//!    Boolean flags also support `--no-{name}` negation syntax, which sets
24//!    the value to `"false"`. After all tokens are consumed, positional
25//!    arguments are bound by declaration order (variadic last arguments
26//!    collect all remaining positionals into a JSON array); required flags
27//!    and arguments are validated; and defaults and environment-variable fallbacks are applied.
28//!
29//! # Example
30//!
31//! ```
32//! # use argot_cmd::{Command, Argument, Flag, Parser};
33//! let cmd = Command::builder("list")
34//!     .argument(Argument::builder("filter").build().unwrap())
35//!     .flag(Flag::builder("verbose").short('v').build().unwrap())
36//!     .build()
37//!     .unwrap();
38//!
39//! let cmds = vec![cmd];
40//! let parser = Parser::new(&cmds);
41//!
42//! let parsed = parser.parse(&["list", "foo", "-v"]).unwrap();
43//! assert_eq!(parsed.command.canonical, "list");
44//! assert_eq!(parsed.args["filter"], "foo");
45//! assert_eq!(parsed.flags["verbose"], "true");
46//! ```
47
48mod tokenizer;
49
50use std::collections::{HashMap, VecDeque};
51
52use thiserror::Error;
53
54use crate::model::{Command, ParsedCommand};
55use crate::resolver::{ResolveError, Resolver};
56
57use tokenizer::{tokenize, Token};
58
59/// Errors produced by [`Parser::parse`].
60#[derive(Debug, Error, PartialEq)]
61pub enum ParseError {
62    /// The argv slice was empty — no command name was provided.
63    #[error("no command provided")]
64    NoCommand,
65    /// The command (or subcommand) token could not be resolved.
66    ///
67    /// Wraps a [`ResolveError`] transparently so callers can match on
68    /// [`ResolveError::Unknown`] and [`ResolveError::Ambiguous`] directly.
69    #[error(transparent)]
70    Resolve(#[from] ResolveError),
71    /// A required positional argument was not supplied.
72    ///
73    /// The inner `String` is the argument's canonical name.
74    #[error("missing required argument: {0}")]
75    MissingArgument(String),
76    /// More positional arguments were supplied than the command declares.
77    ///
78    /// The inner `String` is the first unexpected token.
79    #[error("unexpected argument: {0}")]
80    UnexpectedArgument(String),
81    /// A required flag was not supplied, and no environment-variable fallback
82    /// (registered with [`crate::FlagBuilder::env`]) provided a value.
83    ///
84    /// The inner `String` is the flag's long name (without `--`).
85    #[error("missing required flag: --{0}")]
86    MissingFlag(String),
87    /// A value-taking flag was provided without a following value.
88    #[error("flag --{name} requires a value")]
89    FlagMissingValue {
90        /// The long name of the flag that was missing its value.
91        name: String,
92    },
93    /// A flag token (`--name` or `-c`) was not recognized by the resolved
94    /// command.
95    ///
96    /// The inner `String` includes the leading dashes, e.g. `"--foo"` or
97    /// `"-x"`. This variant is also raised when `--no-{name}` negation syntax
98    /// is used with an unknown flag name or with a value-taking flag (which
99    /// cannot be negated).
100    #[error("unknown flag: {0}")]
101    UnknownFlag(String),
102    /// A word token following a subcommand-only parent did not match any
103    /// declared subcommand.
104    ///
105    /// Only raised when the parent command has no positional arguments defined;
106    /// otherwise the word is treated as a positional value.
107    #[error("unknown subcommand `{got}` for `{parent}`")]
108    UnknownSubcommand {
109        /// The canonical name of the parent command.
110        parent: String,
111        /// The unrecognised token as supplied by the caller.
112        got: String,
113    },
114    /// A flag value was provided that is not in the flag's allowed choices.
115    #[error("invalid value `{value}` for `--{flag}`: expected one of {choices:?}")]
116    InvalidChoice {
117        /// The flag's long name.
118        flag: String,
119        /// The invalid value that was supplied.
120        value: String,
121        /// The allowed values.
122        choices: Vec<String>,
123    },
124    /// Two or more mutually exclusive flags were provided in the same invocation.
125    ///
126    /// The `flags` field lists the conflicting flags (with `--` prefix).
127    #[error("flags {flags:?} are mutually exclusive — provide at most one")]
128    MutuallyExclusive {
129        /// The conflicting flag names that were all set (with `--` prefix).
130        flags: Vec<String>,
131    },
132}
133
134/// Parses raw argument slices against a slice of registered [`Command`]s.
135///
136/// Create a `Parser` with [`Parser::new`], then call [`Parser::parse`] for
137/// each invocation. The parser borrows the command slice for lifetime `'a`;
138/// the returned [`ParsedCommand`] also carries that lifetime.
139///
140/// # Examples
141///
142/// ```
143/// # use argot_cmd::{Command, Parser};
144/// let cmds = vec![Command::builder("status").build().unwrap()];
145/// let parser = Parser::new(&cmds);
146/// let parsed = parser.parse(&["status"]).unwrap();
147/// assert_eq!(parsed.command.canonical, "status");
148/// ```
149pub struct Parser<'a> {
150    commands: &'a [Command],
151}
152
153impl<'a> Parser<'a> {
154    /// Create a new `Parser` over the given command slice.
155    ///
156    /// # Arguments
157    ///
158    /// - `commands` — Top-level commands to parse against. The lifetime `'a`
159    ///   is propagated to the [`ParsedCommand`] returned by [`Parser::parse`].
160    pub fn new(commands: &'a [Command]) -> Self {
161        Self { commands }
162    }
163
164    /// Parse `argv` (the full argument list including the command name) into a
165    /// [`ParsedCommand`] that borrows from the registered command tree.
166    ///
167    /// The first element of `argv` must be a word token naming the top-level
168    /// command. Subsequent tokens are processed as described in the
169    /// [module documentation][self].
170    ///
171    /// # Arguments
172    ///
173    /// - `argv` — The argument slice to parse. Should **not** include the
174    ///   program name (`argv[0]` in `std::env::args`); the first element must
175    ///   be the command name.
176    ///
177    /// # Errors
178    ///
179    /// - [`ParseError::NoCommand`] — `argv` is empty.
180    /// - [`ParseError::Resolve`] — the command or subcommand token could not
181    ///   be resolved (wraps [`ResolveError::Unknown`] or
182    ///   [`ResolveError::Ambiguous`]).
183    /// - [`ParseError::MissingArgument`] — a required positional argument was
184    ///   absent.
185    /// - [`ParseError::UnexpectedArgument`] — more positional arguments were
186    ///   provided than the command declares.
187    /// - [`ParseError::MissingFlag`] — a required flag was absent.
188    /// - [`ParseError::FlagMissingValue`] — a value-taking flag had no
189    ///   following value.
190    /// - [`ParseError::UnknownFlag`] — an unrecognized flag was encountered.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// # use argot_cmd::{Command, Flag, Parser};
196    /// let cmd = Command::builder("build")
197    ///     .flag(
198    ///         Flag::builder("target")
199    ///             .takes_value()
200    ///             .default_value("debug")
201    ///             .build()
202    ///             .unwrap(),
203    ///     )
204    ///     .build()
205    ///     .unwrap();
206    ///
207    /// let cmds = vec![cmd];
208    /// let parser = Parser::new(&cmds);
209    ///
210    /// let parsed = parser.parse(&["build", "--target=release"]).unwrap();
211    /// assert_eq!(parsed.flags["target"], "release");
212    ///
213    /// // Default is applied when flag is absent
214    /// let parsed2 = parser.parse(&["build"]).unwrap();
215    /// assert_eq!(parsed2.flags["target"], "debug");
216    /// ```
217    pub fn parse(&self, argv: &[&str]) -> Result<ParsedCommand<'a>, ParseError> {
218        let tokens = tokenize(argv);
219        let mut pos = 0;
220
221        // First token must be a Word naming the top-level command.
222        let cmd_name = match tokens.get(pos) {
223            Some(Token::Word(w)) => {
224                pos += 1;
225                w.clone()
226            }
227            _ => return Err(ParseError::NoCommand),
228        };
229
230        let resolver = Resolver::new(self.commands);
231        let mut cmd: &'a Command = resolver.resolve(&cmd_name)?;
232
233        // Walk the subcommand tree while the next token is a Word that resolves.
234        loop {
235            if cmd.subcommands.is_empty() {
236                break;
237            }
238            match tokens.get(pos) {
239                Some(Token::Word(w)) => {
240                    let sub_resolver = Resolver::new(&cmd.subcommands);
241                    match sub_resolver.resolve(w) {
242                        Ok(sub) => {
243                            cmd = sub;
244                            pos += 1;
245                        }
246                        Err(e) => match e {
247                            // Ambiguous is always a user error — propagate it.
248                            ResolveError::Ambiguous { .. } => return Err(ParseError::Resolve(e)),
249                            // Unknown: propagate only when the parent has no
250                            // positional arguments (nowhere legitimate for the
251                            // word to land). Otherwise break and treat as positional.
252                            ResolveError::Unknown { .. } => {
253                                if cmd.arguments.is_empty() {
254                                    return Err(ParseError::UnknownSubcommand {
255                                        parent: cmd.canonical.clone(),
256                                        got: w.clone(),
257                                    });
258                                }
259                                break;
260                            }
261                        },
262                    }
263                }
264                _ => break,
265            }
266        }
267
268        // Process remaining tokens: flags and positional arguments.
269        // Uses a queue so that adjacent short flags (-abc) can push synthetic
270        // ShortFlag tokens back to the front for processing.
271        let mut positionals: Vec<String> = Vec::new();
272        let mut flags: HashMap<String, String> = HashMap::new();
273
274        let mut queue: VecDeque<Token> = tokens[pos..].iter().cloned().collect();
275
276        while let Some(token) = queue.pop_front() {
277            match token {
278                Token::Separator => {
279                    // Everything after -- is a positional word (tokenizer already
280                    // converts post-separator args to Token::Word, so this is a
281                    // no-op guard for the separator token itself).
282                }
283                Token::Word(w) => {
284                    positionals.push(w);
285                }
286                Token::LongFlag { name, value } => {
287                    // Check --no-{name} negation for boolean flags.
288                    if let Some(base) = name.strip_prefix("no-") {
289                        if let Some(flag_def) =
290                            cmd.flags.iter().find(|f| f.name == base && !f.takes_value)
291                        {
292                            if value.is_some() {
293                                return Err(ParseError::UnknownFlag(format!("--{}", name)));
294                            }
295                            flags.insert(flag_def.name.clone(), "false".to_string());
296                            continue;
297                        }
298                    }
299
300                    let flag_def = cmd
301                        .flags
302                        .iter()
303                        .find(|f| f.name == name)
304                        .ok_or_else(|| ParseError::UnknownFlag(format!("--{}", name)))?;
305
306                    let val = if flag_def.takes_value {
307                        if let Some(v) = value {
308                            v
309                        } else {
310                            match queue.pop_front() {
311                                Some(Token::Word(w)) => w,
312                                _ => {
313                                    return Err(ParseError::FlagMissingValue {
314                                        name: flag_def.name.clone(),
315                                    })
316                                }
317                            }
318                        }
319                    } else {
320                        "true".to_string()
321                    };
322
323                    // Validate choice constraint
324                    if let Some(choices) = &flag_def.choices {
325                        if !choices.contains(&val) {
326                            return Err(ParseError::InvalidChoice {
327                                flag: flag_def.name.clone(),
328                                value: val,
329                                choices: choices.clone(),
330                            });
331                        }
332                    }
333
334                    // Repeatable flag accumulation
335                    if flag_def.repeatable {
336                        if flag_def.takes_value {
337                            // Accumulate into JSON array
338                            let new_val = match flags.get(&flag_def.name) {
339                                None => serde_json::to_string(&[&val]).expect("serde_json serialization of &[&str] is infallible for simple string types"),
340                                Some(existing) => {
341                                    let mut arr: Vec<String> = serde_json::from_str(existing)
342                                        .unwrap_or_else(|_| vec![existing.clone()]);
343                                    arr.push(val);
344                                    serde_json::to_string(&arr).expect("serde_json serialization of Vec<String> is infallible")
345                                }
346                            };
347                            flags.insert(flag_def.name.clone(), new_val);
348                        } else {
349                            // Count occurrences
350                            let count = flags
351                                .get(&flag_def.name)
352                                .and_then(|v| v.parse::<u64>().ok())
353                                .unwrap_or(0);
354                            flags.insert(flag_def.name.clone(), (count + 1).to_string());
355                        }
356                    } else {
357                        flags.insert(flag_def.name.clone(), val);
358                    }
359                }
360                Token::ShortFlag { name: c, value } => {
361                    let flag_def = cmd
362                        .flags
363                        .iter()
364                        .find(|f| f.short == Some(c))
365                        .ok_or_else(|| ParseError::UnknownFlag(format!("-{}", c)))?;
366
367                    if flag_def.takes_value {
368                        let val = if let Some(v) = value {
369                            v
370                        } else {
371                            match queue.pop_front() {
372                                Some(Token::Word(w)) => w,
373                                _ => {
374                                    return Err(ParseError::FlagMissingValue {
375                                        name: flag_def.name.clone(),
376                                    })
377                                }
378                            }
379                        };
380
381                        // Validate choice constraint
382                        if let Some(choices) = &flag_def.choices {
383                            if !choices.contains(&val) {
384                                return Err(ParseError::InvalidChoice {
385                                    flag: flag_def.name.clone(),
386                                    value: val,
387                                    choices: choices.clone(),
388                                });
389                            }
390                        }
391
392                        // Repeatable flag accumulation
393                        if flag_def.repeatable {
394                            let new_val = match flags.get(&flag_def.name) {
395                                None => serde_json::to_string(&[&val]).expect("serde_json serialization of &[&str] is infallible for simple string types"),
396                                Some(existing) => {
397                                    let mut arr: Vec<String> = serde_json::from_str(existing)
398                                        .unwrap_or_else(|_| vec![existing.clone()]);
399                                    arr.push(val);
400                                    serde_json::to_string(&arr).expect("serde_json serialization of Vec<String> is infallible")
401                                }
402                            };
403                            flags.insert(flag_def.name.clone(), new_val);
404                        } else {
405                            flags.insert(flag_def.name.clone(), val);
406                        }
407                    } else {
408                        // Boolean flag: register as true (or count if repeatable) and expand remaining chars.
409                        if flag_def.repeatable {
410                            let count = flags
411                                .get(&flag_def.name)
412                                .and_then(|v| v.parse::<u64>().ok())
413                                .unwrap_or(0);
414                            flags.insert(flag_def.name.clone(), (count + 1).to_string());
415                        } else {
416                            flags.insert(flag_def.name.clone(), "true".to_string());
417                        }
418                        if let Some(rest) = value {
419                            if !rest.is_empty() {
420                                let mut chars = rest.chars();
421                                let next_c =
422                                    chars.next().expect("guarded by is_empty() check above");
423                                let remainder: String = chars.collect();
424                                queue.push_front(Token::ShortFlag {
425                                    name: next_c,
426                                    value: if remainder.is_empty() {
427                                        None
428                                    } else {
429                                        Some(remainder)
430                                    },
431                                });
432                            }
433                        }
434                    }
435                }
436            }
437        }
438
439        // Bind positional arguments to declared argument slots.
440        let mut args: HashMap<String, String> = HashMap::new();
441        for (i, arg_def) in cmd.arguments.iter().enumerate() {
442            if arg_def.variadic {
443                // Collect all remaining positionals into a JSON array string.
444                let values: Vec<&String> = positionals[i..].iter().collect();
445                if values.is_empty() && arg_def.required {
446                    return Err(ParseError::MissingArgument(arg_def.name.clone()));
447                } else if !values.is_empty() {
448                    let json_val = serde_json::to_string(
449                        &values.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
450                    )
451                    .expect(
452                        "serde_json serialization of &[&str] is infallible for simple string types",
453                    );
454                    args.insert(arg_def.name.clone(), json_val);
455                } else if let Some(default) = &arg_def.default {
456                    args.insert(arg_def.name.clone(), default.clone());
457                }
458                break; // variadic is always last
459            }
460            if let Some(val) = positionals.get(i) {
461                args.insert(arg_def.name.clone(), val.clone());
462            } else if arg_def.required {
463                return Err(ParseError::MissingArgument(arg_def.name.clone()));
464            } else if let Some(default) = &arg_def.default {
465                args.insert(arg_def.name.clone(), default.clone());
466            }
467        }
468
469        // Only error on unexpected positionals if the last argument is NOT variadic.
470        let last_is_variadic = cmd.arguments.last().map(|a| a.variadic).unwrap_or(false);
471        if positionals.len() > cmd.arguments.len() && !last_is_variadic {
472            return Err(ParseError::UnexpectedArgument(
473                positionals[cmd.arguments.len()].clone(),
474            ));
475        }
476
477        // Env var fallback: for each flag not yet set by argv, check its env var.
478        for flag_def in &cmd.flags {
479            if !flags.contains_key(&flag_def.name) {
480                if let Some(ref var_name) = flag_def.env {
481                    if let Ok(val) = std::env::var(var_name) {
482                        if !val.is_empty() {
483                            // Validate against choices if present
484                            if let Some(ref choices) = flag_def.choices {
485                                if !choices.contains(&val) {
486                                    return Err(ParseError::InvalidChoice {
487                                        flag: flag_def.name.clone(),
488                                        value: val,
489                                        choices: choices.clone(),
490                                    });
491                                }
492                            }
493                            flags.insert(flag_def.name.clone(), val);
494                        }
495                    }
496                }
497            }
498        }
499
500        // Enforce mutual exclusivity groups.
501        for group in &cmd.exclusive_groups {
502            let set: Vec<String> = group
503                .iter()
504                .filter(|name| flags.contains_key(*name))
505                .map(|name| format!("--{}", name))
506                .collect();
507            if set.len() > 1 {
508                return Err(ParseError::MutuallyExclusive { flags: set });
509            }
510        }
511
512        // Validate required flags; apply defaults.
513        for flag_def in &cmd.flags {
514            if flag_def.required && !flags.contains_key(&flag_def.name) {
515                return Err(ParseError::MissingFlag(flag_def.name.clone()));
516            }
517            if !flags.contains_key(&flag_def.name) {
518                if let Some(default) = &flag_def.default {
519                    flags.insert(flag_def.name.clone(), default.clone());
520                }
521            }
522        }
523
524        Ok(ParsedCommand {
525            command: cmd,
526            args,
527            flags,
528        })
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::model::{Argument, Command, Example, Flag};
536
537    fn build_commands() -> Vec<Command> {
538        let remote_add = Command::builder("add")
539            .summary("Add a remote")
540            .argument(
541                Argument::builder("name")
542                    .description("remote name")
543                    .required()
544                    .build()
545                    .unwrap(),
546            )
547            .argument(
548                Argument::builder("url")
549                    .description("remote url")
550                    .required()
551                    .build()
552                    .unwrap(),
553            )
554            .build()
555            .unwrap();
556
557        let remote_remove = Command::builder("remove")
558            .alias("rm")
559            .summary("Remove a remote")
560            .argument(
561                Argument::builder("name")
562                    .description("remote name")
563                    .required()
564                    .build()
565                    .unwrap(),
566            )
567            .build()
568            .unwrap();
569
570        let remote = Command::builder("remote")
571            .summary("Manage remotes")
572            .subcommand(remote_add)
573            .subcommand(remote_remove)
574            .build()
575            .unwrap();
576
577        let list = Command::builder("list")
578            .alias("ls")
579            .summary("List items")
580            .argument(
581                Argument::builder("filter")
582                    .description("optional filter")
583                    .build()
584                    .unwrap(),
585            )
586            .flag(
587                Flag::builder("verbose")
588                    .short('v')
589                    .description("verbose output")
590                    .build()
591                    .unwrap(),
592            )
593            .flag(
594                Flag::builder("output")
595                    .short('o')
596                    .description("output format")
597                    .takes_value()
598                    .default_value("text")
599                    .build()
600                    .unwrap(),
601            )
602            .example(Example::new("list all", "myapp list"))
603            .build()
604            .unwrap();
605
606        let deploy = Command::builder("deploy")
607            .summary("Deploy")
608            .flag(
609                Flag::builder("env")
610                    .description("target environment")
611                    .takes_value()
612                    .required()
613                    .build()
614                    .unwrap(),
615            )
616            .build()
617            .unwrap();
618
619        vec![list, remote, deploy]
620    }
621
622    struct TestCase {
623        name: &'static str,
624        argv: &'static [&'static str],
625        expect_err: bool,
626        expected_canonical: Option<&'static str>,
627        expected_args: Vec<(&'static str, &'static str)>,
628        expected_flags: Vec<(&'static str, &'static str)>,
629    }
630
631    #[test]
632    fn test_parse() {
633        let commands = build_commands();
634        let parser = Parser::new(&commands);
635
636        let cases = vec![
637            TestCase {
638                name: "flat command no args",
639                argv: &["list"],
640                expect_err: false,
641                expected_canonical: Some("list"),
642                expected_args: vec![],
643                expected_flags: vec![("output", "text")],
644            },
645            TestCase {
646                name: "flat command with positional",
647                argv: &["list", "foo"],
648                expect_err: false,
649                expected_canonical: Some("list"),
650                expected_args: vec![("filter", "foo")],
651                expected_flags: vec![("output", "text")],
652            },
653            TestCase {
654                name: "alias resolved",
655                argv: &["ls"],
656                expect_err: false,
657                expected_canonical: Some("list"),
658                expected_args: vec![],
659                expected_flags: vec![("output", "text")],
660            },
661            TestCase {
662                name: "boolean flag short",
663                argv: &["list", "-v"],
664                expect_err: false,
665                expected_canonical: Some("list"),
666                expected_args: vec![],
667                expected_flags: vec![("verbose", "true"), ("output", "text")],
668            },
669            TestCase {
670                name: "long flag equals",
671                argv: &["list", "--output=json"],
672                expect_err: false,
673                expected_canonical: Some("list"),
674                expected_args: vec![],
675                expected_flags: vec![("output", "json")],
676            },
677            TestCase {
678                name: "long flag space value",
679                argv: &["list", "--output", "json"],
680                expect_err: false,
681                expected_canonical: Some("list"),
682                expected_args: vec![],
683                expected_flags: vec![("output", "json")],
684            },
685            TestCase {
686                name: "short flag space value",
687                argv: &["list", "-o", "json"],
688                expect_err: false,
689                expected_canonical: Some("list"),
690                expected_args: vec![],
691                expected_flags: vec![("output", "json")],
692            },
693            TestCase {
694                name: "two-level subcommand",
695                argv: &["remote", "add", "origin", "https://example.com"],
696                expect_err: false,
697                expected_canonical: Some("add"),
698                expected_args: vec![("name", "origin"), ("url", "https://example.com")],
699                expected_flags: vec![],
700            },
701            TestCase {
702                name: "subcommand alias",
703                argv: &["remote", "rm", "origin"],
704                expect_err: false,
705                expected_canonical: Some("remove"),
706                expected_args: vec![("name", "origin")],
707                expected_flags: vec![],
708            },
709            TestCase {
710                name: "no command",
711                argv: &[],
712                expect_err: true,
713                expected_canonical: None,
714                expected_args: vec![],
715                expected_flags: vec![],
716            },
717            TestCase {
718                name: "unknown command",
719                argv: &["unknown"],
720                expect_err: true,
721                expected_canonical: None,
722                expected_args: vec![],
723                expected_flags: vec![],
724            },
725            TestCase {
726                name: "unknown flag",
727                argv: &["list", "--nope"],
728                expect_err: true,
729                expected_canonical: None,
730                expected_args: vec![],
731                expected_flags: vec![],
732            },
733            TestCase {
734                name: "missing required flag",
735                argv: &["deploy"],
736                expect_err: true,
737                expected_canonical: None,
738                expected_args: vec![],
739                expected_flags: vec![],
740            },
741            TestCase {
742                name: "unexpected positional",
743                argv: &["list", "one", "two"],
744                expect_err: true,
745                expected_canonical: None,
746                expected_args: vec![],
747                expected_flags: vec![],
748            },
749        ];
750
751        for tc in &cases {
752            let result = parser.parse(tc.argv);
753            if tc.expect_err {
754                assert!(result.is_err(), "case '{}': expected error", tc.name);
755            } else {
756                let parsed = result
757                    .unwrap_or_else(|e| panic!("case '{}': unexpected error: {}", tc.name, e));
758                assert_eq!(
759                    parsed.command.canonical,
760                    tc.expected_canonical.unwrap(),
761                    "case '{}'",
762                    tc.name
763                );
764                for (k, v) in &tc.expected_args {
765                    assert_eq!(
766                        parsed.args.get(*k).map(String::as_str),
767                        Some(*v),
768                        "case '{}': arg {}",
769                        tc.name,
770                        k
771                    );
772                }
773                for (k, v) in &tc.expected_flags {
774                    assert_eq!(
775                        parsed.flags.get(*k).map(String::as_str),
776                        Some(*v),
777                        "case '{}': flag {}",
778                        tc.name,
779                        k
780                    );
781                }
782            }
783        }
784    }
785
786    #[test]
787    fn test_double_dash_separator() {
788        let cmds = vec![Command::builder("run")
789            .argument(
790                Argument::builder("script")
791                    .description("script to run")
792                    .required()
793                    .build()
794                    .unwrap(),
795            )
796            .build()
797            .unwrap()];
798        let parser = Parser::new(&cmds);
799        // "--" separator should make "--not-a-flag" treated as a positional word.
800        // But our command only has one argument, so the second word would be unexpected.
801        // Let's just verify `--` itself doesn't cause a parse error on the separator.
802        let result = parser.parse(&["run", "--", "myscript"]);
803        assert!(result.is_ok());
804        assert_eq!(result.unwrap().args["script"], "myscript");
805    }
806
807    #[test]
808    fn test_missing_required_argument() {
809        let cmds = vec![Command::builder("get")
810            .argument(
811                Argument::builder("id")
812                    .description("item id")
813                    .required()
814                    .build()
815                    .unwrap(),
816            )
817            .build()
818            .unwrap()];
819        let parser = Parser::new(&cmds);
820        assert!(matches!(
821            parser.parse(&["get"]),
822            Err(ParseError::MissingArgument(ref s)) if s == "id"
823        ));
824    }
825
826    #[test]
827    fn test_flag_missing_value() {
828        let cmds = vec![Command::builder("build")
829            .flag(Flag::builder("target").takes_value().build().unwrap())
830            .build()
831            .unwrap()];
832        let parser = Parser::new(&cmds);
833        assert!(matches!(
834            parser.parse(&["build", "--target"]),
835            Err(ParseError::FlagMissingValue { .. })
836        ));
837    }
838
839    #[test]
840    fn test_ambiguous_subcommand() {
841        let fetch = Command::builder("fetch").build().unwrap();
842        let force_push = Command::builder("force-push").build().unwrap();
843        let cmds = vec![Command::builder("git")
844            .subcommand(fetch)
845            .subcommand(force_push)
846            .build()
847            .unwrap()];
848        let parser = Parser::new(&cmds);
849        let result = parser.parse(&["git", "f"]);
850        assert!(
851            matches!(
852                result,
853                Err(ParseError::Resolve(ResolveError::Ambiguous { .. }))
854            ),
855            "expected Resolve(Ambiguous), got {:?}",
856            result
857        );
858    }
859
860    #[test]
861    fn test_unknown_subcommand_on_no_positionals() {
862        let cmds = vec![Command::builder("remote")
863            .subcommand(Command::builder("add").build().unwrap())
864            .build()
865            .unwrap()];
866        let parser = Parser::new(&cmds);
867        assert!(matches!(
868            parser.parse(&["remote", "xyz"]),
869            Err(ParseError::UnknownSubcommand { .. })
870        ));
871    }
872
873    #[test]
874    fn test_unknown_word_treated_as_positional_when_parent_has_args() {
875        let cmds = vec![Command::builder("deploy")
876            .subcommand(Command::builder("production").build().unwrap())
877            .argument(
878                Argument::builder("target")
879                    .description("deployment target")
880                    .required()
881                    .build()
882                    .unwrap(),
883            )
884            .build()
885            .unwrap()];
886        let parser = Parser::new(&cmds);
887        let result = parser.parse(&["deploy", "staging"]);
888        assert!(result.is_ok(), "expected Ok, got {:?}", result);
889        let parsed = result.unwrap();
890        assert_eq!(parsed.command.canonical, "deploy");
891        assert_eq!(
892            parsed.args.get("target").map(String::as_str),
893            Some("staging")
894        );
895    }
896
897    // -----------------------------------------------------------------------
898    // Enhancement 1: Adjacent short flags (-abc → -a -b -c)
899    // -----------------------------------------------------------------------
900
901    fn build_multi_flag_command() -> Vec<Command> {
902        vec![Command::builder("cmd")
903            .flag(Flag::builder("verbose").short('v').build().unwrap())
904            .flag(Flag::builder("no-wait").short('n').build().unwrap())
905            .flag(
906                Flag::builder("output")
907                    .short('o')
908                    .takes_value()
909                    .default_value("text")
910                    .build()
911                    .unwrap(),
912            )
913            .build()
914            .unwrap()]
915    }
916
917    #[test]
918    fn test_adjacent_short_flags() {
919        let cmds = build_multi_flag_command();
920        let parser = Parser::new(&cmds);
921
922        // -vo json: -v is boolean (→ verbose=true), -o takes a value (→ output=json)
923        let result = parser.parse(&["cmd", "-vo", "json"]);
924        assert!(result.is_ok(), "expected Ok, got {:?}", result);
925        let parsed = result.unwrap();
926        assert_eq!(
927            parsed.flags.get("verbose").map(String::as_str),
928            Some("true")
929        );
930        assert_eq!(parsed.flags.get("output").map(String::as_str), Some("json"));
931
932        // -vn: both boolean flags → verbose=true, no-wait=true
933        let result2 = parser.parse(&["cmd", "-vn"]);
934        assert!(result2.is_ok(), "expected Ok, got {:?}", result2);
935        let parsed2 = result2.unwrap();
936        assert_eq!(
937            parsed2.flags.get("verbose").map(String::as_str),
938            Some("true")
939        );
940        assert_eq!(
941            parsed2.flags.get("no-wait").map(String::as_str),
942            Some("true")
943        );
944    }
945
946    #[test]
947    fn test_adjacent_short_flags_with_value() {
948        let cmds = build_multi_flag_command();
949        let parser = Parser::new(&cmds);
950
951        // -ofile.txt: -o takes_value, so "file.txt" is the inline value
952        let result = parser.parse(&["cmd", "-ofile.txt"]);
953        assert!(result.is_ok(), "expected Ok, got {:?}", result);
954        let parsed = result.unwrap();
955        assert_eq!(
956            parsed.flags.get("output").map(String::as_str),
957            Some("file.txt")
958        );
959    }
960
961    // -----------------------------------------------------------------------
962    // Enhancement 2: --no-flag negation
963    // -----------------------------------------------------------------------
964
965    #[test]
966    fn test_flag_negation() {
967        let cmds = vec![Command::builder("cmd")
968            .flag(Flag::builder("verbose").short('v').build().unwrap())
969            .build()
970            .unwrap()];
971        let parser = Parser::new(&cmds);
972
973        let result = parser.parse(&["cmd", "--no-verbose"]);
974        assert!(result.is_ok(), "expected Ok, got {:?}", result);
975        let parsed = result.unwrap();
976        assert_eq!(
977            parsed.flags.get("verbose").map(String::as_str),
978            Some("false")
979        );
980    }
981
982    #[test]
983    fn test_flag_negation_unknown() {
984        let cmds = vec![Command::builder("cmd")
985            .flag(Flag::builder("verbose").short('v').build().unwrap())
986            .build()
987            .unwrap()];
988        let parser = Parser::new(&cmds);
989
990        // --no-nonexistent should return UnknownFlag
991        let result = parser.parse(&["cmd", "--no-nonexistent"]);
992        assert!(
993            matches!(result, Err(ParseError::UnknownFlag(_))),
994            "expected UnknownFlag, got {:?}",
995            result
996        );
997    }
998
999    // -----------------------------------------------------------------------
1000    // Enhancement 3: Variadic arguments
1001    // -----------------------------------------------------------------------
1002
1003    #[test]
1004    fn test_variadic_argument() {
1005        let cmds = vec![Command::builder("cmd")
1006            .argument(Argument::builder("files").variadic().build().unwrap())
1007            .build()
1008            .unwrap()];
1009        let parser = Parser::new(&cmds);
1010
1011        let result = parser.parse(&["cmd", "a", "b", "c"]);
1012        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1013        let parsed = result.unwrap();
1014        let raw = parsed.args.get("files").expect("files key missing");
1015        let values: Vec<String> = serde_json::from_str(raw).expect("not valid JSON array");
1016        assert_eq!(values, vec!["a", "b", "c"]);
1017    }
1018
1019    #[test]
1020    fn test_variadic_argument_required_empty() {
1021        let cmds = vec![Command::builder("cmd")
1022            .argument(
1023                Argument::builder("files")
1024                    .required()
1025                    .variadic()
1026                    .build()
1027                    .unwrap(),
1028            )
1029            .build()
1030            .unwrap()];
1031        let parser = Parser::new(&cmds);
1032
1033        let result = parser.parse(&["cmd"]);
1034        assert!(
1035            matches!(result, Err(ParseError::MissingArgument(ref s)) if s == "files"),
1036            "expected MissingArgument(files), got {:?}",
1037            result
1038        );
1039    }
1040
1041    #[test]
1042    fn test_variadic_argument_default() {
1043        let cmds = vec![Command::builder("cmd")
1044            .argument(
1045                Argument::builder("files")
1046                    .default_value("[]")
1047                    .variadic()
1048                    .build()
1049                    .unwrap(),
1050            )
1051            .build()
1052            .unwrap()];
1053        let parser = Parser::new(&cmds);
1054
1055        // No positionals provided → default applies
1056        let result = parser.parse(&["cmd"]);
1057        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1058        let parsed = result.unwrap();
1059        assert_eq!(parsed.args.get("files").map(String::as_str), Some("[]"));
1060    }
1061
1062    // -----------------------------------------------------------------------
1063    // Flag choices and repeatable
1064    // -----------------------------------------------------------------------
1065
1066    #[test]
1067    fn test_flag_choices_valid() {
1068        let cmds = vec![Command::builder("build")
1069            .flag(
1070                Flag::builder("format")
1071                    .takes_value()
1072                    .choices(["json", "yaml", "text"])
1073                    .build()
1074                    .unwrap(),
1075            )
1076            .build()
1077            .unwrap()];
1078        let parser = Parser::new(&cmds);
1079        let parsed = parser.parse(&["build", "--format=json"]).unwrap();
1080        assert_eq!(parsed.flags["format"], "json");
1081    }
1082
1083    #[test]
1084    fn test_flag_choices_invalid() {
1085        let cmds = vec![Command::builder("build")
1086            .flag(
1087                Flag::builder("format")
1088                    .takes_value()
1089                    .choices(["json", "yaml", "text"])
1090                    .build()
1091                    .unwrap(),
1092            )
1093            .build()
1094            .unwrap()];
1095        let parser = Parser::new(&cmds);
1096        let result = parser.parse(&["build", "--format=xml"]);
1097        assert!(
1098            matches!(result, Err(ParseError::InvalidChoice { ref value, .. }) if value == "xml")
1099        );
1100    }
1101
1102    #[test]
1103    fn test_repeatable_boolean_flag() {
1104        let cmds = vec![Command::builder("run")
1105            .flag(
1106                Flag::builder("verbose")
1107                    .short('v')
1108                    .repeatable()
1109                    .build()
1110                    .unwrap(),
1111            )
1112            .build()
1113            .unwrap()];
1114        let parser = Parser::new(&cmds);
1115        // Three separate -v flags
1116        let parsed = parser.parse(&["run", "-v", "-v", "-v"]).unwrap();
1117        assert_eq!(parsed.flags["verbose"], "3");
1118    }
1119
1120    #[test]
1121    fn test_repeatable_value_flag() {
1122        let cmds = vec![Command::builder("run")
1123            .flag(
1124                Flag::builder("tag")
1125                    .takes_value()
1126                    .repeatable()
1127                    .build()
1128                    .unwrap(),
1129            )
1130            .build()
1131            .unwrap()];
1132        let parser = Parser::new(&cmds);
1133        let parsed = parser.parse(&["run", "--tag=alpha", "--tag=beta"]).unwrap();
1134        let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1135        assert_eq!(tags, vec!["alpha", "beta"]);
1136    }
1137
1138    #[test]
1139    fn test_adjacent_short_repeatable() {
1140        // -vvv should expand to three -v flags and count correctly
1141        let cmds = vec![Command::builder("run")
1142            .flag(
1143                Flag::builder("verbose")
1144                    .short('v')
1145                    .repeatable()
1146                    .build()
1147                    .unwrap(),
1148            )
1149            .build()
1150            .unwrap()];
1151        let parser = Parser::new(&cmds);
1152        let parsed = parser.parse(&["run", "-vvv"]).unwrap();
1153        assert_eq!(parsed.flags["verbose"], "3");
1154    }
1155
1156    #[test]
1157    fn test_empty_choices_build_error() {
1158        use crate::model::BuildError;
1159        let flag = Flag::builder("format")
1160            .takes_value()
1161            .choices(Vec::<String>::new())
1162            .build()
1163            .unwrap();
1164        let result = Command::builder("cmd").flag(flag).build();
1165        assert!(matches!(result, Err(BuildError::EmptyChoices(_))));
1166    }
1167
1168    #[test]
1169    fn test_env_var_fallback_basic() {
1170        let var = "ARGOT_TEST_ENVFLAG_BASIC_11111";
1171        std::env::remove_var(var);
1172        let cmds = vec![Command::builder("cmd")
1173            .flag(
1174                Flag::builder("token")
1175                    .takes_value()
1176                    .env(var)
1177                    .build()
1178                    .unwrap(),
1179            )
1180            .build()
1181            .unwrap()];
1182        let parser = Parser::new(&cmds);
1183
1184        // Not set → absent
1185        assert!(!parser.parse(&["cmd"]).unwrap().flags.contains_key("token"));
1186
1187        // Set → present
1188        std::env::set_var(var, "abc123");
1189        assert_eq!(parser.parse(&["cmd"]).unwrap().flags["token"], "abc123");
1190
1191        // CLI overrides env
1192        assert_eq!(
1193            parser.parse(&["cmd", "--token=override"]).unwrap().flags["token"],
1194            "override"
1195        );
1196        std::env::remove_var(var);
1197    }
1198
1199    #[test]
1200    fn test_env_var_fallback_with_default() {
1201        let var = "ARGOT_TEST_ENVFLAG_DEFAULT_22222";
1202        std::env::remove_var(var);
1203        let cmds = vec![Command::builder("cmd")
1204            .flag(
1205                Flag::builder("mode")
1206                    .takes_value()
1207                    .env(var)
1208                    .default_value("dev")
1209                    .build()
1210                    .unwrap(),
1211            )
1212            .build()
1213            .unwrap()];
1214        let parser = Parser::new(&cmds);
1215
1216        // No CLI, no env → default
1217        assert_eq!(parser.parse(&["cmd"]).unwrap().flags["mode"], "dev");
1218
1219        // No CLI, env set → env wins over default
1220        std::env::set_var(var, "prod");
1221        assert_eq!(parser.parse(&["cmd"]).unwrap().flags["mode"], "prod");
1222        std::env::remove_var(var);
1223    }
1224
1225    #[test]
1226    fn test_env_var_satisfies_required_flag() {
1227        let var = "ARGOT_TEST_ENVFLAG_REQUIRED_33333";
1228        std::env::remove_var(var);
1229        let cmds = vec![Command::builder("cmd")
1230            .flag(
1231                Flag::builder("token")
1232                    .takes_value()
1233                    .required()
1234                    .env(var)
1235                    .build()
1236                    .unwrap(),
1237            )
1238            .build()
1239            .unwrap()];
1240        let parser = Parser::new(&cmds);
1241
1242        // Required + env set → OK
1243        std::env::set_var(var, "secret");
1244        assert_eq!(parser.parse(&["cmd"]).unwrap().flags["token"], "secret");
1245
1246        // Required + env absent → MissingFlag
1247        std::env::remove_var(var);
1248        assert!(matches!(
1249            parser.parse(&["cmd"]),
1250            Err(ParseError::MissingFlag(_))
1251        ));
1252    }
1253
1254    #[test]
1255    fn test_env_var_validates_choices() {
1256        let var = "ARGOT_TEST_ENVFLAG_CHOICES_44444";
1257        std::env::remove_var(var);
1258        let cmds = vec![Command::builder("cmd")
1259            .flag(
1260                Flag::builder("env_name")
1261                    .takes_value()
1262                    .choices(["prod", "staging"])
1263                    .env(var)
1264                    .build()
1265                    .unwrap(),
1266            )
1267            .build()
1268            .unwrap()];
1269        let parser = Parser::new(&cmds);
1270
1271        std::env::set_var(var, "staging");
1272        assert_eq!(parser.parse(&["cmd"]).unwrap().flags["env_name"], "staging");
1273
1274        std::env::set_var(var, "local");
1275        assert!(matches!(
1276            parser.parse(&["cmd"]),
1277            Err(ParseError::InvalidChoice { .. })
1278        ));
1279
1280        std::env::remove_var(var);
1281    }
1282
1283    #[test]
1284    fn test_exclusive_flags_one_set_ok() {
1285        let cmd = Command::builder("export")
1286            .flag(Flag::builder("json").build().unwrap())
1287            .flag(Flag::builder("yaml").build().unwrap())
1288            .exclusive(["json", "yaml"])
1289            .build()
1290            .unwrap();
1291        let cmds = vec![cmd];
1292        let parser = Parser::new(&cmds);
1293        let parsed = parser.parse(&["export", "--json"]).unwrap();
1294        assert_eq!(parsed.flags["json"], "true");
1295    }
1296
1297    #[test]
1298    fn test_exclusive_flags_two_set_errors() {
1299        let cmd = Command::builder("export")
1300            .flag(Flag::builder("json").build().unwrap())
1301            .flag(Flag::builder("yaml").build().unwrap())
1302            .exclusive(["json", "yaml"])
1303            .build()
1304            .unwrap();
1305        let cmds = vec![cmd];
1306        let parser = Parser::new(&cmds);
1307        assert!(matches!(
1308            parser.parse(&["export", "--json", "--yaml"]),
1309            Err(ParseError::MutuallyExclusive { .. })
1310        ));
1311    }
1312
1313    #[test]
1314    fn test_exclusive_neither_set_ok() {
1315        let cmd = Command::builder("export")
1316            .flag(Flag::builder("json").build().unwrap())
1317            .flag(Flag::builder("yaml").build().unwrap())
1318            .exclusive(["json", "yaml"])
1319            .build()
1320            .unwrap();
1321        let cmds = vec![cmd];
1322        let parser = Parser::new(&cmds);
1323        assert!(parser.parse(&["export"]).is_ok());
1324    }
1325
1326    #[test]
1327    fn test_exclusive_group_unknown_flag_build_error() {
1328        use crate::model::BuildError;
1329        let result = Command::builder("cmd")
1330            .flag(Flag::builder("json").build().unwrap())
1331            .exclusive(["json", "nonexistent"])
1332            .build();
1333        assert!(matches!(
1334            result,
1335            Err(BuildError::ExclusiveGroupUnknownFlag(_))
1336        ));
1337    }
1338
1339    #[test]
1340    fn test_exclusive_group_too_small_build_error() {
1341        use crate::model::BuildError;
1342        let result = Command::builder("cmd")
1343            .flag(Flag::builder("json").build().unwrap())
1344            .exclusive(["json"])
1345            .build();
1346        assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
1347    }
1348
1349    // -----------------------------------------------------------------------
1350    // Additional edge-case tests for improved coverage
1351    // -----------------------------------------------------------------------
1352
1353    #[test]
1354    fn test_subcommand_walk_breaks_on_flag_token() {
1355        // If the next token after entering a parent command is a flag (not a word),
1356        // the subcommand loop should break and treat remaining tokens as flags.
1357        let cmds = vec![Command::builder("git")
1358            .subcommand(Command::builder("status").build().unwrap())
1359            .flag(Flag::builder("verbose").build().unwrap())
1360            .build()
1361            .unwrap()];
1362        let parser = Parser::new(&cmds);
1363        // "--verbose" comes before any subcommand token — should break the loop
1364        // and bind "--verbose" as a flag on "git" (not enter subcommand)
1365        let result = parser.parse(&["git", "--verbose"]);
1366        assert!(result.is_ok(), "expected Ok, got {:?}", result);
1367        let parsed = result.unwrap();
1368        assert_eq!(parsed.command.canonical, "git");
1369        assert_eq!(
1370            parsed.flags.get("verbose").map(String::as_str),
1371            Some("true")
1372        );
1373    }
1374
1375    #[test]
1376    fn test_no_negation_with_value_is_unknown_flag() {
1377        // --no-verbose=true is invalid (negation cannot carry a value)
1378        let cmds = vec![Command::builder("cmd")
1379            .flag(Flag::builder("verbose").build().unwrap())
1380            .build()
1381            .unwrap()];
1382        let parser = Parser::new(&cmds);
1383        let result = parser.parse(&["cmd", "--no-verbose=true"]);
1384        assert!(
1385            matches!(result, Err(ParseError::UnknownFlag(_))),
1386            "expected UnknownFlag for --no-<name>=value, got {:?}",
1387            result
1388        );
1389    }
1390
1391    #[test]
1392    fn test_long_flag_missing_value_non_word_token() {
1393        // --output followed by another flag (not a word) → FlagMissingValue
1394        let cmds = vec![Command::builder("cmd")
1395            .flag(Flag::builder("output").takes_value().build().unwrap())
1396            .flag(Flag::builder("verbose").build().unwrap())
1397            .build()
1398            .unwrap()];
1399        let parser = Parser::new(&cmds);
1400        let result = parser.parse(&["cmd", "--output", "--verbose"]);
1401        assert!(
1402            matches!(result, Err(ParseError::FlagMissingValue { .. })),
1403            "expected FlagMissingValue, got {:?}",
1404            result
1405        );
1406    }
1407
1408    #[test]
1409    fn test_short_flag_takes_value_missing_value() {
1410        // -o with takes_value but no next token → FlagMissingValue
1411        let cmds = vec![Command::builder("cmd")
1412            .flag(
1413                Flag::builder("output")
1414                    .short('o')
1415                    .takes_value()
1416                    .build()
1417                    .unwrap(),
1418            )
1419            .build()
1420            .unwrap()];
1421        let parser = Parser::new(&cmds);
1422        // -o at end of argv with no value
1423        let result = parser.parse(&["cmd", "-o"]);
1424        assert!(
1425            matches!(result, Err(ParseError::FlagMissingValue { .. })),
1426            "expected FlagMissingValue for short flag with no value, got {:?}",
1427            result
1428        );
1429    }
1430
1431    #[test]
1432    fn test_short_flag_takes_value_invalid_choice() {
1433        let cmds = vec![Command::builder("cmd")
1434            .flag(
1435                Flag::builder("format")
1436                    .short('f')
1437                    .takes_value()
1438                    .choices(["json", "yaml"])
1439                    .build()
1440                    .unwrap(),
1441            )
1442            .build()
1443            .unwrap()];
1444        let parser = Parser::new(&cmds);
1445        let result = parser.parse(&["cmd", "-f", "xml"]);
1446        assert!(
1447            matches!(result, Err(ParseError::InvalidChoice { .. })),
1448            "expected InvalidChoice for invalid short flag value, got {:?}",
1449            result
1450        );
1451    }
1452
1453    #[test]
1454    fn test_short_flag_repeatable_value() {
1455        let cmds = vec![Command::builder("run")
1456            .flag(
1457                Flag::builder("tag")
1458                    .short('t')
1459                    .takes_value()
1460                    .repeatable()
1461                    .build()
1462                    .unwrap(),
1463            )
1464            .build()
1465            .unwrap()];
1466        let parser = Parser::new(&cmds);
1467        // -t alpha -t beta via short form
1468        let parsed = parser.parse(&["run", "-t", "alpha", "-t", "beta"]).unwrap();
1469        let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1470        assert_eq!(tags, vec!["alpha", "beta"]);
1471    }
1472
1473    #[test]
1474    fn test_optional_arg_with_default_applied() {
1475        // Non-required arg with default: if not supplied, default is used
1476        let cmds = vec![Command::builder("serve")
1477            .argument(
1478                Argument::builder("host")
1479                    .default_value("localhost")
1480                    .build()
1481                    .unwrap(),
1482            )
1483            .build()
1484            .unwrap()];
1485        let parser = Parser::new(&cmds);
1486        let parsed = parser.parse(&["serve"]).unwrap();
1487        assert_eq!(
1488            parsed.args.get("host").map(String::as_str),
1489            Some("localhost")
1490        );
1491    }
1492
1493    #[test]
1494    fn test_long_flag_repeatable_boolean() {
1495        // Repeatable boolean flag via long form --verbose multiple times
1496        let cmds = vec![Command::builder("run")
1497            .flag(Flag::builder("verbose").repeatable().build().unwrap())
1498            .build()
1499            .unwrap()];
1500        let parser = Parser::new(&cmds);
1501        let parsed = parser
1502            .parse(&["run", "--verbose", "--verbose", "--verbose"])
1503            .unwrap();
1504        assert_eq!(parsed.flags["verbose"], "3");
1505    }
1506
1507    #[test]
1508    fn test_long_flag_repeatable_value() {
1509        // Repeatable value flag accumulates into JSON array via long form
1510        let cmds = vec![Command::builder("run")
1511            .flag(
1512                Flag::builder("tag")
1513                    .takes_value()
1514                    .repeatable()
1515                    .build()
1516                    .unwrap(),
1517            )
1518            .build()
1519            .unwrap()];
1520        let parser = Parser::new(&cmds);
1521        // Second occurrence appends to the existing JSON array
1522        let parsed = parser
1523            .parse(&["run", "--tag=first", "--tag=second"])
1524            .unwrap();
1525        let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1526        assert_eq!(tags, vec!["first", "second"]);
1527    }
1528
1529    #[test]
1530    fn test_unknown_short_flag_error() {
1531        let cmds = vec![Command::builder("cmd").build().unwrap()];
1532        let parser = Parser::new(&cmds);
1533        let result = parser.parse(&["cmd", "-z"]);
1534        assert!(
1535            matches!(result, Err(ParseError::UnknownFlag(_))),
1536            "expected UnknownFlag for unknown short flag, got {:?}",
1537            result
1538        );
1539    }
1540}
1541
1542#[cfg(test)]
1543mod typed_getter_tests {
1544    use super::*;
1545    use crate::model::{Argument, Command, Flag};
1546
1547    #[test]
1548    fn test_parsed_command_typed_getters() {
1549        let cmd = Command::builder("run")
1550            .argument(Argument::builder("script").required().build().unwrap())
1551            .flag(Flag::builder("verbose").short('v').build().unwrap())
1552            .flag(
1553                Flag::builder("output")
1554                    .takes_value()
1555                    .default_value("text")
1556                    .build()
1557                    .unwrap(),
1558            )
1559            .build()
1560            .unwrap();
1561        let cmds = vec![cmd];
1562        let parser = Parser::new(&cmds);
1563        let parsed = parser.parse(&["run", "myscript", "-v"]).unwrap();
1564
1565        assert_eq!(parsed.arg("script"), Some("myscript"));
1566        assert_eq!(parsed.arg("missing"), None);
1567        assert_eq!(parsed.flag("verbose"), Some("true"));
1568        assert_eq!(parsed.flag("output"), Some("text")); // default
1569        assert!(parsed.flag_bool("verbose"));
1570        assert!(!parsed.flag_bool("output")); // not a boolean flag
1571        assert_eq!(parsed.flag_count("verbose"), 1);
1572        assert_eq!(parsed.flag_count("missing"), 0);
1573        assert_eq!(parsed.flag_values("output"), vec!["text"]);
1574        assert!(parsed.flag_values("missing").is_empty());
1575    }
1576}
1577
1578#[cfg(test)]
1579mod has_flag_tests {
1580    use super::*;
1581    use crate::model::{Command, Flag};
1582
1583    #[test]
1584    fn test_has_flag() {
1585        let cmd = Command::builder("run")
1586            .flag(Flag::builder("verbose").build().unwrap())
1587            .flag(
1588                Flag::builder("output")
1589                    .takes_value()
1590                    .default_value("text")
1591                    .build()
1592                    .unwrap(),
1593            )
1594            .build()
1595            .unwrap();
1596        let cmds = vec![cmd];
1597        let parser = Parser::new(&cmds);
1598        let parsed = parser.parse(&["run", "--verbose"]).unwrap();
1599        assert!(parsed.has_flag("verbose"));
1600        assert!(parsed.has_flag("output")); // present via default
1601        assert!(!parsed.has_flag("nonexistent"));
1602    }
1603}
1604
1605#[cfg(test)]
1606mod coercion_tests {
1607    use super::*;
1608    use crate::model::{Argument, Command, Flag};
1609
1610    #[test]
1611    fn test_arg_as_u32() {
1612        let cmd = Command::builder("resize")
1613            .argument(Argument::builder("width").required().build().unwrap())
1614            .build()
1615            .unwrap();
1616        let cmds = vec![cmd];
1617        let parsed = Parser::new(&cmds).parse(&["resize", "1920"]).unwrap();
1618        let w: u32 = parsed.arg_as("width").unwrap().unwrap();
1619        assert_eq!(w, 1920);
1620    }
1621
1622    #[test]
1623    fn test_arg_as_parse_error() {
1624        let cmd = Command::builder("cmd")
1625            .argument(Argument::builder("n").required().build().unwrap())
1626            .build()
1627            .unwrap();
1628        let cmds = vec![cmd];
1629        let parsed = Parser::new(&cmds).parse(&["cmd", "notanumber"]).unwrap();
1630        assert!(parsed.arg_as::<u32>("n").unwrap().is_err());
1631    }
1632
1633    #[test]
1634    fn test_arg_as_absent() {
1635        let cmd = Command::builder("cmd").build().unwrap();
1636        let cmds = vec![cmd];
1637        let parsed = Parser::new(&cmds).parse(&["cmd"]).unwrap();
1638        assert!(parsed.arg_as::<u32>("missing").is_none());
1639    }
1640
1641    #[test]
1642    fn test_flag_as_u16() {
1643        let cmd = Command::builder("serve")
1644            .flag(
1645                Flag::builder("port")
1646                    .takes_value()
1647                    .default_value("8080")
1648                    .build()
1649                    .unwrap(),
1650            )
1651            .build()
1652            .unwrap();
1653        let cmds = vec![cmd];
1654        let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
1655        let port: u16 = parsed.flag_as("port").unwrap().unwrap();
1656        assert_eq!(port, 8080);
1657    }
1658
1659    #[test]
1660    fn test_flag_as_bool() {
1661        let cmd = Command::builder("run")
1662            .flag(Flag::builder("verbose").build().unwrap())
1663            .build()
1664            .unwrap();
1665        let cmds = vec![cmd];
1666        let parsed = Parser::new(&cmds).parse(&["run", "--verbose"]).unwrap();
1667        let v: bool = parsed.flag_as("verbose").unwrap().unwrap();
1668        assert!(v);
1669    }
1670
1671    #[test]
1672    fn test_arg_as_or_default() {
1673        let cmd = Command::builder("run")
1674            .argument(Argument::builder("count").build().unwrap())
1675            .build()
1676            .unwrap();
1677        let cmds = vec![cmd];
1678        let parsed = Parser::new(&cmds).parse(&["run"]).unwrap();
1679        assert_eq!(parsed.arg_as_or("count", 42u32), 42u32);
1680    }
1681
1682    #[test]
1683    fn test_flag_as_or_default() {
1684        let cmd = Command::builder("serve")
1685            .flag(Flag::builder("workers").takes_value().build().unwrap())
1686            .build()
1687            .unwrap();
1688        let cmds = vec![cmd];
1689        let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
1690        assert_eq!(parsed.flag_as_or("workers", 4u32), 4u32);
1691    }
1692}