From 39e91de4025bc3cd6b65393bfaff49ca6a91a7fa Mon Sep 17 00:00:00 2001 From: Keiichi Watanabe Date: Wed, 15 Dec 2021 01:39:42 +0900 Subject: [PATCH 1/2] Add helpflag() to `Options` When we add a flag with `reqopt()`, the mandatory flag is always required. This behavior doesn't work well when we want to add a `help` flag, which should work without the required flag is not specified like `my_command --help`. So, let's add `helpflag()` method, which add an optional flag "help" which will allow `parse()` to skip checking the existence of mandatory flags. --- src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++---- src/tests/mod.rs | 21 ++++++++++---- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 378c90b3..3f010854 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,7 @@ impl Options { desc: desc.to_string(), hasarg, occur, + is_help: false, }); self } @@ -215,6 +216,7 @@ impl Options { desc: desc.to_string(), hasarg: No, occur: Optional, + is_help: false, }); self } @@ -245,6 +247,7 @@ impl Options { desc: desc.to_string(), hasarg: No, occur: Multi, + is_help: false, }); self } @@ -285,6 +288,7 @@ impl Options { desc: desc.to_string(), hasarg: Maybe, occur: Optional, + is_help: false, }); self } @@ -327,6 +331,7 @@ impl Options { desc: desc.to_string(), hasarg: Yes, occur: Multi, + is_help: false, }); self } @@ -368,6 +373,7 @@ impl Options { desc: desc.to_string(), hasarg: Yes, occur: Optional, + is_help: false, }); self } @@ -411,6 +417,41 @@ impl Options { desc: desc.to_string(), hasarg: Yes, occur: Req, + is_help: false, + }); + self + } + + /// Create an optional flag `-h`/`--help` that does not take an argument and + /// works even if an required flag option is missing. + /// + /// * `desc` - Description for usage help. + /// + /// # Example + /// + /// ``` + /// # use getopts::Options; + /// let mut opts = Options::new(); + /// opts.helpflag("print this help menu"); + /// opts.reqopt("m", "mandatory", "madatory text option", "TEXT"); + /// + /// let result = opts.parse(&["--help"]); + /// // Success even without the required option. + /// assert!(result.is_ok()); + /// + /// let matches = opts.parse(&["-h", "--mandatory", "foo"]).unwrap(); + /// assert!(matches.opt_present("h")); + /// assert!(matches.opt_present("m")); + /// ``` + pub fn helpflag(&mut self, desc: &str) -> &mut Options { + self.grps.push(OptGroup { + short_name: "h".to_string(), + long_name: "help".to_string(), + hint: "".to_string(), + desc: desc.to_string(), + hasarg: No, + occur: Optional, + is_help: true, }); self } @@ -529,9 +570,7 @@ impl Options { // FloatingFrees is in use. if let Some(i_arg) = i_arg.take() { vals[opt_id].push((arg_pos, Val(i_arg))); - } else if was_long - || args.peek().map_or(true, |n| is_arg(&n)) - { + } else if was_long || args.peek().map_or(true, |n| is_arg(&n)) { vals[opt_id].push((arg_pos, Given)); } else { vals[opt_id].push((arg_pos, Val(args.next().unwrap()))); @@ -552,8 +591,14 @@ impl Options { arg_pos += 1; } debug_assert_eq!(vals.len(), opts.len()); + let help_exists = opts + .iter() + .zip(vals.iter()) + .any(|(opt, vs)| opt.is_help && !vs.is_empty()); for (vals, opt) in vals.iter().zip(opts.iter()) { - if opt.occur == Req && vals.is_empty() { + println!("vals={:?}, opt={:?}", vals, opt); + + if !help_exists && opt.occur == Req && vals.is_empty() { return Err(OptionMissing(opt.name.to_string())); } if opt.occur != Multi && vals.len() > 1 { @@ -565,7 +610,12 @@ impl Options { // in option does not exist in `free` and must be replaced with `None` args_end = args_end.filter(|pos| pos != &free.len()); - Ok(Matches { opts, vals, free, args_end }) + Ok(Matches { + opts, + vals, + free, + args_end, + }) } /// Derive a short one-line usage summary from a set of long options. @@ -749,6 +799,8 @@ struct Opt { hasarg: HasArg, /// How often it can occur occur: Occur, + /// Whether this option is a help flag. + is_help: bool, /// Which options it aliases aliases: Vec, } @@ -769,6 +821,8 @@ struct OptGroup { hasarg: HasArg, /// How often it can occur occur: Occur, + /// Whether this option is a help flag. + is_help: bool, } /// Describes whether an option is given at all or has a value. @@ -842,6 +896,7 @@ impl OptGroup { long_name, hasarg, occur, + is_help, .. } = (*self).clone(); @@ -851,22 +906,26 @@ impl OptGroup { name: Long(long_name), hasarg, occur, + is_help, aliases: Vec::new(), }, (1, 0) => Opt { name: Short(short_name.as_bytes()[0] as char), hasarg, occur, + is_help, aliases: Vec::new(), }, (1, _) => Opt { name: Long(long_name), hasarg, occur, + is_help, aliases: vec![Opt { name: Short(short_name.as_bytes()[0] as char), hasarg: hasarg, occur: occur, + is_help: false, aliases: Vec::new(), }], }, @@ -915,7 +974,10 @@ impl Matches { /// /// This function will panic if the option name is not defined. pub fn opt_positions(&self, name: &str) -> Vec { - self.opt_vals(name).into_iter().map(|(pos, _)| pos).collect() + self.opt_vals(name) + .into_iter() + .map(|(pos, _)| pos) + .collect() } /// Returns true if any of several options were matched. diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f1eb9410..d333ba77 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -248,7 +248,7 @@ fn test_free_trailing_only() { #[test] fn test_free_trailing_args() { - let args = vec!["pre".to_owned(), "--".to_owned(), "post".to_owned() ]; + let args = vec!["pre".to_owned(), "--".to_owned(), "post".to_owned()]; match Options::new().parse(&args) { Ok(ref m) => { assert_eq!(m.free_trailing_start(), Some(1)); @@ -741,10 +741,7 @@ fn test_multi() { ); assert_eq!(matches_both.opts_str_first(&["e"]).unwrap(), "foo"); - assert_eq!( - matches_both.opts_str_first(&["encrypt"]).unwrap(), - "bar" - ); + assert_eq!(matches_both.opts_str_first(&["encrypt"]).unwrap(), "bar"); assert_eq!( matches_both.opts_str_first(&["e", "encrypt"]).unwrap(), "foo" @@ -797,12 +794,14 @@ fn test_long_to_short() { name: Name::Long("banana".to_string()), hasarg: HasArg::Yes, occur: Occur::Req, + is_help: false, aliases: Vec::new(), }; short.aliases = vec![Opt { name: Name::Short('b'), hasarg: HasArg::Yes, occur: Occur::Req, + is_help: false, aliases: Vec::new(), }]; let mut opts = Options::new(); @@ -1319,3 +1318,15 @@ fn test_opt_strs_pos() { ] ); } + +#[test] +fn test_reqopt_with_help() { + let mut opts = Options::new(); + opts.helpflag("Description"); + opts.reqopt("r", "required", "Description", "TEST"); + + match opts.parse(["--help"]) { + Ok(_) => (), + Err(e) => panic!("{}", e), + }; +} From 44a2da3700b55ebeb543b8d1a9077f2470bc960e Mon Sep 17 00:00:00 2001 From: Keiichi Watanabe Date: Wed, 15 Dec 2021 02:15:21 +0900 Subject: [PATCH 2/2] Don't use "help" as sample data unrelated to helpflag() Since `helpflag()` method was added, we shouldn't use "help" as a placeholder in tests unrelated to the helpflag() method. --- src/lib.rs | 44 ++++++++++++++++++++++---------------------- src/tests/mod.rs | 24 ++++++++++++------------ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3f010854..8ba31361 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ //! //! let mut opts = Options::new(); //! opts.optopt("o", "", "set output file name", "NAME"); -//! opts.optflag("h", "help", "print this help menu"); +//! opts.helpflag("print this help menu"); //! let matches = match opts.parse(&args[1..]) { //! Ok(m) => { m } //! Err(f) => { panic!(f.to_string()) } @@ -193,8 +193,8 @@ impl Options { /// Create a long option that is optional and does not take an argument. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// /// # Example @@ -202,10 +202,10 @@ impl Options { /// ``` /// # use getopts::Options; /// let mut opts = Options::new(); - /// opts.optflag("h", "help", "help flag"); + /// opts.optflag("f", "foo", "sample flag"); /// - /// let matches = opts.parse(&["-h"]).unwrap(); - /// assert!(matches.opt_present("h")); + /// let matches = opts.parse(&["-f"]).unwrap(); + /// assert!(matches.opt_present("f")); /// ``` pub fn optflag(&mut self, short_name: &str, long_name: &str, desc: &str) -> &mut Options { validate_names(short_name, long_name); @@ -224,8 +224,8 @@ impl Options { /// Create a long option that can occur more than once and does not /// take an argument. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// /// # Example @@ -254,8 +254,8 @@ impl Options { /// Create a long option that is optional and takes an optional argument. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// * `hint` - Hint that is used in place of the argument in the usage help, /// e.g. `"FILE"` for a `-o FILE` option @@ -296,8 +296,8 @@ impl Options { /// Create a long option that is optional, takes an argument, and may occur /// multiple times. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// * `hint` - Hint that is used in place of the argument in the usage help, /// e.g. `"FILE"` for a `-o FILE` option @@ -338,8 +338,8 @@ impl Options { /// Create a long option that is optional and takes an argument. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// * `hint` - Hint that is used in place of the argument in the usage help, /// e.g. `"FILE"` for a `-o FILE` option @@ -380,8 +380,8 @@ impl Options { /// Create a long option that is required and takes an argument. /// - /// * `short_name` - e.g. `"h"` for a `-h` option, or `""` for none - /// * `long_name` - e.g. `"help"` for a `--help` option, or `""` for none + /// * `short_name` - e.g. `"f"` for a `-f` option, or `""` for none + /// * `long_name` - e.g. `"foo"` for a `--foo` option, or `""` for none /// * `desc` - Description for usage help /// * `hint` - Hint that is used in place of the argument in the usage help, /// e.g. `"FILE"` for a `-o FILE` option @@ -450,7 +450,7 @@ impl Options { hint: "".to_string(), desc: desc.to_string(), hasarg: No, - occur: Optional, + occur: Multi, is_help: true, }); self @@ -761,10 +761,10 @@ pub enum ParsingStyle { #[derive(Clone, Debug, PartialEq, Eq)] enum Name { /// A string representing the long name of an option. - /// For example: "help" + /// For example: "verbose" Long(String), /// A char representing the short name of an option. - /// For example: 'h' + /// For example: 'v' Short(char), } @@ -805,13 +805,13 @@ struct Opt { aliases: Vec, } -/// One group of options, e.g., both `-h` and `--help`, along with +/// One group of options, e.g., both `-v` and `--verbose`, along with /// their shared description and properties. #[derive(Debug, Clone, PartialEq, Eq)] struct OptGroup { - /// Short name of the option, e.g. `h` for a `-h` option + /// Short name of the option, e.g. `v` for a `-v` option short_name: String, - /// Long name of the option, e.g. `help` for a `--help` option + /// Long name of the option, e.g. `verbose` for a `--verbose` option long_name: String, /// Hint for argument, e.g. `FILE` for a `-o FILE` option hint: String, diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d333ba77..965fe958 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1133,7 +1133,7 @@ fn test_long_only_mode() { fn test_long_only_mode_no_short_parse() { let mut opts = Options::new(); opts.long_only(true); - opts.optflag("h", "help", "Description"); + opts.optflag("h", "hello", "Description"); opts.optflag("i", "ignore", "Description"); opts.optflag("", "hi", "Description"); @@ -1154,7 +1154,7 @@ fn test_normal_mode_no_long_parse() { // happens. let mut opts = Options::new(); opts.long_only(true); - opts.optflag("h", "help", "Description"); + opts.optflag("h", "hello", "Description"); opts.optflag("i", "ignore", "Description"); opts.optflag("", "hi", "Description"); opts.long_only(false); @@ -1180,8 +1180,8 @@ fn test_long_name_too_short() { #[should_panic] fn test_undefined_opt_present() { let mut opts = Options::new(); - opts.optflag("h", "help", "Description"); - let args = vec!["-h"]; + opts.optflag("v", "verbose", "Description"); + let args = vec!["-v"]; match opts.parse(args) { Ok(matches) => assert!(!matches.opt_present("undefined")), Err(e) => panic!("{}", e), @@ -1191,7 +1191,7 @@ fn test_undefined_opt_present() { #[test] fn test_opt_default() { let mut opts = Options::new(); - opts.optflag("h", "help", "Description"); + opts.optflag("v", "verbose", "Description"); opts.optflag("i", "ignore", "Description"); opts.optflag("r", "run", "Description"); opts.long_only(false); @@ -1201,14 +1201,14 @@ fn test_opt_default() { Ok(m) => m, Err(e) => panic!("{}", e), }; - assert_eq!(matches.opt_default("help", ""), None); + assert_eq!(matches.opt_default("verbose", ""), None); assert_eq!(matches.opt_default("i", "def"), Some("def".to_string())); } #[test] fn test_opt_get() { let mut opts = Options::new(); - opts.optflag("h", "help", "Description"); + opts.optflag("v", "verbose", "Description"); opts.optflagopt("i", "ignore", "Description", "true | false"); opts.optflagopt("r", "run", "Description", "0 .. 10"); opts.optflagopt("p", "percent", "Description", "0.0 .. 10.0"); @@ -1222,8 +1222,8 @@ fn test_opt_get() { Ok(m) => m, Err(e) => panic!("{}", e), }; - let h_arg = matches.opt_get::("help"); - assert_eq!(h_arg, Ok(None)); + let v_arg = matches.opt_get::("verbose"); + assert_eq!(v_arg, Ok(None)); let i_arg = matches.opt_get("i"); assert_eq!(i_arg, Ok(Some(true))); let p_arg = matches.opt_get("p"); @@ -1233,7 +1233,7 @@ fn test_opt_get() { #[test] fn test_opt_get_default() { let mut opts = Options::new(); - opts.optflag("h", "help", "Description"); + opts.optflag("v", "verbose", "Description"); opts.optflagopt("i", "ignore", "Description", "true | false"); opts.optflagopt("r", "run", "Description", "0 .. 10"); opts.optflagopt("p", "percent", "Description", "0.0 .. 10.0"); @@ -1247,8 +1247,8 @@ fn test_opt_get_default() { Ok(m) => m, Err(e) => panic!("{}", e), }; - let h_arg = matches.opt_get_default("help", 10); - assert_eq!(h_arg, Ok(10)); + let v_arg = matches.opt_get_default("verbose", 10); + assert_eq!(v_arg, Ok(10)); let i_arg = matches.opt_get_default("i", false); assert_eq!(i_arg, Ok(true)); let p_arg = matches.opt_get_default("p", 10.2);