cliargs/
lib.rs

1// Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
2// This program is free software under MIT License.
3// See the file LICENSE in this distribution for more details.
4
5//! This crate is a library to parse command line arguments.
6//!
7//! This crate provides the following functionalities:
8//!
9//! - Supports [POSIX][posix] & [GNU][gnu] like short and long options.
10//!     - This crate supports `--` option.
11//!     - This library doesn't support numeric short option.
12//!     - This library supports not `-ofoo` but `-o=foo` as an alternative to
13//!       `-o foo` for short option.
14//! - Supports parsing with option configurations.
15//! - Supports parsing with option configurations made from struct fields and attributes, and
16//!   setting the option values to them.
17//! - Supports parsing command line arguments including sub commands.
18//! - Generates help text from option configurations.
19//!
20//! [posix]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html#Argument-Syntax
21//! [gnu]: https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html
22//!
23//! ## Install
24//!
25//! In `Cargo.toml`, write this crate as a dependency.
26//!
27//! ```toml
28//! [dependencies]
29//! cliargs = "0.6.0"
30//! ```
31//!
32//! ## Usage
33//!
34//! This crate provides the `Cmd` strcut to parse command line arguments.
35//! The usage of this `Cmd` struct is as follows:
36//!
37//! ### Creates a `Cmd` instance
38//!
39//! The `Cmd::new` function creates a `Cmd` instance with original command line
40//! arguments.
41//! This function uses `std::env::arg_os` and `OsString#into_string` to read
42//! command line arguments in order to avoid `panic!` call that the user cannot
43//! control.
44//!
45//! ```rust
46//! use cliargs::Cmd;
47//! use cliargs::errors::InvalidOsArg;
48//!
49//! let cmd = match Cmd::new() {
50//!     Ok(cmd) => cmd,
51//!     Err(InvalidOsArg::OsArgsContainInvalidUnicode { index, os_arg }) => {
52//!         panic!("Invalid Unicode data: {:?} (index: {})", os_arg, index);
53//!     }
54//! };
55//! ```
56//!
57//! ### Creates a `Cmd` instance with the specified `String` array
58//!
59//! The `Cmd::with_strings` function creates a `Cmd` instance with the
60//! specified `String` array.
61//!
62//! ```rust
63//! use cliargs::Cmd;
64//! use std::env;
65//!
66//! let cmd = Cmd::with_strings(env::args());
67//! ```
68//!
69//! ### Creates a `Cmd` instance with the specified `OsString` array.
70//!
71//! ```rust
72//! use cliargs::Cmd;
73//! use cliargs::errors::InvalidOsArg;
74//! use std::env;
75//!
76//! let cmd = match Cmd::with_os_strings(env::args_os()) {
77//!     Ok(cmd) => cmd,
78//!     Err(InvalidOsArg::OsArgsContainInvalidUnicode { index, os_arg }) => {
79//!         panic!("Invalid Unicode data: {:?} (index: {})", os_arg, index);
80//!     }
81//! };
82//! ```
83//!
84//! ## Parses without configurations
85//!
86//! The `Cmd` struct has the method which parses command line arguments without
87//! configurations.
88//! This method automatically divides command line arguments to options and
89//! command arguments.
90//!
91//! Command line arguments starts with `-` or `--` are options, and others are
92//! command arguments.
93//! If you want to specify a value to an option, follows `"="` and the value
94//! after the option, like `foo=123`.
95//!
96//! All command line arguments after `--` are command arguments, even they
97//! starts with `-` or `--`.
98//!
99//! ```rust
100//! use cliargs::Cmd;
101//! use cliargs::errors::InvalidOption;
102//!
103//! let mut cmd = Cmd::with_strings([ /* ... */ ]);
104//! match cmd.parse() {
105//!     Ok(_) => { /* ... */ },
106//!     Err(InvalidOption::OptionContainsInvalidChar { option }) => {
107//!         panic!("Option contains invalid character: {option}");
108//!     },
109//!     Err(err) => panic!("Invalid option: {}", err.option()),
110//! }
111//! ```
112//!
113//! ## Parses with configurations
114//!
115//! The `Cmd` struct has the method `parse_with` which parses command line
116//! arguments with configurations.
117//! This method takes an array of option configurations: `OptCfg`, and divides
118//! command line arguments to options and command arguments according to this
119//! configurations..
120//!
121//! An option configuration has fields: `store_key`, `names`, `has_arg`,
122//! `is_array`, `defaults`, `desc`, `arg_in_help`, and `validator`.
123//!
124//! `store_key` field is specified the key name to store the option value to
125//! the option map in the `Cmd` instance.
126//! If this field is not specified, the first element of `names` field is used
127//! instead.
128//!
129//! `names` field is a string array and specified the option names, that are
130//! both long options and short options.
131//! The order of elements in this field is used in a help text.
132//! If you want to prioritize the output of short option name first in the help
133//! text, like `-f, --foo-bar`, but use the long option name as the key in the
134//! option map, write `store_key` and `names` fields as follows:
135//! `OptCfg::with([store_key("foo-bar"), names(&["f", "foo-bar"])])`.
136//!
137//! `has_arg` field indicates the option requires one or more values.
138//! `is_array` field indicates the option can have multiple values.
139//! `defaults` field is an array of string which is used as default one or more
140//! option arguments if the option is not specified.
141//! `desc` is a description of the option for help text.
142//! `arg_n_help` field is a text which is output after option name and aliases
143//! as an option value in help text.
144//!
145//! `validator` field is to set a function pointer which validates an option
146//! argument.
147//! This crate provides the validator `cliargs::validators::validate_number<T>`
148//! which validates whether an option argument is valid format as a number.
149//!
150//! In addition,the help printing for an array of [OptCfg] is generated with [Help].
151//!
152//! ```rust
153//! use cliargs::{Cmd, OptCfg};
154//! use cliargs::OptCfgParam::{names, has_arg, defaults, validator, desc, arg_in_help};
155//! use cliargs::validators::validate_number;
156//! use cliargs::errors::InvalidOption;
157//! use cliargs::Help;
158//!
159//! let mut cmd = Cmd::with_strings([ /* ... */ ]);
160//! let opt_cfgs = vec![
161//!     OptCfg {
162//!         store_key: "foo_bar".to_string(),
163//!         names: vec!["foo-bar".to_string(), "f".to_string()],
164//!         has_arg: true,
165//!         is_array: false,
166//!         defaults: Some(vec![123.to_string()]),
167//!         desc: "This is description of foo-bar.".to_string(),
168//!         arg_in_help: "<num>".to_string(),
169//!         validator: validate_number::<u64>,
170//!     },
171//!     OptCfg::with([
172//!         names(&["baz", "z"]),
173//!         has_arg(true),
174//!         defaults(&["1"]),
175//!         desc("This is description of baz."),
176//!         arg_in_help("<num>"),
177//!         validator(validate_number::<u64>),
178//!     ]),
179//! ];
180//!
181//! match cmd.parse_with(opt_cfgs) {
182//!     Ok(_) => { /* ... */ },
183//!     Err(InvalidOption::OptionContainsInvalidChar { option }) => { /* ... */ },
184//!     Err(InvalidOption::UnconfiguredOption { option }) => { /* ... */ },
185//!     Err(InvalidOption::OptionNeedsArg { option, .. }) => { /* ... */ },
186//!     Err(InvalidOption::OptionTakesNoArg { option, .. }) => { /* ... */ },
187//!     Err(InvalidOption::OptionIsNotArray { option, .. }) => { /* ... */ },
188//!     Err(InvalidOption::OptionArgIsInvalid { option, opt_arg, details, .. }) => { /* ... */ },
189//!     Err(err) => panic!("Invalid option: {}", err.option()),
190//! }
191//!
192//! let opt_cfgs = cmd.opt_cfgs();
193//!
194//! let mut help = Help::new();
195//! help.add_text("This is the usage description.".to_string());
196//! help.add_opts_with_margins(opt_cfgs, 2, 0);
197//! help.print();
198//!
199//! // (stdout)
200//! // This is the usage description.
201//! //   --foo-bar, -f    This is description of foo-bar.
202//! //   --bar, -z <num>  This is description of baz.
203//! ```
204//!
205//! ## Parse for a OptStore struct
206//!
207//! The [Cmd] struct has the method parse_for which parses command line arguments and set their option values to
208//! the option store which is passed as an argument.
209//!
210//! This method divides command line arguments to command arguments and options, then sets each option value to a
211//! curresponding field of the option store.
212//!
213//! Within this method, a vector of [OptCfg] is made from the fields of the option store. This [OptCfg] vector is
214//! set to the public field cfgs of the [Cmd] instance. If you want to access this option configurations, get them
215//! from this field.
216//! An option configuration corresponding to each field of an option store is determined by its type and opt field
217//! attribute.
218//! If the type is bool, the option takes no argument. If the type is integer, floating point number or string, the
219//! option can takes single option argument, therefore it can appear once in command line arguments.
220//! If the type is a vector, the option can takes multiple option arguments, therefore it can appear multiple times
221//! in command line arguments.
222//!
223//! A `opt` field attribute can have the following pairs of name and value: one is `cfg` to specify `names` and
224//! `defaults` fields of [OptCfg] struct, another is `desc` to specify `desc` field, and yet another is `arg` to
225//! specify `arg_in_help` field.
226//!
227//! The format of `cfg` is like `cfg="f,foo=123"`. The left side of the equal sign is the option name(s), and the
228//! right side is the default value(s).
229//! If there is no equal sign, it is determined that only the option name is specified.
230//! If you want to specify multiple option names, separate them with commas.
231//! If you want to specify multiple default values, separate them with commas and round them with square brackets,
232//! like `[1,2,3]`.
233//! If you want to use your favorite carachter as a separator, you can use it by putting it on the left side of the
234//! open square bracket, like `/[1/2/3]`.
235//!
236//! NOTE: A default value of empty string array option in a field attribute is `[]`, like `#[opt(cfg="=[]")]`, but
237//! it doesn't represent an array which contains only one empty string.
238//! If you want to specify an array which contains only one emtpy string, write nothing after `=` symbol, like
239//! `#[opt(cfg="=")]`.
240//!
241//! ```rust
242//! use cliargs::Cmd;
243//! use cliargs::errors::InvalidOption;
244//! use cliargs::Help;
245//!
246//! #[derive(cliargs::OptStore)]
247//! struct MyOptions {
248//!     #[opt(cfg = "f,foo-bar", desc="The description of foo_bar.")]
249//!     foo_bar: bool,
250//!     #[opt(cfg = "b,baz", desc="The description of baz.", arg="<s>")]
251//!     baz: String,
252//! }
253//! let mut my_options = MyOptions::with_defaults();
254//!
255//! let mut cmd = Cmd::with_strings([ /* ... */ ]);
256//! match cmd.parse_for(&mut my_options) {
257//!     Ok(_) => { /* ... */ },
258//!     Err(InvalidOption::OptionContainsInvalidChar { option }) => { /* ... */ },
259//!     Err(InvalidOption::UnconfiguredOption { option }) => { /* ... */ },
260//!     Err(InvalidOption::OptionNeedsArg { option, .. }) => { /* ... */ },
261//!     Err(InvalidOption::OptionTakesNoArg { option, .. }) => { /* ... */ },
262//!     Err(InvalidOption::OptionIsNotArray { option, .. }) => { /* ... */ },
263//!     Err(InvalidOption::OptionArgIsInvalid { option, opt_arg, details, .. }) => { /* ... */ },
264//!     Err(err) => panic!("Invalid option: {}", err.option()),
265//! }
266//!
267//! let opt_cfgs = cmd.opt_cfgs();
268//!
269//! let mut help = Help::new();
270//! help.add_text("This is the usage description.".to_string());
271//! help.add_opts_with_margins(opt_cfgs, 2, 0);
272//! help.print();
273//!
274//! // (stdout)
275//! // This is the usage description.
276//! //   -f, --foo-bar  This is description of foo_bar.
277//! //   -z, --baz <s>  This is description of baz.
278//! ```
279//!
280//! ## Parse command line arguments including sub command
281//!
282//! This crate provides methods [Cmd::parse_until_sub_cmd], [Cmd::parse_until_sub_cmd_with], and
283//! [Cmd::parse_until_sub_cmd_for] for parsing command line arguments including sub commands.
284//!
285//! These methods correspond to [Cmd::parse], [Cmd::parse_with], and [Cmd::parse_for],
286//! respectively, and behave the same except that they stop parsing before the first command
287//! argument (= sub command) and
288//! return a [Cmd] instance containing the arguments starting from the the sub command.
289//!
290//! The folowing is an example code using [Cmd::parse_until_sub_cmd]:
291//!
292//! ```rust
293//! use cliargs::Cmd;
294//! use cliargs::errors::InvalidOption;
295//!
296//! let mut cmd = Cmd::with_strings([ /* ... */ ]);
297//!
298//! match cmd.parse_until_sub_cmd() {
299//!     Ok(Some(mut sub_cmd)) => {
300//!         let sub_cmd_name = sub_cmd.name();
301//!         match sub_cmd.parse() {
302//!             Ok(_) => { /* ... */ },
303//!             Err(err) => panic!("Invalid option: {}", err.option()),
304//!         }
305//!     },
306//!     Ok(None) => { /* ... */ },
307//!     Err(InvalidOption::OptionContainsInvalidChar { option }) => {
308//!         panic!("Option contains invalid character: {option}");
309//!     },
310//!     Err(err) => panic!("Invalid option: {}", err.option()),
311//! }
312//! ```
313
314/// Enums for errors that can occur when parsing command line arguments.
315pub mod errors;
316
317mod opt_cfg;
318pub use opt_cfg::OptCfg;
319pub use opt_cfg::OptCfgParam;
320
321mod help;
322pub use help::Help;
323pub use help::HelpIter;
324
325/// Function pointers for validating an option argument.
326pub use opt_cfg::validators;
327
328mod parse;
329pub use parse::make_opt_cfgs_for;
330pub use parse::OptStore;
331
332extern crate cliargs_derive;
333pub use cliargs_derive::OptStore;
334
335use std::collections::HashMap;
336use std::env;
337use std::ffi::OsString;
338use std::fmt;
339use std::mem;
340use std::path;
341
342/// Parses command line arguments and stores them.
343///
344/// The results of parsing are stored by separating into command name, command arguments, options,
345/// and option arguments.
346///
347/// These values are retrieved as string slices with the same lifetime as this `Cmd` instance.
348/// Therefore, if you want to use those values for a longer period, it is needed to convert them
349/// to [String]s.
350pub struct Cmd<'a> {
351    name: &'a str,
352    args: Vec<&'a str>,
353    opts: HashMap<&'a str, Vec<&'a str>>,
354    cfgs: Vec<OptCfg>,
355    is_after_end_opt: bool,
356
357    _leaked_strs: Vec<&'a str>,
358    _num_of_args: usize,
359}
360
361impl<'a> Drop for Cmd<'a> {
362    fn drop(&mut self) {
363        for str in &self._leaked_strs {
364            let boxed = unsafe { Box::from_raw(*str as *const str as *mut str) };
365            mem::drop(boxed);
366        }
367    }
368}
369
370impl fmt::Debug for Cmd<'_> {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        f.debug_struct("Cmd")
373            .field("name", &self.name)
374            .field("args", &self.args)
375            .field("opts", &self.opts)
376            .finish()
377    }
378}
379
380impl<'b, 'a> Cmd<'a> {
381    /// Creates a `Cmd` instance with command line arguments obtained from [std::env::args_os].
382    ///
383    /// Since [std::env::args_os] returns a vector of [OsString] and they can contain invalid
384    /// unicode data, the return value of this funciton is [Result] of `Cmd` or
385    /// `errors::InvalidOsArg`.
386    pub fn new() -> Result<Cmd<'a>, errors::InvalidOsArg> {
387        Self::with_os_strings(env::args_os())
388    }
389
390    /// Creates a `Cmd` instance with the specified iterator of [OsString]s.
391    ///
392    /// [OsString]s can contain invalid unicode data, the return value of this function
393    /// is [Result] of `Cmd` or `errors::InvalidOsArg`.
394    pub fn with_os_strings(
395        osargs: impl IntoIterator<Item = OsString>,
396    ) -> Result<Cmd<'a>, errors::InvalidOsArg> {
397        let osarg_iter = osargs.into_iter();
398        let (size, _) = osarg_iter.size_hint();
399        let mut _leaked_strs = Vec::with_capacity(size);
400
401        let cmd_name_start: usize;
402
403        let mut enm = osarg_iter.enumerate();
404        if let Some((idx, osarg)) = enm.next() {
405            // The first element is the command path.
406            let path = path::Path::new(&osarg);
407            let base_len = if let Some(base_os) = path.file_name() {
408                if let Some(base_str) = base_os.to_str() {
409                    base_str.len()
410                } else {
411                    0
412                }
413            } else {
414                0
415            };
416            match osarg.into_string() {
417                Ok(string) => {
418                    let str: &'a str = string.leak();
419                    _leaked_strs.push(str);
420                    cmd_name_start = str.len() - base_len;
421                }
422                Err(osstring) => {
423                    return Err(errors::InvalidOsArg::OsArgsContainInvalidUnicode {
424                        index: idx,
425                        os_arg: osstring,
426                    });
427                }
428            }
429
430            // The elements from the second one onward are the arguments.
431            for (idx, osarg) in enm {
432                match osarg.into_string() {
433                    Ok(string) => {
434                        let str: &'a str = string.leak();
435                        _leaked_strs.push(str);
436                    }
437                    Err(osstring) => {
438                        for str in _leaked_strs {
439                            let boxed = unsafe { Box::from_raw(str as *const str as *mut str) };
440                            mem::drop(boxed);
441                        }
442                        return Err(errors::InvalidOsArg::OsArgsContainInvalidUnicode {
443                            index: idx,
444                            os_arg: osstring,
445                        });
446                    }
447                }
448            }
449        } else {
450            _leaked_strs.push("");
451            cmd_name_start = 0;
452        }
453
454        let _num_of_args = _leaked_strs.len();
455
456        Ok(Cmd {
457            name: &_leaked_strs[0][cmd_name_start..],
458            args: Vec::new(),
459            opts: HashMap::new(),
460            cfgs: Vec::new(),
461            is_after_end_opt: false,
462            _leaked_strs,
463            _num_of_args,
464        })
465    }
466
467    /// Creates a `Cmd` instance with the specified iterator of [String]s.
468    pub fn with_strings(args: impl IntoIterator<Item = String>) -> Cmd<'a> {
469        let arg_iter = args.into_iter();
470        let (size, _) = arg_iter.size_hint();
471        let mut _leaked_strs = Vec::with_capacity(size);
472
473        for arg in arg_iter {
474            let str: &'a str = arg.leak();
475            _leaked_strs.push(str);
476        }
477
478        let cmd_name_start: usize;
479
480        if _leaked_strs.len() > 0 {
481            let path = path::Path::new(_leaked_strs[0]);
482            let mut base_len = 0;
483            if let Some(base_os) = path.file_name() {
484                if let Some(base_str) = base_os.to_str() {
485                    base_len = base_str.len();
486                }
487            }
488            cmd_name_start = _leaked_strs[0].len() - base_len;
489        } else {
490            _leaked_strs.push("");
491            cmd_name_start = 0;
492        };
493
494        let _num_of_args = _leaked_strs.len();
495
496        Cmd {
497            name: &_leaked_strs[0][cmd_name_start..],
498            args: Vec::new(),
499            opts: HashMap::new(),
500            cfgs: Vec::new(),
501            is_after_end_opt: false,
502            _leaked_strs,
503            _num_of_args,
504        }
505    }
506
507    fn sub_cmd(&'a self, from_index: usize, is_after_end_opt: bool) -> Cmd<'b> {
508        let arg_iter = self._leaked_strs[from_index..(self._num_of_args)].into_iter();
509        let (size, _) = arg_iter.size_hint();
510        let mut _leaked_strs = Vec::with_capacity(size);
511
512        for arg in arg_iter {
513            let str: &'b str = arg.to_string().leak();
514            _leaked_strs.push(str);
515        }
516
517        let _num_of_args = _leaked_strs.len();
518
519        Cmd {
520            name: &_leaked_strs[0],
521            args: Vec::new(),
522            opts: HashMap::new(),
523            cfgs: Vec::new(),
524            is_after_end_opt: is_after_end_opt,
525            _leaked_strs,
526            _num_of_args,
527        }
528    }
529
530    /// Returns the command name.
531    ///
532    /// This name is base name extracted from the command path string slice, which is the first
533    /// element of the command line arguments.
534    pub fn name(&'a self) -> &'a str {
535        self.name
536    }
537
538    /// Returns the command arguments.
539    ///
540    /// These arguments are retrieved as string slices in an array.
541    pub fn args(&'a self) -> &'a [&'a str] {
542        &self.args
543    }
544
545    /// Checks whether an option with the specified name exists.
546    pub fn has_opt(&self, name: &str) -> bool {
547        self.opts.contains_key(name)
548    }
549
550    /// Returns the option argument with the specified name.
551    ///
552    /// If the option has multiple arguments, this method returns the first argument.
553    /// If the option is a boolean flag, this method returns [None].
554    /// If the option is not specified in the command line arguments, the return value
555    /// of this method is [None].
556    pub fn opt_arg(&'a self, name: &str) -> Option<&'a str> {
557        if let Some(opt_vec) = self.opts.get(name) {
558            if opt_vec.len() > 0 {
559                return Some(opt_vec[0]);
560            }
561        }
562        None
563    }
564
565    /// Returns the option arguments with the specified name.
566    ///
567    /// If the option has one or multiple arguments, this method returns an array of the arguments.
568    /// If the option is a boolean flag, this method returns an empty vector.
569    /// If the option is not specified in the command line arguments, the return value
570    /// of this method is [None].
571    pub fn opt_args(&'a self, name: &str) -> Option<&'a [&'a str]> {
572        match self.opts.get(name) {
573            Some(vec) => Some(&vec),
574            None => None,
575        }
576    }
577
578    /// Retrieves the option configurations which was used to parse command line arguments.
579    pub fn opt_cfgs(&'a self) -> &[OptCfg] {
580        &self.cfgs
581    }
582}
583
584#[cfg(test)]
585mod tests_of_cmd {
586    use super::Cmd;
587
588    mod tests_of_new {
589        use super::Cmd;
590
591        #[test]
592        fn should_create_a_new_instance() {
593            let cmd = Cmd::new().unwrap();
594            println!("cmd = {cmd:?}");
595            println!("cmd._leaked_strs = {:?}", cmd._leaked_strs);
596            assert!(cmd.name().starts_with("cliargs-"));
597            assert!(cmd._leaked_strs.len() > 0);
598        }
599    }
600
601    mod tests_of_with_strings {
602        use super::Cmd;
603
604        #[test]
605        fn should_create_a_new_instance() {
606            let mut cmd = Cmd::with_strings([
607                "/path/to/app".to_string(),
608                "--foo".to_string(),
609                "bar".to_string(),
610            ]);
611
612            cmd.args.push(cmd._leaked_strs[2]);
613            cmd.opts
614                .insert(&cmd._leaked_strs[1][2..], Vec::with_capacity(0));
615
616            println!("cmd = {cmd:?}");
617            println!("cmd._leaked_strs = {:?}", cmd._leaked_strs);
618            assert_eq!(cmd.name(), "app");
619        }
620
621        #[test]
622        fn should_get_command_name_from_absolute_path() {
623            let cmd = Cmd::with_strings([
624                "/path/to/app".to_string(),
625                "--foo".to_string(),
626                "--bar".to_string(),
627                "baz".to_string(),
628                "--bar".to_string(),
629                "qux".to_string(),
630                "quux".to_string(),
631                "corge".to_string(),
632            ]);
633            assert_eq!(cmd.name(), "app");
634        }
635
636        #[test]
637        fn should_get_command_name_from_relative_path() {
638            let cmd = Cmd::with_strings([
639                "../path/to/app".to_string(),
640                "--foo".to_string(),
641                "--bar".to_string(),
642                "baz".to_string(),
643                "--bar".to_string(),
644                "qux".to_string(),
645                "quux".to_string(),
646                "corge".to_string(),
647            ]);
648            assert_eq!(cmd.name(), "app");
649        }
650
651        #[test]
652        fn should_get_command_name_from_base_name_only() {
653            let cmd = Cmd::with_strings([
654                "app".to_string(),
655                "--foo".to_string(),
656                "--bar".to_string(),
657                "baz".to_string(),
658                "--bar".to_string(),
659                "qux".to_string(),
660                "quux".to_string(),
661                "corge".to_string(),
662            ]);
663            assert_eq!(cmd.name(), "app");
664        }
665
666        #[test]
667        fn should_get_command_name_when_command_line_arguments_is_empty() {
668            let cmd = Cmd::with_strings([]);
669            assert_eq!(cmd.name(), "");
670        }
671    }
672
673    mod tests_of_with_os_strings {
674        use super::Cmd;
675        use std::ffi;
676
677        #[test]
678        fn should_create_a_new_instance() {
679            let cmd = Cmd::with_os_strings([
680                ffi::OsString::from("/path/to/app"),
681                ffi::OsString::from("--foo"),
682                ffi::OsString::from("bar_baz"),
683                ffi::OsString::from("qux"),
684            ])
685            .unwrap();
686
687            assert_eq!(cmd.name(), "app");
688        }
689
690        #[cfg(not(windows))] // Because OsStr is valid WTF8 and OsString is valid WTF16 on Windows
691        #[test]
692        fn should_fail_because_os_args_contain_invalid_unicode() {
693            let bad_arg = b"bar\xFFbaz";
694            let bad_os_str = unsafe { ffi::OsStr::from_encoded_bytes_unchecked(bad_arg) };
695            let bad_os_string = bad_os_str.to_os_string();
696
697            match Cmd::with_os_strings([
698                ffi::OsString::from("/path/to/app"),
699                ffi::OsString::from("--foo"),
700                bad_os_string.clone(),
701                ffi::OsString::from("qux"),
702            ]) {
703                Ok(_) => assert!(false),
704                Err(crate::errors::InvalidOsArg::OsArgsContainInvalidUnicode { index, os_arg }) => {
705                    assert_eq!(index, 2);
706                    assert_eq!(os_arg, bad_os_string);
707                }
708            }
709        }
710
711        #[cfg(not(windows))] // Because OsStr is valid WTF8 and OsString is valid WTF16 on Windows
712        #[test]
713        fn should_fail_because_command_name_contains_invalid_unicode() {
714            let bad_arg = b"bar\xFFbaz";
715            let bad_os_str = unsafe { ffi::OsStr::from_encoded_bytes_unchecked(bad_arg) };
716            let bad_os_string = bad_os_str.to_os_string();
717
718            match Cmd::with_os_strings([
719                bad_os_string.clone(),
720                ffi::OsString::from("--foo"),
721                ffi::OsString::from("qux"),
722            ]) {
723                Ok(_) => assert!(false),
724                Err(crate::errors::InvalidOsArg::OsArgsContainInvalidUnicode { index, os_arg }) => {
725                    assert_eq!(index, 0);
726                    assert_eq!(os_arg, bad_os_string);
727                }
728            }
729        }
730
731        #[test]
732        fn should_get_command_name_from_absolute_path() {
733            if let Ok(cmd) = Cmd::with_os_strings([
734                ffi::OsString::from("/path/to/app"),
735                ffi::OsString::from("--foo"),
736                ffi::OsString::from("--bar"),
737                ffi::OsString::from("baz"),
738                ffi::OsString::from("--bar"),
739                ffi::OsString::from("qux"),
740                ffi::OsString::from("quux"),
741                ffi::OsString::from("corge"),
742            ]) {
743                assert_eq!(cmd.name(), "app");
744            } else {
745                assert!(false);
746            }
747        }
748
749        #[test]
750        fn should_get_command_name_from_relative_path() {
751            if let Ok(cmd) = Cmd::with_os_strings([
752                ffi::OsString::from("../path/to/app"),
753                ffi::OsString::from("--foo"),
754                ffi::OsString::from("--bar"),
755                ffi::OsString::from("baz"),
756                ffi::OsString::from("--bar"),
757                ffi::OsString::from("qux"),
758                ffi::OsString::from("quux"),
759                ffi::OsString::from("corge"),
760            ]) {
761                assert_eq!(cmd.name(), "app");
762            } else {
763                assert!(false);
764            }
765        }
766
767        #[test]
768        fn should_get_command_name_from_base_name_only() {
769            if let Ok(cmd) = Cmd::with_os_strings([
770                ffi::OsString::from("app"),
771                ffi::OsString::from("--foo"),
772                ffi::OsString::from("--bar"),
773                ffi::OsString::from("baz"),
774                ffi::OsString::from("--bar"),
775                ffi::OsString::from("qux"),
776                ffi::OsString::from("quux"),
777                ffi::OsString::from("corge"),
778            ]) {
779                assert_eq!(cmd.name(), "app");
780            } else {
781                assert!(false);
782            }
783        }
784
785        #[test]
786        fn should_get_command_name_when_command_line_arguments_is_empty() {
787            if let Ok(cmd) = Cmd::with_os_strings([]) {
788                assert_eq!(cmd.name(), "");
789            } else {
790                assert!(false);
791            }
792        }
793    }
794
795    mod tests_of_getters {
796        use super::Cmd;
797
798        #[test]
799        fn should_get_command_name_when_command_line_arguments_is_empty() {
800            let cmd = Cmd::with_strings([]);
801
802            assert_eq!(cmd.name(), "");
803        }
804
805        #[test]
806        fn should_get_command_arguments() {
807            let mut cmd = Cmd::with_strings([
808                "/path/to/app".to_string(),
809                "--foo".to_string(),
810                "--bar".to_string(),
811                "baz".to_string(),
812                "--bar".to_string(),
813                "qux".to_string(),
814                "quux".to_string(),
815                "corge".to_string(),
816            ]);
817
818            cmd.args.push(cmd._leaked_strs[6]);
819            cmd.args.push(cmd._leaked_strs[7]);
820            cmd.opts
821                .insert(&cmd._leaked_strs[1][2..], Vec::with_capacity(0));
822            cmd.opts.insert(
823                &cmd._leaked_strs[2][2..],
824                vec![&cmd._leaked_strs[3], &cmd._leaked_strs[5]],
825            );
826
827            assert_eq!(cmd.args(), ["quux", "corge"]);
828        }
829
830        #[test]
831        fn should_check_option_is_specified() {
832            let mut cmd = Cmd::with_strings([
833                "/path/to/app".to_string(),
834                "--foo".to_string(),
835                "--bar".to_string(),
836                "baz".to_string(),
837                "--bar".to_string(),
838                "qux".to_string(),
839                "quux".to_string(),
840                "corge".to_string(),
841            ]);
842
843            cmd.args.push(cmd._leaked_strs[6]);
844            cmd.args.push(cmd._leaked_strs[7]);
845            cmd.opts
846                .insert(&cmd._leaked_strs[1][2..], Vec::with_capacity(0));
847            cmd.opts.insert(
848                &cmd._leaked_strs[2][2..],
849                vec![&cmd._leaked_strs[3], &cmd._leaked_strs[5]],
850            );
851
852            assert_eq!(cmd.has_opt("foo"), true);
853            assert_eq!(cmd.has_opt("bar"), true);
854            assert_eq!(cmd.has_opt("baz"), false);
855        }
856
857        #[test]
858        fn should_get_single_option_argument() {
859            let mut cmd = Cmd::with_strings([
860                "/path/to/app".to_string(),
861                "--foo".to_string(),
862                "--bar".to_string(),
863                "baz".to_string(),
864                "--bar".to_string(),
865                "qux".to_string(),
866                "quux".to_string(),
867                "corge".to_string(),
868            ]);
869
870            cmd.args.push(cmd._leaked_strs[6]);
871            cmd.args.push(cmd._leaked_strs[7]);
872            cmd.opts
873                .insert(&cmd._leaked_strs[1][2..], Vec::with_capacity(0));
874            cmd.opts.insert(
875                &cmd._leaked_strs[2][2..],
876                vec![&cmd._leaked_strs[3], &cmd._leaked_strs[5]],
877            );
878
879            assert_eq!(cmd.opt_arg("foo"), None);
880            assert_eq!(cmd.opt_arg("bar"), Some("baz"));
881            assert_eq!(cmd.opt_arg("baz"), None);
882        }
883
884        #[test]
885        fn should_get_multiple_option_arguments() {
886            let mut cmd = Cmd::with_strings([
887                "/path/to/app".to_string(),
888                "--foo".to_string(),
889                "--bar".to_string(),
890                "baz".to_string(),
891                "--bar".to_string(),
892                "qux".to_string(),
893                "quux".to_string(),
894                "corge".to_string(),
895            ]);
896
897            cmd.args.push(cmd._leaked_strs[6]);
898            cmd.args.push(cmd._leaked_strs[7]);
899            cmd.opts
900                .insert(&cmd._leaked_strs[1][2..], Vec::with_capacity(0));
901            cmd.opts.insert(
902                &cmd._leaked_strs[2][2..],
903                vec![&cmd._leaked_strs[3], &cmd._leaked_strs[5]],
904            );
905
906            assert_eq!(cmd.opt_args("foo"), Some(&[] as &[&str]));
907            assert_eq!(cmd.opt_args("bar"), Some(&["baz", "qux"] as &[&str]));
908            assert_eq!(cmd.opt_args("baz"), None);
909        }
910    }
911
912    mod tests_of_moving_cmd {
913        use crate::Cmd;
914        use crate::OptCfg;
915        use crate::OptCfgParam::*;
916
917        #[test]
918        fn should_move_by_passing_a_parameter() {
919            fn move_cmd(cmd: Cmd) {
920                assert_eq!(cmd.name(), "app");
921                assert_eq!(cmd.args(), &["baz", "qux", "quux", "corge"]);
922                assert_eq!(cmd.opt_args("foo").unwrap(), &Vec::<&str>::new());
923                assert_eq!(cmd.opt_args("bar").unwrap(), &["ABC", "DEF"]);
924                assert_eq!(
925                    cmd._leaked_strs,
926                    &[
927                        "/path/to/app",
928                        "--foo",
929                        "--bar=ABC",
930                        "baz",
931                        "--bar=DEF",
932                        "qux",
933                        "quux",
934                        "corge",
935                        "foo",
936                        "bar",
937                    ]
938                );
939                assert_eq!(cmd.opt_cfgs().len(), 2);
940                assert_eq!(cmd.opt_cfgs()[0].store_key, "");
941                assert_eq!(cmd.opt_cfgs()[0].names, &["foo"]);
942                assert_eq!(cmd.opt_cfgs()[0].has_arg, false);
943                assert_eq!(cmd.opt_cfgs()[0].is_array, false);
944                assert_eq!(cmd.opt_cfgs()[0].defaults, None);
945                assert_eq!(cmd.opt_cfgs()[0].desc, "");
946                assert_eq!(cmd.opt_cfgs()[0].arg_in_help, "");
947                assert_eq!(cmd.opt_cfgs()[1].store_key, "");
948                assert_eq!(cmd.opt_cfgs()[1].names, &["bar"]);
949                assert_eq!(cmd.opt_cfgs()[1].has_arg, true);
950                assert_eq!(cmd.opt_cfgs()[1].is_array, true);
951                assert_eq!(cmd.opt_cfgs()[1].defaults, None);
952                assert_eq!(cmd.opt_cfgs()[1].desc, "");
953                assert_eq!(cmd.opt_cfgs()[1].arg_in_help, "");
954            }
955
956            let cfgs = vec![
957                OptCfg::with([names(&["foo"])]),
958                OptCfg::with([names(&["bar"]), has_arg(true), is_array(true)]),
959            ];
960
961            let mut cmd = Cmd::with_strings([
962                "/path/to/app".to_string(),
963                "--foo".to_string(),
964                "--bar=ABC".to_string(),
965                "baz".to_string(),
966                "--bar=DEF".to_string(),
967                "qux".to_string(),
968                "quux".to_string(),
969                "corge".to_string(),
970            ]);
971            let _ = cmd.parse_with(cfgs);
972
973            move_cmd(cmd);
974        }
975
976        #[test]
977        fn should_move_by_returning() {
978            fn move_cmd() -> Cmd<'static> {
979                let cfgs = vec![
980                    OptCfg::with([names(&["foo"])]),
981                    OptCfg::with([names(&["bar"]), has_arg(true), is_array(true)]),
982                ];
983
984                let mut cmd = Cmd::with_strings([
985                    "/path/to/app".to_string(),
986                    "--foo".to_string(),
987                    "--bar=ABC".to_string(),
988                    "baz".to_string(),
989                    "--bar=DEF".to_string(),
990                    "qux".to_string(),
991                    "quux".to_string(),
992                    "corge".to_string(),
993                ]);
994                let _ = cmd.parse_with(cfgs);
995                cmd
996            }
997
998            let cmd = move_cmd();
999            assert_eq!(cmd.name(), "app");
1000            assert_eq!(cmd.args(), &["baz", "qux", "quux", "corge"]);
1001            assert_eq!(cmd.opt_args("foo").unwrap(), &Vec::<&str>::new());
1002            assert_eq!(cmd.opt_args("bar").unwrap(), &["ABC", "DEF"]);
1003            assert_eq!(
1004                cmd._leaked_strs,
1005                &[
1006                    "/path/to/app",
1007                    "--foo",
1008                    "--bar=ABC",
1009                    "baz",
1010                    "--bar=DEF",
1011                    "qux",
1012                    "quux",
1013                    "corge",
1014                    "foo",
1015                    "bar",
1016                ]
1017            );
1018            assert_eq!(cmd.opt_cfgs().len(), 2);
1019            assert_eq!(cmd.opt_cfgs()[0].store_key, "");
1020            assert_eq!(cmd.opt_cfgs()[0].names, &["foo"]);
1021            assert_eq!(cmd.opt_cfgs()[0].has_arg, false);
1022            assert_eq!(cmd.opt_cfgs()[0].is_array, false);
1023            assert_eq!(cmd.opt_cfgs()[0].defaults, None);
1024            assert_eq!(cmd.opt_cfgs()[0].desc, "");
1025            assert_eq!(cmd.opt_cfgs()[0].arg_in_help, "");
1026            assert_eq!(cmd.opt_cfgs()[1].store_key, "");
1027            assert_eq!(cmd.opt_cfgs()[1].names, &["bar"]);
1028            assert_eq!(cmd.opt_cfgs()[1].has_arg, true);
1029            assert_eq!(cmd.opt_cfgs()[1].is_array, true);
1030            assert_eq!(cmd.opt_cfgs()[1].defaults, None);
1031            assert_eq!(cmd.opt_cfgs()[1].desc, "");
1032            assert_eq!(cmd.opt_cfgs()[1].arg_in_help, "");
1033        }
1034
1035        #[test]
1036        fn should_move_by_mem_replace() {
1037            fn move_cmd() -> Cmd<'static> {
1038                let cfgs = vec![
1039                    OptCfg::with([names(&["foo"])]),
1040                    OptCfg::with([names(&["bar"]), has_arg(true), is_array(true)]),
1041                ];
1042
1043                let mut cmd = Cmd::with_strings([
1044                    "/path/to/app".to_string(),
1045                    "--foo".to_string(),
1046                    "--bar=ABC".to_string(),
1047                    "baz".to_string(),
1048                    "--bar=DEF".to_string(),
1049                    "qux".to_string(),
1050                    "quux".to_string(),
1051                    "corge".to_string(),
1052                ]);
1053                let _ = cmd.parse_with(cfgs);
1054
1055                let mut cmd1 = Cmd::with_strings([]);
1056                let _ = std::mem::replace(&mut cmd1, cmd);
1057                cmd1
1058            }
1059
1060            let cmd = move_cmd();
1061            assert_eq!(cmd.name(), "app");
1062            assert_eq!(cmd.args(), &["baz", "qux", "quux", "corge"]);
1063            assert_eq!(cmd.opt_args("foo").unwrap(), &Vec::<&str>::new());
1064            assert_eq!(cmd.opt_args("bar").unwrap(), &["ABC", "DEF"]);
1065            assert_eq!(
1066                cmd._leaked_strs,
1067                &[
1068                    "/path/to/app",
1069                    "--foo",
1070                    "--bar=ABC",
1071                    "baz",
1072                    "--bar=DEF",
1073                    "qux",
1074                    "quux",
1075                    "corge",
1076                    "foo",
1077                    "bar",
1078                ]
1079            );
1080            assert_eq!(cmd.opt_cfgs().len(), 2);
1081            assert_eq!(cmd.opt_cfgs()[0].store_key, "");
1082            assert_eq!(cmd.opt_cfgs()[0].names, &["foo"]);
1083            assert_eq!(cmd.opt_cfgs()[0].has_arg, false);
1084            assert_eq!(cmd.opt_cfgs()[0].is_array, false);
1085            assert_eq!(cmd.opt_cfgs()[0].defaults, None);
1086            assert_eq!(cmd.opt_cfgs()[0].desc, "");
1087            assert_eq!(cmd.opt_cfgs()[0].arg_in_help, "");
1088            assert_eq!(cmd.opt_cfgs()[1].store_key, "");
1089            assert_eq!(cmd.opt_cfgs()[1].names, &["bar"]);
1090            assert_eq!(cmd.opt_cfgs()[1].has_arg, true);
1091            assert_eq!(cmd.opt_cfgs()[1].is_array, true);
1092            assert_eq!(cmd.opt_cfgs()[1].defaults, None);
1093            assert_eq!(cmd.opt_cfgs()[1].desc, "");
1094            assert_eq!(cmd.opt_cfgs()[1].arg_in_help, "");
1095        }
1096
1097        #[test]
1098        fn should_move_by_mem_swap() {
1099            fn move_cmd() -> Cmd<'static> {
1100                let cfgs = vec![
1101                    OptCfg::with([names(&["foo"])]),
1102                    OptCfg::with([names(&["bar"]), has_arg(true), is_array(true)]),
1103                ];
1104
1105                let mut cmd = Cmd::with_strings([
1106                    "/path/to/app".to_string(),
1107                    "--foo".to_string(),
1108                    "--bar=ABC".to_string(),
1109                    "baz".to_string(),
1110                    "--bar=DEF".to_string(),
1111                    "qux".to_string(),
1112                    "quux".to_string(),
1113                    "corge".to_string(),
1114                ]);
1115                let _ = cmd.parse_with(cfgs);
1116
1117                let mut cmd1 = Cmd::with_strings([]);
1118                let _ = std::mem::swap(&mut cmd1, &mut cmd);
1119                cmd1
1120            }
1121
1122            let cmd = move_cmd();
1123            assert_eq!(cmd.name(), "app");
1124            assert_eq!(cmd.args(), &["baz", "qux", "quux", "corge"]);
1125            assert_eq!(cmd.opt_args("foo").unwrap(), &Vec::<&str>::new());
1126            assert_eq!(cmd.opt_args("bar").unwrap(), &["ABC", "DEF"]);
1127            assert_eq!(
1128                cmd._leaked_strs,
1129                &[
1130                    "/path/to/app",
1131                    "--foo",
1132                    "--bar=ABC",
1133                    "baz",
1134                    "--bar=DEF",
1135                    "qux",
1136                    "quux",
1137                    "corge",
1138                    "foo",
1139                    "bar",
1140                ]
1141            );
1142            assert_eq!(cmd.opt_cfgs().len(), 2);
1143            assert_eq!(cmd.opt_cfgs()[0].store_key, "");
1144            assert_eq!(cmd.opt_cfgs()[0].names, &["foo"]);
1145            assert_eq!(cmd.opt_cfgs()[0].has_arg, false);
1146            assert_eq!(cmd.opt_cfgs()[0].is_array, false);
1147            assert_eq!(cmd.opt_cfgs()[0].defaults, None);
1148            assert_eq!(cmd.opt_cfgs()[0].desc, "");
1149            assert_eq!(cmd.opt_cfgs()[0].arg_in_help, "");
1150            assert_eq!(cmd.opt_cfgs()[1].store_key, "");
1151            assert_eq!(cmd.opt_cfgs()[1].names, &["bar"]);
1152            assert_eq!(cmd.opt_cfgs()[1].has_arg, true);
1153            assert_eq!(cmd.opt_cfgs()[1].is_array, true);
1154            assert_eq!(cmd.opt_cfgs()[1].defaults, None);
1155            assert_eq!(cmd.opt_cfgs()[1].desc, "");
1156            assert_eq!(cmd.opt_cfgs()[1].arg_in_help, "");
1157        }
1158    }
1159}