shovel/
opts.rs

1//! Optional argument parsing and specification.
2//!
3//! This module provides the [`Opts`] trait and related types for defining and parsing
4//! optional command-line arguments (flags and options). Options are parameters that
5//! can appear anywhere in the command line and are typically prefixed with `-` or `--`.
6//!
7//! # Core Types
8//!
9//! ## [`Opts`] - Optional Argument Trait
10//!
11//! The main trait for defining optional arguments. Typically implemented using
12//! the `#[derive(Opts)]` macro for automatic parsing and validation.
13//!
14//! ## [`Opt`] - Option Specification
15//!
16//! Describes an individual option with its properties:
17//! - Short and long forms (`-v`, `--verbose`)
18//! - Value requirements (flag vs. valued option)
19//! - Type information and parsing rules
20//! - Help text and descriptions
21//!
22//! ## [`RawOpts<'a, S>`] - Raw Option Container
23//!
24//! Internal type that holds unparsed command-line options and manages parsing state.
25//!
26//! ## [`RawOptsIter<'a, S>`] - Option Iterator
27//!
28//! Iterator over raw command-line options for processing.
29//!
30//! # Option Types
31//!
32//! ## Boolean Flags
33//! Simple on/off switches:
34//! ```
35//! # use shovel::*;
36//! #[derive(Opts)]
37//! struct MyOpts {
38//!     /// Enable verbose output
39//!     #[short = 'v']
40//!     verbose: bool,
41//! }
42//! ```
43//!
44//! ## Valued Options
45//! Options that accept values:
46//! ```
47//! # use shovel::*;
48//!
49//! #[derive(Opts)]
50//! struct MyOpts {
51//!     /// Output file path
52//!     #[short = 'o']
53//!     output: Option<String>,
54//!
55//!     /// Number of threads
56//!     #[short = 'j']
57//!     jobs: Option<u32>,
58//!
59//!     /// Tags
60//!     tags: Option<Vec<String>>,
61//! }
62//! ```
63//!
64//! ## Required Options
65//! Options that must be provided:
66//! ```
67//! # use shovel::*;
68//!
69//! #[derive(Opts)]
70//! struct MyOpts {
71//!     /// Configuration file (required)
72//!     #[short = 'c']
73//!     config: String,
74//!
75//!     /// Tags (required)
76//!     tags: Vec<String>,
77//! }
78//! ```
79//!
80//! ## Multiple Values
81//! Options that can be specified multiple times:
82//! ```
83//! # use shovel::*;
84//! #[derive(Opts)]
85//! struct MyOpts {
86//!     /// Include directories
87//!     #[short = 'I']
88//!     #[format = "<DIR>,..."]
89//!     include: String,
90//! }
91//! ```
92//!
93//! # Option Syntax
94//!
95//! ## Short Options
96//! Single character options prefixed with `-`:
97//! ```bash
98//! myapp -v -o output.txt -j 4
99//! myapp -vj 4 -o output.txt    # Short options can be combined
100//! ```
101//!
102//! ## Long Options
103//! Multi-character options prefixed with `--`:
104//! ```bash
105//! myapp --verbose --output output.txt --jobs 4
106//! myapp --output=output.txt    # Values can use = syntax
107//! ```
108//!
109//! ## Mixed Usage
110//! Short and long forms can be used interchangeably:
111//! ```bash
112//! myapp -v --output output.txt -j 4
113//! ```
114//!
115//! # Attribute Syntax
116//!
117//! The `#[derive(Opts)]` macro supports several attributes:
118//!
119//! ## `#[short = 'c']`
120//! Defines the short form of the option:
121//! ```
122//! # use shovel::*;
123//! #[derive(Opts)]
124//! struct MyOpts {
125//!     #[short = 'v']
126//!     verbose: bool,
127//! }
128//! ```
129//!
130//! ## `#[short]`
131//! Uses the first character of the field name:
132//! ```
133//! # use shovel::*;
134//! #[derive(Opts)]
135//! struct MyOpts {
136//!     #[short]  // Becomes -v
137//!     verbose: bool,
138//! }
139//! ```
140//!
141//!
142//! # Built-in Options
143//!
144//! Shovel automatically provides several built-in options:
145//!
146//! ## Help Options
147//! - `-h, --help` - Show help information
148//! - Available on all commands automatically
149//!
150//! ## Version Options
151//! - `-V, --version` - Show version information
152//! - Available on the root application
153//!
154//! ## Completion Options (when enabled)
155//! - `--completion <SHELL>` - Generate completion scripts
156//! - Available when the `completion` feature is enabled
157//!
158//! # Parsing Behavior
159//!
160//! - **Type Safety**: Options are parsed to their target types with validation
161//! - **Default Values**: `Option<T>` fields default to `None`, `bool` fields to `false`
162//! - **Error Handling**: Invalid values or unknown options result in parse errors
163//! - **Flexible Syntax**: Supports various command-line conventions
164//!
165//! # Global vs. Local Options
166//!
167//! ## Local Options
168//! Specific to individual commands:
169//! ```rust
170//! # use shovel::*;
171//! #[derive(Opts)]
172//! struct BuildOpts {
173//!     #[short = 'r']
174//!     release: bool,
175//! }
176//! ```
177//!
178//! ## Global Options
179//!
180//! Available to all commands in the application:
181//!
182//! ```rust
183//! use shovel::*;
184//!
185//! #[derive(Opts)]
186//! pub struct GlobalOpts {
187//!     /// Enable verbose output
188//!     #[short = 'v']
189//!     pub verbose: bool,
190//!     /// Set log level
191//!     #[short = 'l']
192//!     pub log_level: Option<String>,
193//! }
194//!
195//!
196//! # fn main() {
197//! let action: Action<(), (), GlobalOpts, ()> = Action::global_opts(|global_opts: GlobalOpts| {
198//!     if global_opts.verbose {
199//!         println!("Verbose mode enabled");
200//!     }
201//!     if let Some(level) = global_opts.log_level {
202//!         println!("Setting log level to: {}", level);
203//!         // Here you would typically set up your logging framework
204//!     }
205//! });
206//! # }
207//! ```
208//!
209//! # Examples
210//!
211//! ## Compiler-like Options
212//! ```
213//! # use shovel::*;
214//! #[derive(Opts)]
215//! struct CompilerOpts {
216//!     /// Optimization level
217//!     #[short = 'O']
218//!     optimize: Option<u32>,
219//!
220//!     /// Enable warnings
221//!     #[short = 'W']
222//!     warnings: bool,
223//!
224//!     /// Output file
225//!     #[short = 'o']
226//!     output: Option<String>,
227//!
228//!     /// Include directories
229//!     #[short = 'I']
230//!     #[format = "<DIR>,..."]
231//!     include: String,
232//! }
233//!
234//! let action: Action<CompilerOpts, (), (), ()> = Action::opts(|opts: CompilerOpts| {
235//!     if opts.warnings {
236//!         println!("Warnings enabled");
237//!     }
238//!
239//!     if let Some(level) = opts.optimize {
240//!         println!("Optimization level: {}", level);
241//!     }
242//!
243//!     for dir in opts.include.split(',') {
244//!         println!("Include directory: {}", dir);
245//!     }
246//! });
247//! ```
248//!
249//! ## Server Configuration Options
250//! ```
251//! # use shovel::*;
252//! #[derive(Opts)]
253//! struct ServerOpts {
254//!     /// Server port
255//!     #[short = 'p']
256//!     port: Option<u16>,
257//!
258//!     /// Bind address
259//!     #[short = 'b']
260//!     bind: Option<String>,
261//!
262//!     /// Enable TLS
263//!     #[short = 's']
264//!     secure: bool,
265//!
266//!     /// Configuration file
267//!     #[short = 'c']
268//!     config: Option<String>,
269//! }
270//! ```
271//!
272//! See also: [`Args`](crate::Args), [`Action`](crate::Action), [`Context`](crate::Context)
273
274use core::cell::{RefCell, RefMut};
275use core::marker::PhantomData;
276
277use crate::ParseError;
278#[cfg(feature = "completion")]
279use crate::Shell;
280
281/// Represents a collection of optional arguments for a command.
282///
283/// This trait should be implemented by user-defined types that represent
284/// the optional arguments of a command. Each field of the implementing type
285/// corresponds to one optional argument.
286///
287/// - If a field is of type [`core::option::Option`], the argument is optional.
288/// - Otherwise, the argument is required.
289///
290/// # Implementing
291///
292/// This trait can be derived using the `#[derive(Opts)]` attribute, which is
293/// the recommended approach for most use cases. However, manual implementation
294/// is possible for more complex scenarios.
295///
296/// # Example
297///
298/// ```no_run
299/// use shovel::Opts;
300///
301/// #[derive(Opts)]
302/// pub struct TickOpts {
303///     /// The delay time (in seconds)
304///     #[short = 'd']
305///     pub delay: u64, // This field is required
306///     /// Number of times
307///     #[short = 'n']
308///     pub number: Option<u32>, // The field is optional.
309/// }
310/// ```
311///
312/// # Manual Implementation Example
313///
314/// For advanced use cases, you can manually implement the `Opts` trait:
315///
316/// ```no_run
317/// use shovel::{Opt, Opts, ParseError, RawOpts};
318///
319/// pub struct TickOpts {
320///     pub delay: u64,
321///     pub number: Option<u32>,
322/// }
323///
324/// impl Opts for TickOpts {
325///     fn parse<'a, 'b: 'a>(
326///         opts: &'a RawOpts<'a, 'b>,
327///     ) -> Result<(Self, usize), ParseError<'b>> {
328///         let mut delay = None;
329///         let mut number = None;
330///
331///         // Parse each option
332///         for opt in opts {
333///             match opt.0 {
334///                 "delay" | "d" => {
335///                     let value = opt.1.ok_or(ParseError::MissingValueForOption { name: "delay" })?;
336///                     delay = Some(value.parse::<u64>().map_err(|_| {
337///                         ParseError::InvalidOptionValue {
338///                             name: "delay",
339///                             value,
340///                             expected: "a positive number",
341///                         }
342///                     })?);
343///                 }
344///                 "number" | "n" => {
345///                     let value = opt.1.ok_or(ParseError::MissingValueForOption { name: "number" })?;
346///                     number = Some(value.parse::<u32>().map_err(|_| {
347///                         ParseError::InvalidOptionValue {
348///                             name: "number",
349///                             value,
350///                             expected: "a positive integer",
351///                         }
352///                     })?);
353///                 }
354///                 unknown => return Err(ParseError::UnknownOption(unknown)),
355///             }
356///         }
357///
358///         // Validate required options
359///         let delay = delay.ok_or(ParseError::MissingRequiredOption { name: "delay" })?;
360///
361///         Ok((Self { delay, number }, opts.len_consumed()))
362///     }
363///
364///     fn specs() -> &'static [Opt] {
365///         &[
366///             Opt {
367///                 name: "delay",
368///                 ty: "u64",
369///                 required: true,
370///                 flag: false,
371///                 description: "The delay time (in seconds)",
372///                 short: Some('d'),
373///                 format: "-d, --delay <SECONDS>",
374///             },
375///             Opt {
376///                 name: "number",
377///                 ty: "u32",
378///                 required: false,
379///                 flag: false,
380///                 description: "Number of times to repeat",
381///                 short: Some('n'),
382///                 format: "-n, --number <COUNT>",
383///             },
384///         ]
385///     }
386/// }
387/// ```
388pub trait Opts: Sized + 'static {
389    /// Parses the raw optional arguments into this type.
390    ///
391    /// # Arguments
392    ///
393    /// * `opts` - The raw optional arguments to parse.
394    ///
395    /// # Returns
396    ///
397    /// Returns a `Result` containing:
398    /// - On success: A tuple with the parsed `Opts` instance and the number of consumed arguments.
399    /// - On failure: A `ParseError` describing the parsing failure.
400    ///
401    /// # Errors
402    ///
403    /// This function may return a `ParseError` in the following cases:
404    /// - If a required option is missing
405    /// - If an option value is invalid or cannot be parsed
406    /// - If an unknown option is encountered
407    fn parse<'a, 'b: 'a>(
408        opts: &'a RawOpts<'a, 'b>,
409    ) -> Result<(Self, usize), ParseError<'b>>;
410
411    /// Provides specifications for all optional arguments.
412    ///
413    /// # Returns
414    ///
415    /// A static slice of `Opt` structs, each describing an optional argument.
416    fn specs() -> &'static [Opt];
417}
418
419/// Specifies the properties of an individual optional argument.
420#[derive(Debug)]
421pub struct Opt {
422    /// The full name of the optional argument.
423    pub name: &'static str,
424    /// The expected type of the argument's value.
425    pub ty: &'static str,
426    /// Indicates whether the argument is required.
427    pub required: bool,
428    /// Indicates whether the argument is a flag (boolean, no value).
429    pub flag: bool,
430    /// A brief description of the argument's purpose.
431    pub description: &'static str,
432    /// The optional short (single-character) version of the argument.
433    pub short: Option<char>,
434    /// The formatted representation used in help messages.
435    pub format: &'static str,
436}
437
438/// An iterator over the raw optional arguments.
439///
440/// This struct is used internally to iterate over the raw arguments
441/// during the parsing process.
442#[derive(Debug)]
443pub struct RawOptsIter<'a, 'b: 'a> {
444    args: &'b [&'b str],
445    index: RefMut<'a, usize>,
446    multi_flags_index: Option<usize>,
447    specs: &'static [Opt],
448    is_end: RefMut<'a, bool>,
449}
450
451/// Represents the raw optional arguments of a command.
452///
453/// This struct holds the unparsed arguments and provides methods
454/// to iterate over them during the parsing process.
455#[derive(Debug)]
456pub struct RawOpts<'a, 'b: 'a> {
457    args: &'b [&'b str],
458    index: RefCell<usize>,
459    specs: &'static [Opt],
460    is_end: RefCell<bool>,
461    _lifetime: PhantomData<&'a ()>,
462}
463
464pub(crate) struct HelpOpt;
465
466pub(crate) struct VersionOpt;
467
468#[cfg(feature = "completion")]
469pub(crate) struct CompletionOpt;
470
471const BUILTIN_OPTIONS: &[&str] = &[
472    "help",
473    "h",
474    "version",
475    "V",
476    #[cfg(feature = "completion")]
477    "completion",
478];
479
480impl Opts for () {
481    fn parse<'a, 'b: 'a>(
482        opts: &'a RawOpts<'a, 'b>,
483    ) -> Result<(Self, usize), ParseError<'b>> {
484        for opt in opts {
485            if !BUILTIN_OPTIONS.contains(&opt.0) {
486                Err(ParseError::UnknownOption(opt.0))?;
487            }
488        }
489        Ok(((), opts.len_consumed()))
490    }
491
492    fn specs() -> &'static [Opt] {
493        &[]
494    }
495}
496
497pub(crate) static BUILTIN_OPTS: &[Opt] = &[
498    Opt {
499        name: "help",
500        ty: "bool",
501        required: false,
502        flag: true,
503        description: "Show this help",
504        short: Some('h'),
505        format: "-h, --help",
506    },
507    Opt {
508        name: "version",
509        ty: "bool",
510        required: false,
511        flag: true,
512        description: "Print version information and quit",
513        short: Some('V'),
514        format: "-V, --version",
515    },
516    #[cfg(feature = "completion")]
517    Opt {
518        name: "completion",
519        ty: "Shell:<bash|zsh|fish>",
520        required: false,
521        flag: false,
522        description: "Generate shell completion script",
523        short: None,
524        format: "    --completion",
525    },
526];
527
528impl HelpOpt {
529    pub(crate) fn parse(opts: RawOpts<'_, '_>) -> bool {
530        let mut help = false;
531        for opt in &opts {
532            if opt.0 == "help" || opt.0 == "h" {
533                help = true;
534                break;
535            }
536        }
537        help
538    }
539}
540
541impl VersionOpt {
542    pub(crate) fn parse(opts: RawOpts<'_, '_>) -> bool {
543        let mut version = false;
544        for opt in &opts {
545            if opt.0 == "version" || opt.0 == "V" {
546                version = true;
547                break;
548            }
549        }
550        version
551    }
552}
553
554#[cfg(feature = "completion")]
555impl CompletionOpt {
556    pub(crate) fn parse(
557        opts: RawOpts<'_, '_>,
558    ) -> Option<Result<Shell, ParseError<'static>>> {
559        for opt in &opts {
560            if opt.0 == "completion" {
561                if let Some(s) = opt.1 {
562                    return Some(s.parse().map_err(|_| {
563                        ParseError::InvalidOptionValue {
564                            name: "completion",
565                            value: "Unknown shell",
566                            expected: "a valid shell name",
567                        }
568                    }));
569                }
570                return Some(Err(ParseError::MissingValueForOption {
571                    name: "completion",
572                }));
573            }
574        }
575        None
576    }
577}
578
579impl<'a, 'b: 'a> RawOpts<'a, 'b> {
580    pub(crate) fn new(args: &'b [&'b str], specs: &'static [Opt]) -> Self {
581        Self {
582            args,
583            index: RefCell::new(0),
584            specs,
585            is_end: RefCell::new(false),
586            _lifetime: PhantomData,
587        }
588    }
589
590    /// Returns an iterator over the raw optional arguments.
591    pub fn iter(&'a self) -> RawOptsIter<'a, 'b> {
592        RawOptsIter::new(self)
593    }
594
595    /// Returns the number of raw arguments that has been consumed.
596    pub fn len_consumed(&self) -> usize {
597        debug_assert!(*self.is_end.borrow());
598        *self.index.borrow()
599    }
600}
601
602impl<'a, 'b: 'a> IntoIterator for &'a RawOpts<'a, 'b> {
603    type Item = (&'b str, Option<&'b str>);
604    type IntoIter = RawOptsIter<'a, 'b>;
605
606    fn into_iter(self) -> Self::IntoIter {
607        self.iter()
608    }
609}
610
611impl<'a, 'b: 'a> RawOptsIter<'a, 'b> {
612    fn new(raw_opts: &'a RawOpts<'a, 'b>) -> Self {
613        Self {
614            args: raw_opts.args,
615            index: raw_opts.index.borrow_mut(),
616            multi_flags_index: None,
617            specs: raw_opts.specs,
618            is_end: raw_opts.is_end.borrow_mut(),
619        }
620    }
621
622    fn find_spec(&self, name: &str) -> Option<&'static Opt> {
623        for spec in self.specs {
624            if name == spec.name {
625                return Some(spec);
626            }
627            if let Some(s) = spec.short
628                && name.len() == s.len_utf8()
629                && name.starts_with(s)
630            {
631                return Some(spec);
632            }
633        }
634        None
635    }
636}
637
638impl<'a, 'b: 'a> Iterator for RawOptsIter<'a, 'b> {
639    type Item = (&'b str, Option<&'b str>);
640
641    fn next(&mut self) -> Option<Self::Item> {
642        if *self.is_end || *self.index >= self.args.len() {
643            *self.is_end = true;
644            return None;
645        }
646
647        let arg = self.args[*self.index];
648        if !arg.starts_with('-') {
649            *self.is_end = true;
650            return None;
651        }
652
653        // End of options
654        if arg == "--" {
655            *self.index += 1;
656            *self.is_end = true;
657            return None;
658        }
659
660        if !arg.is_ascii() && arg.starts_with('-') && !arg.starts_with("--") {
661            *self.index += 1;
662            return Some((arg.strip_prefix('-').unwrap_or(arg), None));
663        }
664
665        let name = if let Some(name) = arg.strip_prefix("--") {
666            // Handle --name=value syntax
667            if let Some((name, value)) = name.split_once('=') {
668                *self.index += 1;
669                return Some((name, Some(value)));
670            }
671            name
672        } else {
673            if arg.len() > 2 {
674                // Handle -n=value syntax for short options (only single character)
675                if let Some(eq_pos) = arg.find('=')
676                    && eq_pos == 2
677                {
678                    let name = &arg[1..2];
679                    let value = &arg[eq_pos + 1..];
680                    *self.index += 1;
681                    return Some((name, Some(value)));
682                }
683
684                let index = self.multi_flags_index.unwrap_or(0);
685                if index < arg.len() - 2 {
686                    self.multi_flags_index = Some(index + 1);
687                    return Some((&arg[index + 1..=index + 1], None));
688                }
689            }
690            self.multi_flags_index = None;
691            &arg[arg.len() - 1..]
692        };
693
694        *self.index += 1;
695
696        match self.find_spec(name) {
697            None => {
698                if !BUILTIN_OPTIONS.contains(&name) {
699                    *self.is_end = true;
700                }
701                Some((name, None))
702            }
703            Some(spec) => {
704                if *self.index >= self.args.len() {
705                    return Some((name, None));
706                }
707
708                let value = self.args[*self.index];
709                if spec.flag || value.starts_with('-') {
710                    return Some((name, None));
711                }
712
713                *self.index += 1;
714                Some((name, Some(value)))
715            }
716        }
717    }
718}
719
720/// A simple and fast config option parser.
721///
722/// # Example
723///
724/// ```edition2021
725/// use shovel::*;
726///
727/// # fn main() {
728/// App {
729///     name: "config",
730///     description: "An example of ConfigOpts",
731///     version: "0.1.0",
732///     authors: ["Author Name <author@example.com>"],
733///     copyright: "(c) 2026 Company Name",
734///     action: Action::opts(|opts: ConfigOpts| {
735///         match opts.config {
736///             Some(p) => println!("The path of config is '{}'", p.display()),
737///             None => println!("No config provided")
738///         }
739///     }),
740/// }
741/// .run()
742/// .expect("Failed to run application");
743/// # }
744/// ```
745#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
746#[cfg(feature = "std")]
747#[derive(Debug)]
748pub struct ConfigOpts {
749    /// The path of config.
750    pub config: Option<std::path::PathBuf>,
751}
752
753#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
754#[cfg(feature = "std")]
755impl Opts for ConfigOpts {
756    fn parse<'a, 'b: 'a>(
757        opts: &'a RawOpts<'a, 'b>,
758    ) -> Result<(Self, usize), ParseError<'b>> {
759        let mut config = None;
760
761        for opt in opts {
762            if opt.0 == "config" || opt.0 == "c" {
763                match opt.1 {
764                    Some(s) => {
765                        config =
766                            Some(s.parse::<std::path::PathBuf>().map_err(
767                                |_| ParseError::InvalidOptionValue {
768                                    name: "config",
769                                    value: s,
770                                    expected: "a valid file path",
771                                },
772                            )?);
773                    }
774                    None => {
775                        Err(ParseError::MissingValueForOption {
776                            name: "config",
777                        })?;
778                    }
779                }
780            } else {
781                Err(ParseError::UnknownOption(opt.0))?;
782            }
783        }
784
785        Ok((Self { config }, opts.len_consumed()))
786    }
787
788    fn specs() -> &'static [Opt] {
789        &[Opt {
790            name: "config",
791            description: "The path of config",
792            ty: "Path",
793            required: false,
794            flag: false,
795            short: Some('c'),
796            format: "-c, --config",
797        }]
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804
805    #[derive(Debug, PartialEq)]
806    struct TestOpts {
807        a: bool,
808        b: bool,
809        c: Option<u32>,
810    }
811
812    impl Opts for TestOpts {
813        fn specs() -> &'static [Opt] {
814            &[
815                Opt {
816                    name: "a",
817                    ty: "bool",
818                    required: false,
819                    flag: true,
820                    description: "",
821                    short: Some('a'),
822                    format: "-a",
823                },
824                Opt {
825                    name: "b",
826                    ty: "bool",
827                    required: false,
828                    flag: true,
829                    description: "",
830                    short: Some('b'),
831                    format: "-b",
832                },
833                Opt {
834                    name: "c",
835                    ty: "Option<u32>",
836                    required: false,
837                    flag: false,
838                    description: "",
839                    short: Some('c'),
840                    format: "-c <u32>",
841                },
842            ]
843        }
844
845        fn parse<'a, 'b: 'a>(
846            opts: &'a RawOpts<'a, 'b>,
847        ) -> Result<(Self, usize), ParseError<'b>> {
848            let mut a = false;
849            let mut b = false;
850            let mut c = None;
851
852            for (name, value) in opts {
853                match name {
854                    "a" => match value {
855                        Some(_) => Err(ParseError::UnexpectedFlagValue {
856                            name: "a",
857                            value: value.unwrap(),
858                        })?,
859                        None => a = true,
860                    },
861                    "b" => match value {
862                        Some(_) => Err(ParseError::UnexpectedFlagValue {
863                            name: "b",
864                            value: value.unwrap(),
865                        })?,
866                        None => b = true,
867                    },
868                    "c" => {
869                        let v =
870                            value.ok_or(ParseError::MissingValueForOption {
871                                name: "c",
872                            })?;
873                        c = Some(v.parse().map_err(|_| {
874                            ParseError::InvalidOptionValue {
875                                name: "c",
876                                value: v,
877                                expected: "u32",
878                            }
879                        })?);
880                    }
881                    _ => (), // Ignore unknown options in test
882                }
883            }
884
885            Ok((TestOpts { a, b, c }, opts.len_consumed()))
886        }
887    }
888
889    #[test]
890    fn test_clustered_bool_flags() {
891        let args = &["-ab"];
892        let raw_opts = RawOpts::new(args, TestOpts::specs());
893        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
894        assert_eq!(
895            opts,
896            TestOpts {
897                a: true,
898                b: true,
899                c: None
900            }
901        );
902        assert_eq!(len, 1);
903    }
904
905    #[test]
906    fn test_clustered_short_options() {
907        let args = &["-ab", "-c", "123"];
908        let raw_opts = RawOpts::new(args, TestOpts::specs());
909        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
910        assert_eq!(
911            opts,
912            TestOpts {
913                a: true,
914                b: true,
915                c: Some(123)
916            }
917        );
918        assert_eq!(len, 3);
919    }
920
921    #[test]
922    fn test_clustered_short_options_with_value_at_end() {
923        let args = &["-ac", "456"];
924        let raw_opts = RawOpts::new(args, TestOpts::specs());
925        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
926        assert_eq!(
927            opts,
928            TestOpts {
929                a: true,
930                b: false,
931                c: Some(456)
932            }
933        );
934        assert_eq!(len, 2);
935    }
936
937    #[test]
938    fn test_single_flag() {
939        let args = &["-a"];
940        let raw_opts = RawOpts::new(args, TestOpts::specs());
941        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
942        assert_eq!(
943            opts,
944            TestOpts {
945                a: true,
946                b: false,
947                c: None
948            }
949        );
950        assert_eq!(len, 1);
951    }
952
953    #[test]
954    fn test_clustered_flags_and_value_option() {
955        let args = &["-abc", "789"];
956        let raw_opts = RawOpts::new(args, TestOpts::specs());
957        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
958        assert_eq!(
959            opts,
960            TestOpts {
961                a: true,
962                b: true,
963                c: Some(789)
964            }
965        );
966        assert_eq!(len, 2);
967    }
968
969    #[test]
970    fn test_long_option_with_equals() {
971        let args = &["--c=456"];
972        let raw_opts = RawOpts::new(args, TestOpts::specs());
973        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
974        assert_eq!(
975            opts,
976            TestOpts {
977                a: false,
978                b: false,
979                c: Some(456)
980            }
981        );
982        assert_eq!(len, 1);
983    }
984
985    #[test]
986    fn test_short_option_with_equals() {
987        let args = &["-c=789"];
988        let raw_opts = RawOpts::new(args, TestOpts::specs());
989        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
990        assert_eq!(
991            opts,
992            TestOpts {
993                a: false,
994                b: false,
995                c: Some(789)
996            }
997        );
998        assert_eq!(len, 1);
999    }
1000
1001    #[test]
1002    fn test_version_flag_uses_uppercase_v() {
1003        let args = &["-V"];
1004        let raw_opts = RawOpts::new(args, &[]);
1005        let ((), len) = <() as Opts>::parse(&raw_opts).unwrap();
1006        assert_eq!(len, 1);
1007
1008        // Test that -v is not recognized as version (it would be unknown)
1009        let args = &["-v"];
1010        let raw_opts = RawOpts::new(args, TestOpts::specs());
1011        let (opts, len) = TestOpts::parse(&raw_opts).unwrap();
1012        assert_eq!(
1013            opts,
1014            TestOpts {
1015                a: false,
1016                b: false,
1017                c: None
1018            }
1019        );
1020        assert_eq!(len, 1);
1021    }
1022}