From e5f29116035281f017957f2f8b83943f07f7a2a1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 16:44:25 -0500 Subject: [PATCH 1/7] fix(derive): Reference correct path --- clap_derive/src/attrs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index a3107001..f0cc9c9a 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -958,7 +958,7 @@ impl Parser { ), ParserKind::FromFlag => Method::new( func, - quote_spanned! { self.kind.span()=> clap::ValueParser::bool()}, + quote_spanned! { self.kind.span()=> clap::builder::ValueParser::bool()}, ), } } From cccc88bcc2fc4feeaaa9786266e8280328a1df5d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 14:53:18 -0500 Subject: [PATCH 2/7] fix(derive): Fix typo in error messages --- clap_derive/src/attrs.rs | 38 +++++++++---------- .../derive_ui/parse_with_value_parser.stderr | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index f0cc9c9a..c9f1979c 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -64,10 +64,10 @@ impl Attrs { res.push_attrs(attrs); res.push_doc_comment(attrs, "about"); - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is only allowed on fields" + value_parser.span(), + "`value_parser` attribute is only allowed on fields" ); } if let Some(parser) = res.parser.as_ref() { @@ -104,10 +104,10 @@ impl Attrs { match &*res.kind { Kind::Flatten => { - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is only allowed flattened entry" + value_parser.span(), + "`value_parser` attribute is not allowed for flattened entry" ); } if let Some(parser) = res.parser.as_ref() { @@ -130,10 +130,10 @@ impl Attrs { Kind::ExternalSubcommand => (), Kind::Subcommand(_) => { - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is only allowed for subcommand" + value_parser.span(), + "`value_parser` attribute is not allowed for subcommand" ); } if let Some(parser) = res.parser.as_ref() { @@ -204,10 +204,10 @@ impl Attrs { res.push_attrs(&variant.attrs); res.push_doc_comment(&variant.attrs, "help"); - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is only allowed on fields" + value_parser.span(), + "`value_parser` attribute is only allowed on fields" ); } if let Some(parser) = res.parser.as_ref() { @@ -244,10 +244,10 @@ impl Attrs { match &*res.kind { Kind::Flatten => { - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is not allowed for flattened entry" + value_parser.span(), + "`value_parser` attribute is not allowed for flattened entry" ); } if let Some(parser) = res.parser.as_ref() { @@ -274,10 +274,10 @@ impl Attrs { } Kind::Subcommand(_) => { - if let Some(parser) = res.value_parser.as_ref() { + if let Some(value_parser) = res.value_parser.as_ref() { abort!( - parser.span(), - "`value_parse` attribute is not allowed for subcommand" + value_parser.span(), + "`value_parser` attribute is not allowed for subcommand" ); } if let Some(parser) = res.parser.as_ref() { @@ -330,7 +330,7 @@ impl Attrs { if let Some(value_parser) = res.value_parser.as_ref() { abort!( value_parser.span(), - "`value_parse` attribute conflicts with `parse` attribute" + "`value_parser` attribute conflicts with `parse` attribute" ); } match *ty { diff --git a/tests/derive_ui/parse_with_value_parser.stderr b/tests/derive_ui/parse_with_value_parser.stderr index 9b626352..1f3e33e2 100644 --- a/tests/derive_ui/parse_with_value_parser.stderr +++ b/tests/derive_ui/parse_with_value_parser.stderr @@ -1,4 +1,4 @@ -error: `value_parse` attribute conflicts with `parse` attribute +error: `value_parser` attribute conflicts with `parse` attribute --> tests/derive_ui/parse_with_value_parser.rs:14:29 | 14 | #[clap(parse(from_str), value_parser = clap::value_parser!(String))] From ebf9c16a23fdb4f23fdc354b1d16bb5a42dd9ee6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 14:58:38 -0500 Subject: [PATCH 3/7] refactor(derive): Clarify functions role --- clap_derive/src/attrs.rs | 2 +- clap_derive/src/derives/args.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index c9f1979c..dbe8115b 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -721,7 +721,7 @@ impl Attrs { .unwrap_or_else(|| self.parser(field_type).value_parser()) } - pub fn custom_value_parser(&self) -> bool { + pub fn ignore_parser(&self) -> bool { self.value_parser.is_some() } diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index afd90c60..54ef3999 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -251,7 +251,7 @@ pub fn gen_augment( let func = &parser.func; let validator = match *parser.kind { - _ if attrs.custom_value_parser() || attrs.is_enum() => quote!(), + _ if attrs.ignore_parser() || attrs.is_enum() => quote!(), ParserKind::TryFromStr => quote_spanned! { func.span()=> .validator(|s| { #func(s) @@ -268,7 +268,7 @@ pub fn gen_augment( }; let value_name = attrs.value_name(); - let possible_values = if attrs.is_enum() && !attrs.custom_value_parser() { + let possible_values = if attrs.is_enum() && !attrs.ignore_parser() { gen_arg_enum_possible_values(convert_type) } else { quote!() @@ -537,7 +537,7 @@ fn gen_parsers( quote!(|s| ::std::ops::Deref::deref(s)), func.clone(), ), - _ if attrs.custom_value_parser() => ( + _ if attrs.ignore_parser() => ( quote_spanned!(span=> remove_one::<#convert_type>), quote_spanned!(span=> remove_many::<#convert_type>), quote!(|s| s), @@ -568,7 +568,7 @@ fn gen_parsers( quote_spanned!(func.span()=> |s| #func(s).map_err(|err| clap::Error::raw(clap::ErrorKind::ValueValidation, format!("Invalid value for {}: {}", #id, err)))), ), }; - if attrs.is_enum() && !attrs.custom_value_parser() { + if attrs.is_enum() && !attrs.ignore_parser() { let ci = attrs.ignore_case(); parse = quote_spanned! { convert_type.span()=> From f4004b653ba43e1f36c598e3046b67bcfd4d81b4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 15:08:55 -0500 Subject: [PATCH 4/7] refactor(derive): Give new-style highest precedence This exposed a potential bug but I figure it isn't worth fixing without actions. --- clap_derive/src/derives/args.rs | 12 ++++++------ examples/tutorial_derive/01_quick.rs | 2 +- examples/tutorial_derive/03_01_flag_bool.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index 54ef3999..f0501979 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -525,6 +525,12 @@ fn gen_parsers( let convert_type = inner_type(&field.ty); let id = attrs.id(); let (get_one, get_many, deref, mut parse) = match *parser.kind { + _ if attrs.ignore_parser() => ( + quote_spanned!(span=> remove_one::<#convert_type>), + quote_spanned!(span=> remove_many::<#convert_type>), + quote!(|s| s), + quote_spanned!(func.span()=> |s| ::std::result::Result::Ok::<_, clap::Error>(s)), + ), FromOccurrences => ( quote_spanned!(span=> occurrences_of), quote!(), @@ -537,12 +543,6 @@ fn gen_parsers( quote!(|s| ::std::ops::Deref::deref(s)), func.clone(), ), - _ if attrs.ignore_parser() => ( - quote_spanned!(span=> remove_one::<#convert_type>), - quote_spanned!(span=> remove_many::<#convert_type>), - quote!(|s| s), - quote_spanned!(func.span()=> |s| ::std::result::Result::Ok::<_, clap::Error>(s)), - ), FromStr => ( quote_spanned!(span=> get_one::), quote_spanned!(span=> get_many::), diff --git a/examples/tutorial_derive/01_quick.rs b/examples/tutorial_derive/01_quick.rs index 7bc55b2a..b78bf782 100644 --- a/examples/tutorial_derive/01_quick.rs +++ b/examples/tutorial_derive/01_quick.rs @@ -26,7 +26,7 @@ enum Commands { /// does testing things Test { /// lists test values - #[clap(short, long, value_parser)] + #[clap(short, long)] list: bool, }, } diff --git a/examples/tutorial_derive/03_01_flag_bool.rs b/examples/tutorial_derive/03_01_flag_bool.rs index 34c9a352..8b574b74 100644 --- a/examples/tutorial_derive/03_01_flag_bool.rs +++ b/examples/tutorial_derive/03_01_flag_bool.rs @@ -3,7 +3,7 @@ use clap::Parser; #[derive(Parser)] #[clap(author, version, about, long_about = None)] struct Cli { - #[clap(short, long, value_parser)] + #[clap(short, long)] verbose: bool, } From 552e6feb3f0f0b2d82ff391145eee5e9b22cb5ad Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 15:38:16 -0500 Subject: [PATCH 5/7] fix(derive): Adjust precedence for flag/occurrence logic --- clap_derive/src/derives/args.rs | 51 +++++++++++++++++----------- tests/derive_ui/bool_arg_enum.stderr | 45 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index f0501979..98b83cc6 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -244,12 +244,12 @@ pub fn gen_augment( let convert_type = inner_type(&field.ty); let parser = attrs.parser(&field.ty); - let occurrences = *parser.kind == ParserKind::FromOccurrences; - let flag = *parser.kind == ParserKind::FromFlag; let value_parser = attrs.value_parser(&field.ty); let func = &parser.func; + let mut occurrences = false; + let mut flag = false; let validator = match *parser.kind { _ if attrs.ignore_parser() || attrs.is_enum() => quote!(), ParserKind::TryFromStr => quote_spanned! { func.span()=> @@ -261,10 +261,15 @@ pub fn gen_augment( ParserKind::TryFromOsStr => quote_spanned! { func.span()=> .validator_os(|s| #func(s).map(|_: #convert_type| ())) }, - ParserKind::FromStr - | ParserKind::FromOsStr - | ParserKind::FromFlag - | ParserKind::FromOccurrences => quote!(), + ParserKind::FromStr | ParserKind::FromOsStr => quote!(), + ParserKind::FromFlag => { + flag = true; + quote!() + } + ParserKind::FromOccurrences => { + occurrences = true; + quote!() + } }; let value_name = attrs.value_name(); @@ -524,6 +529,8 @@ fn gen_parsers( let span = parser.kind.span(); let convert_type = inner_type(&field.ty); let id = attrs.id(); + let mut flag = false; + let mut occurrences = false; let (get_one, get_many, deref, mut parse) = match *parser.kind { _ if attrs.ignore_parser() => ( quote_spanned!(span=> remove_one::<#convert_type>), @@ -531,18 +538,24 @@ fn gen_parsers( quote!(|s| s), quote_spanned!(func.span()=> |s| ::std::result::Result::Ok::<_, clap::Error>(s)), ), - FromOccurrences => ( - quote_spanned!(span=> occurrences_of), - quote!(), - quote!(|s| ::std::ops::Deref::deref(s)), - func.clone(), - ), - FromFlag => ( - quote!(), - quote!(), - quote!(|s| ::std::ops::Deref::deref(s)), - func.clone(), - ), + FromOccurrences => { + occurrences = true; + ( + quote_spanned!(span=> occurrences_of), + quote!(), + quote!(|s| ::std::ops::Deref::deref(s)), + func.clone(), + ) + } + FromFlag => { + flag = true; + ( + quote!(), + quote!(), + quote!(|s| ::std::ops::Deref::deref(s)), + func.clone(), + ) + } FromStr => ( quote_spanned!(span=> get_one::), quote_spanned!(span=> get_many::), @@ -576,8 +589,6 @@ fn gen_parsers( } } - let flag = *parser.kind == ParserKind::FromFlag; - let occurrences = *parser.kind == ParserKind::FromOccurrences; // Give this identifier the same hygiene // as the `arg_matches` parameter definition. This // allows us to refer to `arg_matches` within a `quote_spanned` block diff --git a/tests/derive_ui/bool_arg_enum.stderr b/tests/derive_ui/bool_arg_enum.stderr index 9ac13d08..de944a6a 100644 --- a/tests/derive_ui/bool_arg_enum.stderr +++ b/tests/derive_ui/bool_arg_enum.stderr @@ -20,3 +20,48 @@ help: `bool` is a unit variant, you need to write it without the parenthesis | 7 | opts: bool, | ~~~~ + +warning: use of deprecated associated function `clap::Arg::<'help>::possible_values`: Replaced with `Arg::value_parser(PossibleValuesParser::new(...)).takes_value(true)` + --> tests/derive_ui/bool_arg_enum.rs:7:11 + | +7 | opts: bool, + | ^^^^ + | + = note: `#[warn(deprecated)]` on by default + +error[E0277]: the trait bound `bool: ArgEnum` is not satisfied + --> tests/derive_ui/bool_arg_enum.rs:7:11 + | +7 | opts: bool, + | ^^^^ the trait `ArgEnum` is not implemented for `bool` + | +note: required by `value_variants` + --> src/derive.rs + | + | fn value_variants<'a>() -> &'a [Self]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0277]: the trait bound `bool: ArgEnum` is not satisfied + --> tests/derive_ui/bool_arg_enum.rs:6:5 + | +6 | / #[clap(short, arg_enum)] +7 | | opts: bool, + | |______________^ the trait `ArgEnum` is not implemented for `bool` + | +note: required by a bound in `ArgEnum` + --> src/derive.rs + | + | / pub trait ArgEnum: Sized + Clone { + | | /// All possible argument values, in display order. + | | fn value_variants<'a>() -> &'a [Self]; + | | +... | + | | fn to_possible_value<'a>(&self) -> Option>; + | | } + | |_^ required by this bound in `ArgEnum` + +warning: use of deprecated associated function `clap::Arg::<'help>::possible_values`: Replaced with `Arg::value_parser(PossibleValuesParser::new(...)).takes_value(true)` + --> tests/derive_ui/bool_arg_enum.rs:7:11 + | +7 | opts: bool, + | ^^^^ From 002d4421e57b35b9284fa32382d0d6a0f458e179 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 16:32:47 -0500 Subject: [PATCH 6/7] Revert "fix(parser): Don't treat missing values as missing args" This reverts commit 50f4018dcfb1f33cb092780e27f14bfed5b0ed2c. This broke compatibility with the derive when dealing with `Option>` and related cases. --- src/parser/error.rs | 8 -------- src/parser/matches/arg_matches.rs | 7 ++----- tests/builder/env.rs | 5 +---- tests/builder/groups.rs | 5 +---- tests/builder/ignore_errors.rs | 15 +++------------ 5 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/parser/error.rs b/src/parser/error.rs index e23ee5e3..bdafa9ae 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -18,11 +18,6 @@ pub enum MatchesError { UnknownArgument { // Missing `id` but blocked on a public id type which will hopefully come with `unstable-v4` }, - /// Present argument must have one value - #[non_exhaustive] - ExpectedOne { - // Missing `id` but blocked on a public id type which will hopefully come with `unstable-v4` - }, } impl MatchesError { @@ -56,9 +51,6 @@ impl std::fmt::Display for MatchesError { Self::UnknownArgument {} => { writeln!(f, "Unknown argument or group id. Make sure you are using the argument id and not the short or long flags") } - Self::ExpectedOne {} => { - writeln!(f, "Present argument must have one value. Make sure you are using the correct lookup (`get_one` vs `get_many`)") - } } } } diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 0df555f3..6b27174f 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -1063,11 +1063,8 @@ impl ArgMatches { ) -> Result, MatchesError> { let id = Id::from(name); let arg = self.try_get_arg_t::(&id)?; - let value = match arg.map(|a| a.first()) { - Some(Some(value)) => value, - Some(None) => { - return Err(MatchesError::ExpectedOne {}); - } + let value = match arg.and_then(|a| a.first()) { + Some(value) => value, None => { return Ok(None); } diff --git a/tests/builder/env.rs b/tests/builder/env.rs index 14a37bbc..28129df3 100644 --- a/tests/builder/env.rs +++ b/tests/builder/env.rs @@ -38,10 +38,7 @@ fn env_bool_literal() { let m = r.unwrap(); assert!(m.is_present("present")); assert_eq!(m.occurrences_of("present"), 0); - assert!(matches!( - m.try_get_one::("present").unwrap_err(), - clap::parser::MatchesError::ExpectedOne { .. } - )); + assert_eq!(m.get_one::("present").map(|v| v.as_str()), None); assert!(!m.is_present("negated")); assert!(!m.is_present("absent")); } diff --git a/tests/builder/groups.rs b/tests/builder/groups.rs index 9ea39ada..2247b989 100644 --- a/tests/builder/groups.rs +++ b/tests/builder/groups.rs @@ -111,10 +111,7 @@ fn group_single_flag() { let m = res.unwrap(); assert!(m.is_present("grp")); - assert!(matches!( - m.try_get_one::("grp").unwrap_err(), - clap::parser::MatchesError::ExpectedOne { .. } - )); + assert!(m.get_one::("grp").map(|v| v.as_str()).is_none()); } #[test] diff --git a/tests/builder/ignore_errors.rs b/tests/builder/ignore_errors.rs index 6b29bc0f..547320c5 100644 --- a/tests/builder/ignore_errors.rs +++ b/tests/builder/ignore_errors.rs @@ -49,10 +49,7 @@ fn multiple_args_and_final_arg_without_value() { Some("file") ); assert!(m.is_present("f")); - assert!(matches!( - m.try_get_one::("stuff").unwrap_err(), - clap::parser::MatchesError::ExpectedOne { .. } - )); + assert_eq!(m.get_one::("stuff").map(|v| v.as_str()), None); } #[test] @@ -79,10 +76,7 @@ fn multiple_args_and_intermittent_arg_without_value() { Some("file") ); assert!(m.is_present("f")); - assert!(matches!( - m.try_get_one::("stuff").unwrap_err(), - clap::parser::MatchesError::ExpectedOne { .. } - )); + assert_eq!(m.get_one::("stuff").map(|v| v.as_str()), None); } #[test] @@ -124,10 +118,7 @@ fn subcommand() { sub_m.is_present("test"), "expected subcommand to be present due to partial parsing" ); - assert!(matches!( - sub_m.try_get_one::("test").unwrap_err(), - clap::parser::MatchesError::ExpectedOne { .. } - )); + assert_eq!(sub_m.get_one::("test").map(|v| v.as_str()), None); assert_eq!( sub_m.get_one::("stuff").map(|v| v.as_str()), Some("some other val") From 5db611384e52e5d471c339b606672d649c2be53c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 2 Jun 2022 16:32:15 -0500 Subject: [PATCH 7/7] test(derive): Ensure we don't break compatibility --- tests/derive/legacy/app_name.rs | 97 +++ tests/derive/legacy/arg_enum.rs | 526 +++++++++++++++ tests/derive/legacy/arguments.rs | 120 ++++ tests/derive/legacy/author_version_about.rs | 51 ++ tests/derive/legacy/basic.rs | 51 ++ tests/derive/legacy/boxed.rs | 48 ++ tests/derive/legacy/custom_string_parsers.rs | 351 ++++++++++ tests/derive/legacy/default_value.rs | 57 ++ tests/derive/legacy/deny_warnings.rs | 53 ++ tests/derive/legacy/doc_comments_help.rs | 240 +++++++ .../legacy/explicit_name_no_renaming.rs | 36 ++ tests/derive/legacy/flags.rs | 189 ++++++ tests/derive/legacy/flatten.rs | 256 ++++++++ tests/derive/legacy/generic.rs | 145 +++++ tests/derive/legacy/help.rs | 473 ++++++++++++++ tests/derive/legacy/issues.rs | 130 ++++ tests/derive/legacy/macros.rs | 53 ++ tests/derive/legacy/mod.rs | 33 + tests/derive/legacy/naming.rs | 354 ++++++++++ tests/derive/legacy/nested_subcommands.rs | 194 ++++++ tests/derive/legacy/non_literal_attributes.rs | 156 +++++ tests/derive/legacy/options.rs | 422 ++++++++++++ tests/derive/legacy/privacy.rs | 36 ++ tests/derive/legacy/raw_bool_literal.rs | 29 + tests/derive/legacy/raw_idents.rs | 24 + tests/derive/legacy/rename_all_env.rs | 47 ++ tests/derive/legacy/skip.rs | 155 +++++ tests/derive/legacy/structopt.rs | 23 + tests/derive/legacy/subcommands.rs | 605 ++++++++++++++++++ tests/derive/legacy/type_alias_regressions.rs | 47 ++ tests/derive/legacy/utf8.rs | 226 +++++++ tests/derive/legacy/utils.rs | 56 ++ tests/derive/main.rs | 1 + 33 files changed, 5284 insertions(+) create mode 100644 tests/derive/legacy/app_name.rs create mode 100644 tests/derive/legacy/arg_enum.rs create mode 100644 tests/derive/legacy/arguments.rs create mode 100644 tests/derive/legacy/author_version_about.rs create mode 100644 tests/derive/legacy/basic.rs create mode 100644 tests/derive/legacy/boxed.rs create mode 100644 tests/derive/legacy/custom_string_parsers.rs create mode 100644 tests/derive/legacy/default_value.rs create mode 100644 tests/derive/legacy/deny_warnings.rs create mode 100644 tests/derive/legacy/doc_comments_help.rs create mode 100644 tests/derive/legacy/explicit_name_no_renaming.rs create mode 100644 tests/derive/legacy/flags.rs create mode 100644 tests/derive/legacy/flatten.rs create mode 100644 tests/derive/legacy/generic.rs create mode 100644 tests/derive/legacy/help.rs create mode 100644 tests/derive/legacy/issues.rs create mode 100644 tests/derive/legacy/macros.rs create mode 100644 tests/derive/legacy/mod.rs create mode 100644 tests/derive/legacy/naming.rs create mode 100644 tests/derive/legacy/nested_subcommands.rs create mode 100644 tests/derive/legacy/non_literal_attributes.rs create mode 100644 tests/derive/legacy/options.rs create mode 100644 tests/derive/legacy/privacy.rs create mode 100644 tests/derive/legacy/raw_bool_literal.rs create mode 100644 tests/derive/legacy/raw_idents.rs create mode 100644 tests/derive/legacy/rename_all_env.rs create mode 100644 tests/derive/legacy/skip.rs create mode 100644 tests/derive/legacy/structopt.rs create mode 100644 tests/derive/legacy/subcommands.rs create mode 100644 tests/derive/legacy/type_alias_regressions.rs create mode 100644 tests/derive/legacy/utf8.rs create mode 100644 tests/derive/legacy/utils.rs diff --git a/tests/derive/legacy/app_name.rs b/tests/derive/legacy/app_name.rs new file mode 100644 index 00000000..9e2f8d80 --- /dev/null +++ b/tests/derive/legacy/app_name.rs @@ -0,0 +1,97 @@ +use clap::CommandFactory; +use clap::Parser; +#[test] +fn app_name_in_short_help_from_struct() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + struct MyApp {} + + let mut help = Vec::new(); + MyApp::command().write_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("my-cmd")); +} + +#[test] +fn app_name_in_long_help_from_struct() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + struct MyApp {} + + let mut help = Vec::new(); + MyApp::command().write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("my-cmd")); +} + +#[test] +fn app_name_in_short_help_from_enum() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + enum MyApp {} + + let mut help = Vec::new(); + MyApp::command().write_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("my-cmd")); +} + +#[test] +fn app_name_in_long_help_from_enum() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + enum MyApp {} + + let mut help = Vec::new(); + MyApp::command().write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("my-cmd")); +} + +#[test] +fn app_name_in_short_version_from_struct() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + struct MyApp {} + + let version = MyApp::command().render_version(); + + assert!(version.contains("my-cmd")); +} + +#[test] +fn app_name_in_long_version_from_struct() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + struct MyApp {} + + let version = MyApp::command().render_long_version(); + + assert!(version.contains("my-cmd")); +} + +#[test] +fn app_name_in_short_version_from_enum() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + enum MyApp {} + + let version = MyApp::command().render_version(); + + assert!(version.contains("my-cmd")); +} + +#[test] +fn app_name_in_long_version_from_enum() { + #[derive(Parser)] + #[clap(name = "my-cmd")] + enum MyApp {} + + let version = MyApp::command().render_long_version(); + + assert!(version.contains("my-cmd")); +} diff --git a/tests/derive/legacy/arg_enum.rs b/tests/derive/legacy/arg_enum.rs new file mode 100644 index 00000000..1cd8ee0d --- /dev/null +++ b/tests/derive/legacy/arg_enum.rs @@ -0,0 +1,526 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +use clap::Parser; + +#[test] +fn basic() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Foo + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Bar + }, + Opt::try_parse_from(&["", "bar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "fOo"]).is_err()); +} + +#[test] +fn default_value() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + impl Default for ArgChoice { + fn default() -> Self { + Self::Bar + } + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, default_value_t)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Foo + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Bar + }, + Opt::try_parse_from(&["", "bar"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Bar + }, + Opt::try_parse_from(&[""]).unwrap() + ); +} + +#[test] +fn multi_word_is_renamed_kebab() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + #[allow(non_camel_case_types)] + enum ArgChoice { + FooBar, + BAR_BAZ, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::FooBar + }, + Opt::try_parse_from(&["", "foo-bar"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::BAR_BAZ + }, + Opt::try_parse_from(&["", "bar-baz"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "FooBar"]).is_err()); +} + +#[test] +fn variant_with_defined_casing() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + #[clap(rename_all = "screaming_snake")] + FooBar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::FooBar + }, + Opt::try_parse_from(&["", "FOO_BAR"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "FooBar"]).is_err()); +} + +#[test] +fn casing_is_propagated_from_parent() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + #[clap(rename_all = "screaming_snake")] + enum ArgChoice { + FooBar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::FooBar + }, + Opt::try_parse_from(&["", "FOO_BAR"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "FooBar"]).is_err()); +} + +#[test] +fn casing_propagation_is_overridden() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + #[clap(rename_all = "screaming_snake")] + enum ArgChoice { + #[clap(rename_all = "camel")] + FooBar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::FooBar + }, + Opt::try_parse_from(&["", "fooBar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "FooBar"]).is_err()); + assert!(Opt::try_parse_from(&["", "FOO_BAR"]).is_err()); +} + +#[test] +fn ignore_case() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, ignore_case(true))] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Foo + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Foo + }, + Opt::try_parse_from(&["", "fOo"]).unwrap() + ); +} + +#[test] +fn ignore_case_set_to_false() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, ignore_case(false))] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Foo + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "fOo"]).is_err()); +} + +#[test] +fn alias() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + #[clap(alias = "TOTP")] + Totp, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, ignore_case(false))] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Totp + }, + Opt::try_parse_from(&["", "totp"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Totp + }, + Opt::try_parse_from(&["", "TOTP"]).unwrap() + ); +} + +#[test] +fn multiple_alias() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + #[clap(alias = "TOTP", alias = "t")] + Totp, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, ignore_case(false))] + arg: ArgChoice, + } + + assert_eq!( + Opt { + arg: ArgChoice::Totp + }, + Opt::try_parse_from(&["", "totp"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Totp + }, + Opt::try_parse_from(&["", "TOTP"]).unwrap() + ); + assert_eq!( + Opt { + arg: ArgChoice::Totp + }, + Opt::try_parse_from(&["", "t"]).unwrap() + ); +} + +#[test] +fn skip_variant() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + #[allow(dead_code)] // silence warning about `Baz` being unused + enum ArgChoice { + Foo, + Bar, + #[clap(skip)] + Baz, + } + + assert_eq!( + ::value_variants() + .iter() + .map(clap::ArgEnum::to_possible_value) + .map(Option::unwrap) + .collect::>(), + vec![ + clap::PossibleValue::new("foo"), + clap::PossibleValue::new("bar") + ] + ); + + { + use clap::ArgEnum; + assert!(ArgChoice::from_str("foo", true).is_ok()); + assert!(ArgChoice::from_str("bar", true).is_ok()); + assert!(ArgChoice::from_str("baz", true).is_err()); + } +} + +#[test] +fn skip_non_unit_variant() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + #[allow(dead_code)] // silence warning about `Baz` being unused + enum ArgChoice { + Foo, + Bar, + #[clap(skip)] + Baz(usize), + } + + assert_eq!( + ::value_variants() + .iter() + .map(clap::ArgEnum::to_possible_value) + .map(Option::unwrap) + .collect::>(), + vec![ + clap::PossibleValue::new("foo"), + clap::PossibleValue::new("bar") + ] + ); + + { + use clap::ArgEnum; + assert!(ArgChoice::from_str("foo", true).is_ok()); + assert!(ArgChoice::from_str("bar", true).is_ok()); + assert!(ArgChoice::from_str("baz", true).is_err()); + } +} + +#[test] +fn from_str_invalid() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + } + + { + use clap::ArgEnum; + assert!(ArgChoice::from_str("bar", true).is_err()); + } +} + +#[test] +fn option_type() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum)] + arg: Option, + } + + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&[""]).unwrap()); + assert_eq!( + Opt { + arg: Some(ArgChoice::Foo) + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(ArgChoice::Bar) + }, + Opt::try_parse_from(&["", "bar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "fOo"]).is_err()); +} + +#[test] +fn option_option_type() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, long)] + arg: Option>, + } + + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&[""]).unwrap()); + assert_eq!( + Opt { arg: Some(None) }, + Opt::try_parse_from(&["", "--arg"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(Some(ArgChoice::Foo)) + }, + Opt::try_parse_from(&["", "--arg", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(Some(ArgChoice::Bar)) + }, + Opt::try_parse_from(&["", "--arg", "bar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "--arg", "fOo"]).is_err()); +} + +#[test] +fn vec_type() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, short, long)] + arg: Vec, + } + + assert_eq!(Opt { arg: vec![] }, Opt::try_parse_from(&[""]).unwrap()); + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo] + }, + Opt::try_parse_from(&["", "-a", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&["", "-a", "foo", "-a", "bar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "-a", "fOo"]).is_err()); +} + +#[test] +fn option_vec_type() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(arg_enum, short, long)] + arg: Option>, + } + + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&[""]).unwrap()); + assert_eq!( + Opt { + arg: Some(vec![ArgChoice::Foo]) + }, + Opt::try_parse_from(&["", "-a", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(vec![ArgChoice::Foo, ArgChoice::Bar]) + }, + Opt::try_parse_from(&["", "-a", "foo", "-a", "bar"]).unwrap() + ); + assert!(Opt::try_parse_from(&["", "-a", "fOo"]).is_err()); +} + +#[test] +fn vec_type_default_value() { + #[derive(clap::ArgEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + Baz, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap( + arg_enum, + short, + long, + default_value = "foo,bar", + value_delimiter = ',' + )] + arg: Vec, + } + + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&[""]).unwrap() + ); + + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo, ArgChoice::Baz] + }, + Opt::try_parse_from(&["", "-a", "foo,baz"]).unwrap() + ); +} diff --git a/tests/derive/legacy/arguments.rs b/tests/derive/legacy/arguments.rs new file mode 100644 index 00000000..57c5866d --- /dev/null +++ b/tests/derive/legacy/arguments.rs @@ -0,0 +1,120 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::CommandFactory; +use clap::Parser; + +#[test] +fn required_argument() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + arg: i32, + } + assert_eq!( + Opt { arg: 42 }, + Opt::try_parse_from(&["test", "42"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert!(Opt::try_parse_from(&["test", "42", "24"]).is_err()); +} + +#[test] +fn argument_with_default() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_value = "42")] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::try_parse_from(&["test", "24"]).unwrap() + ); + assert_eq!(Opt { arg: 42 }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "42", "24"]).is_err()); +} + +#[test] +fn auto_value_name() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + my_special_arg: i32, + } + + let mut help = Vec::new(); + Opt::command().write_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("MY_SPECIAL_ARG")); + // Ensure the implicit `num_vals` is just 1 + assert_eq!( + Opt { my_special_arg: 10 }, + Opt::try_parse_from(&["test", "10"]).unwrap() + ); +} + +#[test] +fn explicit_value_name() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(value_name = "BROWNIE_POINTS")] + my_special_arg: i32, + } + + let mut help = Vec::new(); + Opt::command().write_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + + assert!(help.contains("BROWNIE_POINTS")); + assert!(!help.contains("MY_SPECIAL_ARG")); + // Ensure the implicit `num_vals` is just 1 + assert_eq!( + Opt { my_special_arg: 10 }, + Opt::try_parse_from(&["test", "10"]).unwrap() + ); +} + +#[test] +fn option_type_is_optional() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + arg: Option, + } + assert_eq!( + Opt { arg: Some(42) }, + Opt::try_parse_from(&["test", "42"]).unwrap() + ); + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "42", "24"]).is_err()); +} + +#[test] +fn vec_type_is_multiple_values() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::try_parse_from(&["test", "24"]).unwrap() + ); + assert_eq!(Opt { arg: vec![] }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::try_parse_from(&["test", "24", "42"]).unwrap() + ); + assert_eq!( + clap::ErrorKind::ValueValidation, + Opt::try_parse_from(&["test", "NOPE"]).err().unwrap().kind() + ); +} diff --git a/tests/derive/legacy/author_version_about.rs b/tests/derive/legacy/author_version_about.rs new file mode 100644 index 00000000..3c68cd56 --- /dev/null +++ b/tests/derive/legacy/author_version_about.rs @@ -0,0 +1,51 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use crate::utils; + +use clap::Parser; + +#[test] +fn no_author_version_about() { + #[derive(Parser, PartialEq, Debug)] + #[clap(name = "foo")] + struct Opt {} + + let output = utils::get_long_help::(); + assert!(output.starts_with("foo \n\nUSAGE:")); +} + +#[test] +fn use_env() { + #[derive(Parser, PartialEq, Debug)] + #[clap(author, about, version)] + struct Opt {} + + let output = utils::get_long_help::(); + assert!(output.starts_with("clap")); + assert!(output + .contains("A simple to use, efficient, and full-featured Command Line Argument Parser")); +} + +#[test] +fn explicit_version_not_str_lit() { + const VERSION: &str = "custom version"; + + #[derive(Parser)] + #[clap(version = VERSION)] + pub struct Opt {} + + let output = utils::get_long_help::(); + assert!(output.contains("custom version")); +} diff --git a/tests/derive/legacy/basic.rs b/tests/derive/legacy/basic.rs new file mode 100644 index 00000000..d2e1ca90 --- /dev/null +++ b/tests/derive/legacy/basic.rs @@ -0,0 +1,51 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::Parser; + +#[test] +fn basic() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short = 'a', long = "arg")] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); +} + +#[test] +fn update_basic() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short = 'a', long = "arg")] + single_value: i32, + } + + let mut opt = Opt::try_parse_from(&["test", "-a0"]).unwrap(); + + opt.update_from(&["test", "-a42"]); + + assert_eq!(Opt { single_value: 42 }, opt); +} + +#[test] +fn unit_struct() { + #[derive(Parser, PartialEq, Debug)] + struct Opt; + + assert_eq!(Opt {}, Opt::try_parse_from(&["test"]).unwrap()); +} diff --git a/tests/derive/legacy/boxed.rs b/tests/derive/legacy/boxed.rs new file mode 100644 index 00000000..03efbe9c --- /dev/null +++ b/tests/derive/legacy/boxed.rs @@ -0,0 +1,48 @@ +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser, PartialEq, Debug)] +struct Opt { + #[clap(subcommand)] + sub: Box, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Sub { + Flame { + #[clap(flatten)] + arg: Box, + }, +} + +#[derive(Args, PartialEq, Debug)] +struct Ext { + arg: u32, +} + +#[test] +fn boxed_flatten_subcommand() { + assert_eq!( + Opt { + sub: Box::new(Sub::Flame { + arg: Box::new(Ext { arg: 1 }) + }) + }, + Opt::try_parse_from(&["test", "flame", "1"]).unwrap() + ); +} + +#[test] +fn update_boxed_flatten_subcommand() { + let mut opt = Opt::try_parse_from(&["test", "flame", "1"]).unwrap(); + + opt.update_from(&["test", "flame", "42"]); + + assert_eq!( + Opt { + sub: Box::new(Sub::Flame { + arg: Box::new(Ext { arg: 42 }) + }) + }, + opt + ); +} diff --git a/tests/derive/legacy/custom_string_parsers.rs b/tests/derive/legacy/custom_string_parsers.rs new file mode 100644 index 00000000..84279df3 --- /dev/null +++ b/tests/derive/legacy/custom_string_parsers.rs @@ -0,0 +1,351 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::Parser; + +use std::ffi::{CString, OsStr}; +use std::num::ParseIntError; +use std::path::PathBuf; + +#[derive(Parser, PartialEq, Debug)] +struct PathOpt { + #[clap(short, long, parse(from_os_str))] + path: PathBuf, + + #[clap(short, default_value = "../", parse(from_os_str))] + default_path: PathBuf, + + #[clap(short, parse(from_os_str), multiple_occurrences(true))] + vector_path: Vec, + + #[clap(short, parse(from_os_str))] + option_path_1: Option, + + #[clap(short = 'q', parse(from_os_str))] + option_path_2: Option, +} + +#[test] +fn test_path_opt_simple() { + assert_eq!( + PathOpt { + path: PathBuf::from("/usr/bin"), + default_path: PathBuf::from("../"), + vector_path: vec![ + PathBuf::from("/a/b/c"), + PathBuf::from("/d/e/f"), + PathBuf::from("/g/h/i"), + ], + option_path_1: None, + option_path_2: Some(PathBuf::from("j.zip")), + }, + PathOpt::try_parse_from(&[ + "test", "-p", "/usr/bin", "-v", "/a/b/c", "-v", "/d/e/f", "-v", "/g/h/i", "-q", + "j.zip", + ]) + .unwrap() + ); +} + +fn parse_hex(input: &str) -> Result { + u64::from_str_radix(input, 16) +} + +#[derive(Parser, PartialEq, Debug)] +struct HexOpt { + #[clap(short, parse(try_from_str = parse_hex))] + number: u64, +} + +#[test] +fn test_parse_hex() { + assert_eq!( + HexOpt { number: 5 }, + HexOpt::try_parse_from(&["test", "-n", "5"]).unwrap() + ); + assert_eq!( + HexOpt { + number: 0x00ab_cdef + }, + HexOpt::try_parse_from(&["test", "-n", "abcdef"]).unwrap() + ); + + let err = HexOpt::try_parse_from(&["test", "-n", "gg"]).unwrap_err(); + assert!( + err.to_string().contains("invalid digit found in string"), + "{}", + err + ); +} + +fn custom_parser_1(_: &str) -> &'static str { + "A" +} +#[derive(Debug)] +struct ErrCode(u32); +impl std::error::Error for ErrCode {} +impl std::fmt::Display for ErrCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} +fn custom_parser_2(_: &str) -> Result<&'static str, ErrCode> { + Ok("B") +} +fn custom_parser_3(_: &OsStr) -> &'static str { + "C" +} +fn custom_parser_4(_: &OsStr) -> Result<&'static str, String> { + Ok("D") +} + +#[derive(Parser, PartialEq, Debug)] +struct NoOpOpt { + #[clap(short, parse(from_str = custom_parser_1))] + a: &'static str, + #[clap(short, parse(try_from_str = custom_parser_2))] + b: &'static str, + #[clap(short, parse(from_os_str = custom_parser_3))] + c: &'static str, + #[clap(short, parse(try_from_os_str = custom_parser_4))] + d: &'static str, +} + +#[test] +fn test_every_custom_parser() { + assert_eq!( + NoOpOpt { + a: "A", + b: "B", + c: "C", + d: "D" + }, + NoOpOpt::try_parse_from(&["test", "-a=?", "-b=?", "-c=?", "-d=?"]).unwrap() + ); +} + +#[test] +fn update_every_custom_parser() { + let mut opt = NoOpOpt { + a: "0", + b: "0", + c: "0", + d: "D", + }; + + opt.try_update_from(&["test", "-a=?", "-b=?", "-d=?"]) + .unwrap(); + + assert_eq!( + NoOpOpt { + a: "A", + b: "B", + c: "0", + d: "D" + }, + opt + ); +} + +// Note: can't use `Vec` directly, as clap would instead look for +// conversion function from `&str` to `u8`. +type Bytes = Vec; + +#[derive(Parser, PartialEq, Debug)] +struct DefaultedOpt { + #[clap(short, parse(from_str))] + bytes: Bytes, + + #[clap(short, parse(try_from_str))] + integer: u64, + + #[clap(short, parse(from_os_str))] + path: PathBuf, +} + +#[test] +fn test_parser_with_default_value() { + assert_eq!( + DefaultedOpt { + bytes: b"E\xc2\xb2=p\xc2\xb2c\xc2\xb2+m\xc2\xb2c\xe2\x81\xb4".to_vec(), + integer: 9000, + path: PathBuf::from("src/lib.rs"), + }, + DefaultedOpt::try_parse_from(&[ + "test", + "-b", + "E²=p²c²+m²c⁴", + "-i", + "9000", + "-p", + "src/lib.rs", + ]) + .unwrap() + ); +} + +#[derive(PartialEq, Debug)] +struct Foo(u8); + +fn foo(value: u64) -> Foo { + Foo(value as u8) +} + +#[derive(Parser, PartialEq, Debug)] +struct Occurrences { + #[clap(short, long, parse(from_occurrences))] + signed: i32, + + #[clap(short, parse(from_occurrences))] + little_signed: i8, + + #[clap(short, parse(from_occurrences))] + unsigned: usize, + + #[clap(short = 'r', parse(from_occurrences))] + little_unsigned: u8, + + #[clap(short, long, parse(from_occurrences = foo))] + custom: Foo, +} + +#[test] +fn test_parser_occurrences() { + assert_eq!( + Occurrences { + signed: 3, + little_signed: 1, + unsigned: 0, + little_unsigned: 4, + custom: Foo(5), + }, + Occurrences::try_parse_from(&[ + "test", "-s", "--signed", "--signed", "-l", "-rrrr", "-cccc", "--custom", + ]) + .unwrap() + ); +} + +#[test] +fn test_custom_bool() { + fn parse_bool(s: &str) -> Result { + match s { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(format!("invalid bool {}", s)), + } + } + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, parse(try_from_str = parse_bool))] + debug: bool, + #[clap( + short, + default_value = "false", + parse(try_from_str = parse_bool) + )] + verbose: bool, + #[clap(short, parse(try_from_str = parse_bool))] + tribool: Option, + #[clap(short, parse(try_from_str = parse_bool), multiple_occurrences(true))] + bitset: Vec, + } + + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-d"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-dfoo"]).is_err()); + assert_eq!( + Opt { + debug: false, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dfalse"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dtrue"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dtrue", "-vfalse"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: true, + tribool: None, + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dtrue", "-vtrue"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: Some(false), + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dtrue", "-tfalse"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: Some(true), + bitset: vec![], + }, + Opt::try_parse_from(&["test", "-dtrue", "-ttrue"]).unwrap() + ); + assert_eq!( + Opt { + debug: true, + verbose: false, + tribool: None, + bitset: vec![false, true, false, false], + }, + Opt::try_parse_from(&["test", "-dtrue", "-bfalse", "-btrue", "-bfalse", "-bfalse"]) + .unwrap() + ); +} + +#[test] +fn test_cstring() { + #[derive(Parser)] + struct Opt { + #[clap(parse(try_from_str = CString::new))] + c_string: CString, + } + + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert_eq!( + Opt::try_parse_from(&["test", "bla"]) + .unwrap() + .c_string + .to_bytes(), + b"bla" + ); + assert!(Opt::try_parse_from(&["test", "bla\0bla"]).is_err()); +} diff --git a/tests/derive/legacy/default_value.rs b/tests/derive/legacy/default_value.rs new file mode 100644 index 00000000..db2b7838 --- /dev/null +++ b/tests/derive/legacy/default_value.rs @@ -0,0 +1,57 @@ +use clap::{CommandFactory, Parser}; + +use crate::utils; + +#[test] +fn default_value() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_value = "3")] + arg: i32, + } + assert_eq!(Opt { arg: 3 }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!(Opt { arg: 1 }, Opt::try_parse_from(&["test", "1"]).unwrap()); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: 3]")); +} + +#[test] +fn default_value_t() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_value_t = 3)] + arg: i32, + } + assert_eq!(Opt { arg: 3 }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!(Opt { arg: 1 }, Opt::try_parse_from(&["test", "1"]).unwrap()); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: 3]")); +} + +#[test] +fn auto_default_value_t() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_value_t)] + arg: i32, + } + assert_eq!(Opt { arg: 0 }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!(Opt { arg: 1 }, Opt::try_parse_from(&["test", "1"]).unwrap()); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: 0]")); +} + +#[test] +fn detect_os_variant() { + #![allow(unused_parens)] // needed for `as_ref` call + + #[derive(clap::Parser)] + pub struct Options { + #[clap(default_value_os = ("123".as_ref()))] + x: String, + } + Options::command().debug_assert(); +} diff --git a/tests/derive/legacy/deny_warnings.rs b/tests/derive/legacy/deny_warnings.rs new file mode 100644 index 00000000..ab642b64 --- /dev/null +++ b/tests/derive/legacy/deny_warnings.rs @@ -0,0 +1,53 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +#![deny(warnings)] + +use clap::Parser; + +fn try_str(s: &str) -> Result { + Ok(s.into()) +} + +#[test] +fn warning_never_struct() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(parse(try_from_str = try_str), default_value_t)] + s: String, + } + assert_eq!( + Opt { + s: "foo".to_string() + }, + Opt::try_parse_from(&["test", "foo"]).unwrap() + ); +} + +#[test] +fn warning_never_enum() { + #[derive(Parser, Debug, PartialEq)] + enum Opt { + Foo { + #[clap(parse(try_from_str = try_str), default_value_t)] + s: String, + }, + } + assert_eq!( + Opt::Foo { + s: "foo".to_string() + }, + Opt::try_parse_from(&["test", "foo", "foo"]).unwrap() + ); +} diff --git a/tests/derive/legacy/doc_comments_help.rs b/tests/derive/legacy/doc_comments_help.rs new file mode 100644 index 00000000..d5836c25 --- /dev/null +++ b/tests/derive/legacy/doc_comments_help.rs @@ -0,0 +1,240 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use crate::utils; + +use clap::{ArgEnum, CommandFactory, Parser}; + +#[test] +fn doc_comments() { + /// Lorem ipsum + #[derive(Parser, PartialEq, Debug)] + struct LoremIpsum { + /// Fooify a bar + /// and a baz + #[clap(short, long)] + foo: bool, + } + + let help = utils::get_long_help::(); + assert!(help.contains("Lorem ipsum")); + assert!(help.contains("Fooify a bar and a baz")); +} + +#[test] +fn help_is_better_than_comments() { + /// Lorem ipsum + #[derive(Parser, PartialEq, Debug)] + #[clap(name = "lorem-ipsum", about = "Dolor sit amet")] + struct LoremIpsum { + /// Fooify a bar + #[clap(short, long, help = "DO NOT PASS A BAR UNDER ANY CIRCUMSTANCES")] + foo: bool, + } + + let help = utils::get_long_help::(); + assert!(help.contains("Dolor sit amet")); + assert!(!help.contains("Lorem ipsum")); + assert!(help.contains("DO NOT PASS A BAR")); +} + +#[test] +fn empty_line_in_doc_comment_is_double_linefeed() { + /// Foo. + /// + /// Bar + #[derive(Parser, PartialEq, Debug)] + #[clap(name = "lorem-ipsum")] + struct LoremIpsum {} + + let help = utils::get_long_help::(); + assert!(help.starts_with("lorem-ipsum \nFoo.\n\nBar\n\nUSAGE:")); +} + +#[test] +fn field_long_doc_comment_both_help_long_help() { + /// Lorem ipsumclap + #[derive(Parser, PartialEq, Debug)] + #[clap(name = "lorem-ipsum", about = "Dolor sit amet")] + struct LoremIpsum { + /// Dot is removed from multiline comments. + /// + /// Long help + #[clap(long)] + foo: bool, + + /// Dot is removed from one short comment. + #[clap(long)] + bar: bool, + } + + let short_help = utils::get_help::(); + let long_help = utils::get_long_help::(); + + assert!(short_help.contains("Dot is removed from one short comment")); + assert!(!short_help.contains("Dot is removed from one short comment.")); + assert!(short_help.contains("Dot is removed from multiline comments")); + assert!(!short_help.contains("Dot is removed from multiline comments.")); + assert!(long_help.contains("Long help")); + assert!(!short_help.contains("Long help")); +} + +#[test] +fn top_long_doc_comment_both_help_long_help() { + /// Lorem ipsumclap + #[derive(Parser, Debug)] + #[clap(name = "lorem-ipsum", about = "Dolor sit amet")] + struct LoremIpsum { + #[clap(subcommand)] + foo: SubCommand, + } + + #[derive(Parser, Debug)] + pub enum SubCommand { + /// DO NOT PASS A BAR UNDER ANY CIRCUMSTANCES + /// + /// Or something else + Foo { + #[clap(help = "foo")] + bars: String, + }, + } + + let short_help = utils::get_help::(); + let long_help = utils::get_subcommand_long_help::("foo"); + + assert!(!short_help.contains("Or something else")); + assert!(long_help.contains("DO NOT PASS A BAR UNDER ANY CIRCUMSTANCES")); + assert!(long_help.contains("Or something else")); +} + +#[test] +fn verbatim_doc_comment() { + /// DANCE! + /// + /// () + /// | + /// ( () ) + /// ) ________ // ) + /// () |\ \ // + /// ( \\__ \ ______\// + /// \__) | | + /// | | | + /// \ | | + /// \|_______| + /// // \\ + /// (( || + /// \\ || + /// ( () || + /// ( () ) ) + #[derive(Parser, Debug)] + #[clap(verbatim_doc_comment)] + struct SeeFigure1 { + #[clap(long)] + foo: bool, + } + + let help = utils::get_long_help::(); + let sample = r#" + () + | + ( () ) + ) ________ // ) + () |\ \ // +( \\__ \ ______\// + \__) | | + | | | + \ | | + \|_______| + // \\ + (( || + \\ || + ( () || + ( () ) )"#; + + assert!(help.contains(sample)) +} + +#[test] +fn verbatim_doc_comment_field() { + #[derive(Parser, Debug)] + struct Command { + /// This help ends in a period. + #[clap(long, verbatim_doc_comment)] + foo: bool, + /// This help does not end in a period. + #[clap(long)] + bar: bool, + } + + let help = utils::get_long_help::(); + + assert!(help.contains("This help ends in a period.")); + assert!(help.contains("This help does not end in a period")); +} + +#[test] +fn multiline_separates_default() { + #[derive(Parser, Debug)] + struct Command { + /// Multiline + /// + /// Doc comment + #[clap(long, default_value = "x")] + x: String, + } + + let help = utils::get_long_help::(); + assert!(!help.contains("Doc comment [default")); + assert!(help.lines().any(|s| s.trim().starts_with("[default"))); + + // The short help should still have the default on the same line + let help = utils::get_help::(); + assert!(help.contains("Multiline [default")); +} + +#[test] +fn argenum_multiline_doc_comment() { + #[derive(ArgEnum, Clone)] + enum LoremIpsum { + /// Multiline + /// + /// Doc comment + Bar, + } +} + +#[test] +fn doc_comment_about_handles_both_abouts() { + /// Opts doc comment summary + #[derive(Parser, Debug)] + pub struct Opts { + #[clap(subcommand)] + pub cmd: Sub, + } + + /// Sub doc comment summary + /// + /// Sub doc comment body + #[derive(Parser, PartialEq, Eq, Debug)] + pub enum Sub { + Compress { output: String }, + } + + let cmd = Opts::command(); + assert_eq!(cmd.get_about(), Some("Opts doc comment summary")); + // clap will fallback to `about` on `None`. The main care about is not providing a `Sub` doc + // comment. + assert_eq!(cmd.get_long_about(), None); +} diff --git a/tests/derive/legacy/explicit_name_no_renaming.rs b/tests/derive/legacy/explicit_name_no_renaming.rs new file mode 100644 index 00000000..dcdbc382 --- /dev/null +++ b/tests/derive/legacy/explicit_name_no_renaming.rs @@ -0,0 +1,36 @@ +use crate::utils; + +use clap::Parser; + +#[test] +fn explicit_short_long_no_rename() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short = '.', long = ".foo")] + foo: String, + } + + assert_eq!( + Opt { foo: "long".into() }, + Opt::try_parse_from(&["test", "--.foo", "long"]).unwrap() + ); + + assert_eq!( + Opt { + foo: "short".into(), + }, + Opt::try_parse_from(&["test", "-.", "short"]).unwrap() + ); +} + +#[test] +fn explicit_name_no_rename() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(name = ".options")] + foo: String, + } + + let help = utils::get_long_help::(); + assert!(help.contains("<.options>")) +} diff --git a/tests/derive/legacy/flags.rs b/tests/derive/legacy/flags.rs new file mode 100644 index 00000000..ea770e68 --- /dev/null +++ b/tests/derive/legacy/flags.rs @@ -0,0 +1,189 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::Parser; + +#[test] +fn bool_type_is_flag() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long)] + alice: bool, + } + + assert_eq!( + Opt { alice: false }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { alice: true }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + assert_eq!( + Opt { alice: true }, + Opt::try_parse_from(&["test", "--alice"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test", "-i"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-a", "foo"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-a", "-a"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-a", "--alice"]).is_err()); +} + +#[test] +fn from_occurrences() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long, parse(from_occurrences))] + alice: u64, + #[clap(short, long, parse(from_occurrences))] + bob: u8, + } + + assert_eq!( + Opt { alice: 0, bob: 0 }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { alice: 1, bob: 0 }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + assert_eq!( + Opt { alice: 2, bob: 0 }, + Opt::try_parse_from(&["test", "-a", "-a"]).unwrap() + ); + assert_eq!( + Opt { alice: 2, bob: 2 }, + Opt::try_parse_from(&["test", "-a", "--alice", "-bb"]).unwrap() + ); + assert_eq!( + Opt { alice: 3, bob: 1 }, + Opt::try_parse_from(&["test", "-aaa", "--bob"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test", "-i"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-a", "foo"]).is_err()); +} + +#[test] +fn non_bool_type_flag() { + fn parse_from_flag(b: bool) -> std::sync::atomic::AtomicBool { + std::sync::atomic::AtomicBool::new(b) + } + + #[derive(Parser, Debug)] + struct Opt { + #[clap(short, long, parse(from_flag = parse_from_flag))] + alice: std::sync::atomic::AtomicBool, + #[clap(short, long, parse(from_flag))] + bob: std::sync::atomic::AtomicBool, + } + + let falsey = Opt::try_parse_from(&["test"]).unwrap(); + assert!(!falsey.alice.load(std::sync::atomic::Ordering::Relaxed)); + assert!(!falsey.bob.load(std::sync::atomic::Ordering::Relaxed)); + + let alice = Opt::try_parse_from(&["test", "-a"]).unwrap(); + assert!(alice.alice.load(std::sync::atomic::Ordering::Relaxed)); + assert!(!alice.bob.load(std::sync::atomic::Ordering::Relaxed)); + + let bob = Opt::try_parse_from(&["test", "-b"]).unwrap(); + assert!(!bob.alice.load(std::sync::atomic::Ordering::Relaxed)); + assert!(bob.bob.load(std::sync::atomic::Ordering::Relaxed)); + + let both = Opt::try_parse_from(&["test", "-b", "-a"]).unwrap(); + assert!(both.alice.load(std::sync::atomic::Ordering::Relaxed)); + assert!(both.bob.load(std::sync::atomic::Ordering::Relaxed)); +} + +#[test] +fn mixed_type_flags() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long)] + alice: bool, + #[clap(short, long, parse(from_occurrences))] + bob: u64, + } + + assert_eq!( + Opt { + alice: false, + bob: 0 + }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { + alice: true, + bob: 0 + }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + assert_eq!( + Opt { + alice: true, + bob: 0 + }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + assert_eq!( + Opt { + alice: false, + bob: 1 + }, + Opt::try_parse_from(&["test", "-b"]).unwrap() + ); + assert_eq!( + Opt { + alice: true, + bob: 1 + }, + Opt::try_parse_from(&["test", "--alice", "--bob"]).unwrap() + ); + assert_eq!( + Opt { + alice: true, + bob: 4 + }, + Opt::try_parse_from(&["test", "-bb", "-a", "-bb"]).unwrap() + ); +} + +#[test] +fn ignore_qualified_bool_type() { + mod inner { + #[allow(non_camel_case_types)] + #[derive(PartialEq, Debug)] + pub struct bool(pub String); + + impl std::str::FromStr for self::bool { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(self::bool(s.into())) + } + } + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + arg: inner::bool, + } + + assert_eq!( + Opt { + arg: inner::bool("success".into()) + }, + Opt::try_parse_from(&["test", "success"]).unwrap() + ); +} diff --git a/tests/derive/legacy/flatten.rs b/tests/derive/legacy/flatten.rs new file mode 100644 index 00000000..40d77a77 --- /dev/null +++ b/tests/derive/legacy/flatten.rs @@ -0,0 +1,256 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use crate::utils; + +use clap::{Args, Parser, Subcommand}; + +#[test] +fn flatten() { + #[derive(Args, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(flatten)] + common: Common, + } + assert_eq!( + Opt { + common: Common { arg: 42 } + }, + Opt::try_parse_from(&["test", "42"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert!(Opt::try_parse_from(&["test", "42", "24"]).is_err()); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic] +fn flatten_twice() { + #[derive(Args, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(flatten)] + c1: Common, + // Defines "arg" twice, so this should not work. + #[clap(flatten)] + c2: Common, + } + Opt::try_parse_from(&["test", "42", "43"]).unwrap(); +} + +#[test] +fn flatten_in_subcommand() { + #[derive(Args, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(Args, PartialEq, Debug)] + struct Add { + #[clap(short)] + interactive: bool, + #[clap(flatten)] + common: Common, + } + + #[derive(Parser, PartialEq, Debug)] + enum Opt { + Fetch { + #[clap(short)] + all: bool, + #[clap(flatten)] + common: Common, + }, + + Add(Add), + } + + assert_eq!( + Opt::Fetch { + all: false, + common: Common { arg: 42 } + }, + Opt::try_parse_from(&["test", "fetch", "42"]).unwrap() + ); + assert_eq!( + Opt::Add(Add { + interactive: true, + common: Common { arg: 43 } + }), + Opt::try_parse_from(&["test", "add", "-i", "43"]).unwrap() + ); +} + +#[test] +fn update_args_with_flatten() { + #[derive(Args, PartialEq, Debug)] + struct Common { + arg: i32, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(flatten)] + common: Common, + } + + let mut opt = Opt { + common: Common { arg: 42 }, + }; + opt.try_update_from(&["test"]).unwrap(); + assert_eq!(Opt::try_parse_from(&["test", "42"]).unwrap(), opt); + + let mut opt = Opt { + common: Common { arg: 42 }, + }; + opt.try_update_from(&["test", "52"]).unwrap(); + assert_eq!(Opt::try_parse_from(&["test", "52"]).unwrap(), opt); +} + +#[derive(Subcommand, PartialEq, Debug)] +enum BaseCli { + Command1(Command1), +} + +#[derive(Args, PartialEq, Debug)] +struct Command1 { + arg1: i32, + arg2: i32, +} + +#[derive(Args, PartialEq, Debug)] +struct Command2 { + arg2: i32, +} + +#[derive(Parser, PartialEq, Debug)] +enum Opt { + #[clap(flatten)] + BaseCli(BaseCli), + Command2(Command2), +} + +#[test] +fn merge_subcommands_with_flatten() { + assert_eq!( + Opt::BaseCli(BaseCli::Command1(Command1 { arg1: 42, arg2: 44 })), + Opt::try_parse_from(&["test", "command1", "42", "44"]).unwrap() + ); + assert_eq!( + Opt::Command2(Command2 { arg2: 43 }), + Opt::try_parse_from(&["test", "command2", "43"]).unwrap() + ); +} + +#[test] +fn update_subcommands_with_flatten() { + let mut opt = Opt::BaseCli(BaseCli::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "command1", "42", "44"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command1", "42", "44"]).unwrap(), + opt + ); + + let mut opt = Opt::BaseCli(BaseCli::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "command1", "42"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command1", "42", "14"]).unwrap(), + opt + ); + + let mut opt = Opt::BaseCli(BaseCli::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "command2", "43"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command2", "43"]).unwrap(), + opt + ); +} + +#[test] +fn flatten_with_doc_comment() { + #[derive(Args, PartialEq, Debug)] + struct Common { + /// This is an arg. Arg means "argument". Command line argument. + arg: i32, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + /// The very important comment that clippy had me put here. + /// It knows better. + #[clap(flatten)] + common: Common, + } + assert_eq!( + Opt { + common: Common { arg: 42 } + }, + Opt::try_parse_from(&["test", "42"]).unwrap() + ); + + let help = utils::get_help::(); + assert!(help.contains("This is an arg.")); + assert!(!help.contains("The very important")); +} + +#[test] +fn docstrings_ordering_with_multiple_clap() { + /// This is the docstring for Flattened + #[derive(Args)] + struct Flattened { + #[clap(long)] + foo: bool, + } + + /// This is the docstring for Command + #[derive(Parser)] + struct Command { + #[clap(flatten)] + flattened: Flattened, + } + + let short_help = utils::get_help::(); + + assert!(short_help.contains("This is the docstring for Command")); +} + +#[test] +fn docstrings_ordering_with_multiple_clap_partial() { + /// This is the docstring for Flattened + #[derive(Args)] + struct Flattened { + #[clap(long)] + foo: bool, + } + + #[derive(Parser)] + struct Command { + #[clap(flatten)] + flattened: Flattened, + } + + let short_help = utils::get_help::(); + + assert!(short_help.contains("This is the docstring for Flattened")); +} diff --git a/tests/derive/legacy/generic.rs b/tests/derive/legacy/generic.rs new file mode 100644 index 00000000..76cccd33 --- /dev/null +++ b/tests/derive/legacy/generic.rs @@ -0,0 +1,145 @@ +use clap::{Args, Parser}; + +#[test] +fn generic_struct_flatten() { + #[derive(Args, PartialEq, Debug)] + struct Inner { + pub answer: isize, + } + + #[derive(Parser, PartialEq, Debug)] + struct Outer { + #[clap(flatten)] + pub inner: T, + } + + assert_eq!( + Outer { + inner: Inner { answer: 42 } + }, + Outer::parse_from(&["--answer", "42"]) + ) +} + +#[test] +fn generic_struct_flatten_w_where_clause() { + #[derive(Args, PartialEq, Debug)] + struct Inner { + pub answer: isize, + } + + #[derive(Parser, PartialEq, Debug)] + struct Outer + where + T: Args, + { + #[clap(flatten)] + pub inner: T, + } + + assert_eq!( + Outer { + inner: Inner { answer: 42 } + }, + Outer::parse_from(&["--answer", "42"]) + ) +} + +#[test] +fn generic_enum() { + #[derive(Args, PartialEq, Debug)] + struct Inner { + pub answer: isize, + } + + #[derive(Parser, PartialEq, Debug)] + enum GenericEnum { + Start(T), + Stop, + } + + assert_eq!( + GenericEnum::Start(Inner { answer: 42 }), + GenericEnum::parse_from(&["test", "start", "42"]) + ) +} + +#[test] +fn generic_enum_w_where_clause() { + #[derive(Args, PartialEq, Debug)] + struct Inner { + pub answer: isize, + } + + #[derive(Parser, PartialEq, Debug)] + enum GenericEnum + where + T: Args, + { + Start(T), + Stop, + } + + assert_eq!( + GenericEnum::Start(Inner { answer: 42 }), + GenericEnum::parse_from(&["test", "start", "42"]) + ) +} + +#[test] +fn generic_w_fromstr_trait_bound() { + use std::str::FromStr; + + #[derive(Parser, PartialEq, Debug)] + struct Opt + where + T: FromStr, + ::Err: std::error::Error + Sync + Send + 'static, + { + answer: T, + } + + assert_eq!( + Opt:: { answer: 42 }, + Opt::::parse_from(&["--answer", "42"]) + ) +} + +#[test] +fn generic_wo_trait_bound() { + use std::time::Duration; + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + answer: isize, + #[clap(skip)] + took: Option, + } + + assert_eq!( + Opt:: { + answer: 42, + took: None + }, + Opt::::parse_from(&["--answer", "42"]) + ) +} + +#[test] +fn generic_where_clause_w_trailing_comma() { + use std::str::FromStr; + + #[derive(Parser, PartialEq, Debug)] + struct Opt + where + T: FromStr, + ::Err: std::error::Error + Sync + Send + 'static, + { + pub answer: T, + } + + assert_eq!( + Opt:: { answer: 42 }, + Opt::::parse_from(&["--answer", "42"]) + ) +} diff --git a/tests/derive/legacy/help.rs b/tests/derive/legacy/help.rs new file mode 100644 index 00000000..1362ac86 --- /dev/null +++ b/tests/derive/legacy/help.rs @@ -0,0 +1,473 @@ +use clap::{AppSettings, Args, CommandFactory, Parser, Subcommand}; + +#[test] +fn arg_help_heading_applied() { + #[derive(Debug, Clone, Parser)] + struct CliOptions { + #[clap(long)] + #[clap(help_heading = Some("HEADING A"))] + should_be_in_section_a: u32, + + #[clap(long)] + no_section: u32, + } + + let cmd = CliOptions::command(); + + let should_be_in_section_a = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_section_a") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-section-a") + .unwrap() + }; + assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A")); + + let should_be_in_section_b = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "no_section") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "no-section") + .unwrap() + }; + assert_eq!(should_be_in_section_b.get_help_heading(), None); +} + +#[test] +fn app_help_heading_applied() { + #[derive(Debug, Clone, Parser)] + #[clap(next_help_heading = "DEFAULT")] + struct CliOptions { + #[clap(long)] + #[clap(help_heading = Some("HEADING A"))] + should_be_in_section_a: u32, + + #[clap(long)] + should_be_in_default_section: u32, + } + + let cmd = CliOptions::command(); + + let should_be_in_section_a = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_section_a") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-section-a") + .unwrap() + }; + assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A")); + + let should_be_in_default_section = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_default_section") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-default-section") + .unwrap() + }; + assert_eq!( + should_be_in_default_section.get_help_heading(), + Some("DEFAULT") + ); +} + +#[test] +fn app_help_heading_flattened() { + // Used to help track the cause in tests + #![allow(clippy::enum_variant_names)] + + #[derive(Debug, Clone, Parser)] + struct CliOptions { + #[clap(flatten)] + options_a: OptionsA, + + #[clap(flatten)] + options_b: OptionsB, + + #[clap(subcommand)] + sub_a: SubA, + + #[clap(long)] + should_be_in_default_section: u32, + } + + #[derive(Debug, Clone, Args)] + #[clap(next_help_heading = "HEADING A")] + struct OptionsA { + #[clap(long)] + should_be_in_section_a: u32, + } + + #[derive(Debug, Clone, Args)] + #[clap(next_help_heading = "HEADING B")] + struct OptionsB { + #[clap(long)] + should_be_in_section_b: u32, + } + + #[derive(Debug, Clone, Subcommand)] + enum SubA { + #[clap(flatten)] + SubB(SubB), + #[clap(subcommand)] + SubC(SubC), + SubAOne, + #[clap(next_help_heading = "SUB A")] + SubATwo { + should_be_in_sub_a: u32, + }, + } + + #[derive(Debug, Clone, Subcommand)] + enum SubB { + #[clap(next_help_heading = "SUB B")] + SubBOne { should_be_in_sub_b: u32 }, + } + + #[derive(Debug, Clone, Subcommand)] + enum SubC { + #[clap(next_help_heading = "SUB C")] + SubCOne { should_be_in_sub_c: u32 }, + } + + let cmd = CliOptions::command(); + + let should_be_in_section_a = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_section_a") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-section-a") + .unwrap() + }; + assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A")); + + let should_be_in_section_b = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_section_b") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-section-b") + .unwrap() + }; + assert_eq!(should_be_in_section_b.get_help_heading(), Some("HEADING B")); + + let should_be_in_default_section = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_default_section") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-default-section") + .unwrap() + }; + assert_eq!(should_be_in_default_section.get_help_heading(), None); + + let sub_a_two = cmd.find_subcommand("sub-a-two").unwrap(); + + let should_be_in_sub_a = if cfg!(feature = "unstable-v4") { + sub_a_two + .get_arguments() + .find(|a| a.get_id() == "should_be_in_sub_a") + .unwrap() + } else { + sub_a_two + .get_arguments() + .find(|a| a.get_id() == "should-be-in-sub-a") + .unwrap() + }; + assert_eq!(should_be_in_sub_a.get_help_heading(), Some("SUB A")); + + let sub_b_one = cmd.find_subcommand("sub-b-one").unwrap(); + + let should_be_in_sub_b = if cfg!(feature = "unstable-v4") { + sub_b_one + .get_arguments() + .find(|a| a.get_id() == "should_be_in_sub_b") + .unwrap() + } else { + sub_b_one + .get_arguments() + .find(|a| a.get_id() == "should-be-in-sub-b") + .unwrap() + }; + assert_eq!(should_be_in_sub_b.get_help_heading(), Some("SUB B")); + + let sub_c = cmd.find_subcommand("sub-c").unwrap(); + let sub_c_one = sub_c.find_subcommand("sub-c-one").unwrap(); + + let should_be_in_sub_c = if cfg!(feature = "unstable-v4") { + sub_c_one + .get_arguments() + .find(|a| a.get_id() == "should_be_in_sub_c") + .unwrap() + } else { + sub_c_one + .get_arguments() + .find(|a| a.get_id() == "should-be-in-sub-c") + .unwrap() + }; + assert_eq!(should_be_in_sub_c.get_help_heading(), Some("SUB C")); +} + +#[test] +fn flatten_field_with_help_heading() { + #[derive(Debug, Clone, Parser)] + struct CliOptions { + #[clap(flatten)] + #[clap(next_help_heading = "HEADING A")] + options_a: OptionsA, + } + + #[derive(Debug, Clone, Args)] + struct OptionsA { + #[clap(long)] + should_be_in_section_a: u32, + } + + let cmd = CliOptions::command(); + + let should_be_in_section_a = if cfg!(feature = "unstable-v4") { + cmd.get_arguments() + .find(|a| a.get_id() == "should_be_in_section_a") + .unwrap() + } else { + cmd.get_arguments() + .find(|a| a.get_id() == "should-be-in-section-a") + .unwrap() + }; + assert_eq!(should_be_in_section_a.get_help_heading(), Some("HEADING A")); +} + +// The challenge with this test is creating an error situation not caught by `clap`'s error checking +// but by the code that `clap_derive` generates. +// +// Ultimately, the easiest way to confirm is to put a debug statement in the desired error path. +#[test] +fn derive_generated_error_has_full_context() { + #[derive(Debug, Parser)] + #[clap(subcommand_negates_reqs = true)] + struct Opts { + #[clap(long)] + req_str: String, + + #[clap(subcommand)] + cmd: Option, + } + + #[derive(Debug, Parser)] + enum SubCommands { + Sub { + #[clap(short, long, parse(from_occurrences))] + verbose: u8, + }, + } + + let result = Opts::try_parse_from(&["test", "sub"]); + assert!( + result.is_err(), + "`SubcommandsNegateReqs` with non-optional `req_str` should fail: {:?}", + result.unwrap() + ); + + if cfg!(feature = "unstable-v4") { + let expected = r#"error: The following required argument was not provided: req_str + +USAGE: + clap --req-str + clap + +For more information try --help +"#; + assert_eq!(result.unwrap_err().to_string(), expected); + } else { + let expected = r#"error: The following required argument was not provided: req-str + +USAGE: + clap --req-str + clap + +For more information try --help +"#; + assert_eq!(result.unwrap_err().to_string(), expected); + } +} + +#[test] +fn derive_order_next_order() { + static HELP: &str = "test 1.2 + +USAGE: + test [OPTIONS] + +OPTIONS: + --flag-b first flag + --option-b first option + -h, --help Print help information + -V, --version Print version information + --flag-a second flag + --option-a second option +"; + + #[derive(Parser, Debug)] + #[clap(name = "test", version = "1.2")] + #[clap(setting = AppSettings::DeriveDisplayOrder)] + struct Args { + #[clap(flatten)] + a: A, + #[clap(flatten)] + b: B, + } + + #[derive(Args, Debug)] + #[clap(next_display_order = 10000)] + struct A { + /// second flag + #[clap(long)] + flag_a: bool, + /// second option + #[clap(long)] + option_a: Option, + } + + #[derive(Args, Debug)] + #[clap(next_display_order = 10)] + struct B { + /// first flag + #[clap(long)] + flag_b: bool, + /// first option + #[clap(long)] + option_b: Option, + } + + use clap::CommandFactory; + let mut cmd = Args::command(); + + let mut buffer: Vec = Default::default(); + cmd.write_help(&mut buffer).unwrap(); + let help = String::from_utf8(buffer).unwrap(); + assert_eq!(help, HELP); +} + +#[test] +fn derive_order_next_order_flatten() { + static HELP: &str = "test 1.2 + +USAGE: + test [OPTIONS] + +OPTIONS: + --flag-b first flag + --option-b first option + -h, --help Print help information + -V, --version Print version information + --flag-a second flag + --option-a second option +"; + + #[derive(Parser, Debug)] + #[clap(setting = AppSettings::DeriveDisplayOrder)] + #[clap(name = "test", version = "1.2")] + struct Args { + #[clap(flatten)] + #[clap(next_display_order = 10000)] + a: A, + #[clap(flatten)] + #[clap(next_display_order = 10)] + b: B, + } + + #[derive(Args, Debug)] + struct A { + /// second flag + #[clap(long)] + flag_a: bool, + /// second option + #[clap(long)] + option_a: Option, + } + + #[derive(Args, Debug)] + struct B { + /// first flag + #[clap(long)] + flag_b: bool, + /// first option + #[clap(long)] + option_b: Option, + } + + use clap::CommandFactory; + let mut cmd = Args::command(); + + let mut buffer: Vec = Default::default(); + cmd.write_help(&mut buffer).unwrap(); + let help = String::from_utf8(buffer).unwrap(); + assert_eq!(help, HELP); +} + +#[test] +fn derive_order_no_next_order() { + static HELP: &str = "test 1.2 + +USAGE: + test [OPTIONS] + +OPTIONS: + --flag-a first flag + --flag-b second flag + -h, --help Print help information + --option-a first option + --option-b second option + -V, --version Print version information +"; + + #[derive(Parser, Debug)] + #[clap(name = "test", version = "1.2")] + #[clap(setting = AppSettings::DeriveDisplayOrder)] + #[clap(next_display_order = None)] + struct Args { + #[clap(flatten)] + a: A, + #[clap(flatten)] + b: B, + } + + #[derive(Args, Debug)] + struct A { + /// first flag + #[clap(long)] + flag_a: bool, + /// first option + #[clap(long)] + option_a: Option, + } + + #[derive(Args, Debug)] + struct B { + /// second flag + #[clap(long)] + flag_b: bool, + /// second option + #[clap(long)] + option_b: Option, + } + + use clap::CommandFactory; + let mut cmd = Args::command(); + + let mut buffer: Vec = Default::default(); + cmd.write_help(&mut buffer).unwrap(); + let help = String::from_utf8(buffer).unwrap(); + assert_eq!(help, HELP); +} diff --git a/tests/derive/legacy/issues.rs b/tests/derive/legacy/issues.rs new file mode 100644 index 00000000..44e90057 --- /dev/null +++ b/tests/derive/legacy/issues.rs @@ -0,0 +1,130 @@ +// https://github.com/TeXitoi/structopt/issues/{NUMBER} + +use crate::utils; + +use clap::{ArgGroup, Args, Parser, Subcommand}; + +#[test] +fn issue_151() { + #[derive(Args, Debug)] + #[clap(group = ArgGroup::new("verb").required(true).multiple(true))] + struct Opt { + #[clap(long, group = "verb")] + foo: bool, + #[clap(long, group = "verb")] + bar: bool, + } + + #[derive(Debug, Parser)] + struct Cli { + #[clap(flatten)] + a: Opt, + } + + assert!(Cli::try_parse_from(&["test"]).is_err()); + assert!(Cli::try_parse_from(&["test", "--foo"]).is_ok()); + assert!(Cli::try_parse_from(&["test", "--bar"]).is_ok()); + assert!(Cli::try_parse_from(&["test", "--zebra"]).is_err()); + assert!(Cli::try_parse_from(&["test", "--foo", "--bar"]).is_ok()); +} + +#[test] +fn issue_289() { + #[derive(Parser)] + #[clap(infer_subcommands = true)] + enum Args { + SomeCommand { + #[clap(subcommand)] + sub: SubSubCommand, + }, + AnotherCommand, + } + + #[derive(Subcommand)] + #[clap(infer_subcommands = true)] + enum SubSubCommand { + TestCommand, + } + + assert!(Args::try_parse_from(&["test", "some-command", "test-command"]).is_ok()); + assert!(Args::try_parse_from(&["test", "some", "test-command"]).is_ok()); + assert!(Args::try_parse_from(&["test", "some-command", "test"]).is_ok()); + assert!(Args::try_parse_from(&["test", "some", "test"]).is_ok()); +} + +#[test] +fn issue_324() { + fn my_version() -> &'static str { + "MY_VERSION" + } + + #[derive(Parser)] + #[clap(version = my_version())] + struct Opt { + #[clap(subcommand)] + _cmd: SubCommand, + } + + #[derive(Subcommand)] + enum SubCommand { + Start, + } + + let help = utils::get_long_help::(); + assert!(help.contains("MY_VERSION")); +} + +#[test] +fn issue_418() { + #[derive(Debug, Parser)] + struct Opts { + #[clap(subcommand)] + /// The command to run + command: Command, + } + + #[derive(Debug, Subcommand)] + enum Command { + /// Reticulate the splines + #[clap(visible_alias = "ret")] + Reticulate { + /// How many splines + num_splines: u8, + }, + /// Frobnicate the rest + #[clap(visible_alias = "frob")] + Frobnicate, + } + + let help = utils::get_long_help::(); + assert!(help.contains("Reticulate the splines [aliases: ret]")); +} + +#[test] +fn issue_490() { + use clap::Parser; + use std::iter::FromIterator; + use std::str::FromStr; + + struct U16ish; + impl FromStr for U16ish { + type Err = (); + fn from_str(_: &str) -> Result { + unimplemented!() + } + } + impl<'a> FromIterator<&'a U16ish> for Vec { + fn from_iter>(_: T) -> Self { + unimplemented!() + } + } + + #[derive(Parser, Debug)] + struct Opt { + opt_vec: Vec, + #[clap(long)] + opt_opt_vec: Option>, + } + + // Assert that it compiles +} diff --git a/tests/derive/legacy/macros.rs b/tests/derive/legacy/macros.rs new file mode 100644 index 00000000..a2e671b4 --- /dev/null +++ b/tests/derive/legacy/macros.rs @@ -0,0 +1,53 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::Parser; + +// Tests that clap_derive properly detects an `Option` field +// that results from a macro expansion +#[test] +fn use_option() { + macro_rules! expand_ty { + ($name:ident: $ty:ty) => { + #[derive(Parser)] + struct Outer { + #[clap(short, long)] + #[allow(dead_code)] + $name: $ty, + } + }; + } + + expand_ty!(my_field: Option); +} + +#[test] +fn issue_447() { + macro_rules! Command { + ( $name:ident, [ + #[$meta:meta] $var:ident($inner:ty) + ] ) => { + #[derive(Debug, PartialEq, clap::Parser)] + enum $name { + #[$meta] + $var($inner), + } + }; + } + + Command! {GitCmd, [ + #[clap(external_subcommand)] + Ext(Vec) + ]} +} diff --git a/tests/derive/legacy/mod.rs b/tests/derive/legacy/mod.rs new file mode 100644 index 00000000..6578b8c4 --- /dev/null +++ b/tests/derive/legacy/mod.rs @@ -0,0 +1,33 @@ +#![allow(deprecated)] + +mod app_name; +mod arg_enum; +mod arguments; +mod author_version_about; +mod basic; +mod boxed; +mod custom_string_parsers; +mod default_value; +mod deny_warnings; +mod doc_comments_help; +mod explicit_name_no_renaming; +mod flags; +mod flatten; +mod generic; +mod help; +mod issues; +mod macros; +mod naming; +mod nested_subcommands; +mod non_literal_attributes; +mod options; +mod privacy; +mod raw_bool_literal; +mod raw_idents; +mod rename_all_env; +mod skip; +mod structopt; +mod subcommands; +mod type_alias_regressions; +mod utf8; +mod utils; diff --git a/tests/derive/legacy/naming.rs b/tests/derive/legacy/naming.rs new file mode 100644 index 00000000..e3018a02 --- /dev/null +++ b/tests/derive/legacy/naming.rs @@ -0,0 +1,354 @@ +use clap::Parser; + +#[test] +fn test_standalone_long_generates_kebab_case() { + #[derive(Parser, Debug, PartialEq)] + #[allow(non_snake_case)] + struct Opt { + #[clap(long)] + FOO_OPTION: bool, + } + + assert_eq!( + Opt { FOO_OPTION: true }, + Opt::try_parse_from(&["test", "--foo-option"]).unwrap() + ); +} + +#[test] +fn test_custom_long_overwrites_default_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(long = "foo")] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--foo"]).unwrap() + ); +} + +#[test] +fn test_standalone_long_uses_previous_defined_custom_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(name = "foo", long)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--foo"]).unwrap() + ); +} + +#[test] +fn test_standalone_long_ignores_afterwards_defined_custom_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(long, name = "foo")] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--foo-option"]).unwrap() + ); +} + +#[test] +fn test_standalone_short_generates_kebab_case() { + #[derive(Parser, Debug, PartialEq)] + #[allow(non_snake_case)] + struct Opt { + #[clap(short)] + FOO_OPTION: bool, + } + + assert_eq!( + Opt { FOO_OPTION: true }, + Opt::try_parse_from(&["test", "-f"]).unwrap() + ); +} + +#[test] +fn test_custom_short_overwrites_default_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(short = 'o')] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "-o"]).unwrap() + ); +} + +#[test] +fn test_standalone_short_uses_previous_defined_custom_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(name = "option", short)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "-o"]).unwrap() + ); +} + +#[test] +fn test_standalone_short_ignores_afterwards_defined_custom_name() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(short, name = "option")] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "-f"]).unwrap() + ); +} + +#[test] +fn test_standalone_long_uses_previous_defined_casing() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(rename_all = "screaming_snake", long)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--FOO_OPTION"]).unwrap() + ); +} + +#[test] +fn test_standalone_short_uses_previous_defined_casing() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(rename_all = "screaming_snake", short)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "-F"]).unwrap() + ); +} + +#[test] +fn test_standalone_long_works_with_verbatim_casing() { + #[derive(Parser, Debug, PartialEq)] + #[allow(non_snake_case)] + struct Opt { + #[clap(rename_all = "verbatim", long)] + _fOO_oPtiON: bool, + } + + assert_eq!( + Opt { _fOO_oPtiON: true }, + Opt::try_parse_from(&["test", "--_fOO_oPtiON"]).unwrap() + ); +} + +#[test] +fn test_standalone_short_works_with_verbatim_casing() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(rename_all = "verbatim", short)] + _foo: bool, + } + + assert_eq!( + Opt { _foo: true }, + Opt::try_parse_from(&["test", "-_"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_propagated_from_struct_to_fields() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + struct Opt { + #[clap(long)] + foo: bool, + } + + assert_eq!( + Opt { foo: true }, + Opt::try_parse_from(&["test", "--FOO"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_not_propagated_from_struct_into_flattened() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + struct Opt { + #[clap(flatten)] + foo: Foo, + } + + #[derive(Parser, Debug, PartialEq)] + struct Foo { + #[clap(long)] + foo: bool, + } + + assert_eq!( + Opt { + foo: Foo { foo: true } + }, + Opt::try_parse_from(&["test", "--foo"]).unwrap() + ); +} + +#[test] +fn test_lower_is_renamed() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(rename_all = "lower", long)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--foooption"]).unwrap() + ); +} + +#[test] +fn test_upper_is_renamed() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(rename_all = "upper", long)] + foo_option: bool, + } + + assert_eq!( + Opt { foo_option: true }, + Opt::try_parse_from(&["test", "--FOOOPTION"]).unwrap() + ); +} + +#[test] +fn test_single_word_enum_variant_is_default_renamed_into_kebab_case() { + #[derive(Parser, Debug, PartialEq)] + enum Opt { + Command { foo: u32 }, + } + + assert_eq!( + Opt::Command { foo: 0 }, + Opt::try_parse_from(&["test", "command", "0"]).unwrap() + ); +} + +#[test] +fn test_multi_word_enum_variant_is_renamed() { + #[derive(Parser, Debug, PartialEq)] + enum Opt { + FirstCommand { foo: u32 }, + } + + assert_eq!( + Opt::FirstCommand { foo: 0 }, + Opt::try_parse_from(&["test", "first-command", "0"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_not_propagated_from_struct_into_subcommand() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + struct Opt { + #[clap(subcommand)] + foo: Foo, + } + + #[derive(Parser, Debug, PartialEq)] + enum Foo { + Command { + #[clap(long)] + foo: bool, + }, + } + + assert_eq!( + Opt { + foo: Foo::Command { foo: true } + }, + Opt::try_parse_from(&["test", "command", "--foo"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_propagated_from_enum_to_variants() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + enum Opt { + FirstVariant, + SecondVariant { + #[clap(long)] + foo: String, + }, + } + + assert_eq!( + Opt::FirstVariant, + Opt::try_parse_from(&["test", "FIRST_VARIANT"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_propagated_from_enum_to_variant_fields() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + enum Opt { + FirstVariant, + SecondVariant { + #[clap(long)] + foo: String, + }, + } + + assert_eq!( + Opt::SecondVariant { + foo: "value".into() + }, + Opt::try_parse_from(&["test", "SECOND_VARIANT", "--FOO", "value"]).unwrap() + ); +} + +#[test] +fn test_rename_all_is_propagation_can_be_overridden() { + #[derive(Parser, Debug, PartialEq)] + #[clap(rename_all = "screaming_snake")] + enum Opt { + #[clap(rename_all = "kebab_case")] + FirstVariant { + #[clap(long)] + foo_option: bool, + }, + SecondVariant { + #[clap(rename_all = "kebab_case", long)] + foo_option: bool, + }, + } + + assert_eq!( + Opt::FirstVariant { foo_option: true }, + Opt::try_parse_from(&["test", "first-variant", "--foo-option"]).unwrap() + ); + + assert_eq!( + Opt::SecondVariant { foo_option: true }, + Opt::try_parse_from(&["test", "SECOND_VARIANT", "--foo-option"]).unwrap() + ); +} diff --git a/tests/derive/legacy/nested_subcommands.rs b/tests/derive/legacy/nested_subcommands.rs new file mode 100644 index 00000000..bf9344a6 --- /dev/null +++ b/tests/derive/legacy/nested_subcommands.rs @@ -0,0 +1,194 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::{Parser, Subcommand}; + +#[derive(Parser, PartialEq, Debug)] +struct Opt { + #[clap(short, long)] + force: bool, + #[clap(short, long, parse(from_occurrences))] + verbose: u64, + #[clap(subcommand)] + cmd: Sub, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Sub { + Fetch {}, + Add {}, +} + +#[derive(Parser, PartialEq, Debug)] +struct Opt2 { + #[clap(short, long)] + force: bool, + #[clap(short, long, parse(from_occurrences))] + verbose: u64, + #[clap(subcommand)] + cmd: Option, +} + +#[test] +fn test_no_cmd() { + let result = Opt::try_parse_from(&["test"]); + assert!(result.is_err()); + + assert_eq!( + Opt2 { + force: false, + verbose: 0, + cmd: None + }, + Opt2::try_parse_from(&["test"]).unwrap() + ); +} + +#[test] +fn test_fetch() { + assert_eq!( + Opt { + force: false, + verbose: 3, + cmd: Sub::Fetch {} + }, + Opt::try_parse_from(&["test", "-vvv", "fetch"]).unwrap() + ); + assert_eq!( + Opt { + force: true, + verbose: 0, + cmd: Sub::Fetch {} + }, + Opt::try_parse_from(&["test", "--force", "fetch"]).unwrap() + ); +} + +#[test] +fn test_add() { + assert_eq!( + Opt { + force: false, + verbose: 0, + cmd: Sub::Add {} + }, + Opt::try_parse_from(&["test", "add"]).unwrap() + ); + assert_eq!( + Opt { + force: false, + verbose: 2, + cmd: Sub::Add {} + }, + Opt::try_parse_from(&["test", "-vv", "add"]).unwrap() + ); +} + +#[test] +fn test_badinput() { + let result = Opt::try_parse_from(&["test", "badcmd"]); + assert!(result.is_err()); + let result = Opt::try_parse_from(&["test", "add", "--verbose"]); + assert!(result.is_err()); + let result = Opt::try_parse_from(&["test", "--badopt", "add"]); + assert!(result.is_err()); + let result = Opt::try_parse_from(&["test", "add", "--badopt"]); + assert!(result.is_err()); +} + +#[derive(Parser, PartialEq, Debug)] +struct Opt3 { + #[clap(short, long)] + all: bool, + #[clap(subcommand)] + cmd: Sub2, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Sub2 { + Foo { + file: String, + #[clap(subcommand)] + cmd: Sub3, + }, + Bar {}, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Sub3 { + Baz {}, + Quux {}, +} + +#[test] +fn test_subsubcommand() { + assert_eq!( + Opt3 { + all: true, + cmd: Sub2::Foo { + file: "lib.rs".to_string(), + cmd: Sub3::Quux {} + } + }, + Opt3::try_parse_from(&["test", "--all", "foo", "lib.rs", "quux"]).unwrap() + ); +} + +#[derive(Parser, PartialEq, Debug)] +enum SubSubCmdWithOption { + Remote { + #[clap(subcommand)] + cmd: Option, + }, + Stash { + #[clap(subcommand)] + cmd: Stash, + }, +} +#[derive(Subcommand, PartialEq, Debug)] +enum Remote { + Add { name: String, url: String }, + Remove { name: String }, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Stash { + Save, + Pop, +} + +#[test] +fn sub_sub_cmd_with_option() { + fn make(args: &[&str]) -> Option { + SubSubCmdWithOption::try_parse_from(args).ok() + } + assert_eq!( + Some(SubSubCmdWithOption::Remote { cmd: None }), + make(&["", "remote"]) + ); + assert_eq!( + Some(SubSubCmdWithOption::Remote { + cmd: Some(Remote::Add { + name: "origin".into(), + url: "http".into() + }) + }), + make(&["", "remote", "add", "origin", "http"]) + ); + assert_eq!( + Some(SubSubCmdWithOption::Stash { cmd: Stash::Save }), + make(&["", "stash", "save"]) + ); + assert_eq!(None, make(&["", "stash"])); +} diff --git a/tests/derive/legacy/non_literal_attributes.rs b/tests/derive/legacy/non_literal_attributes.rs new file mode 100644 index 00000000..4fde2057 --- /dev/null +++ b/tests/derive/legacy/non_literal_attributes.rs @@ -0,0 +1,156 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use clap::{ErrorKind, Parser}; +use std::num::ParseIntError; + +pub const DISPLAY_ORDER: usize = 2; + +// Check if the global settings compile +#[derive(Parser, Debug, PartialEq, Eq)] +#[clap(allow_hyphen_values = true)] +struct Opt { + #[clap( + long = "x", + display_order = DISPLAY_ORDER, + next_line_help = true, + default_value = "0", + require_equals = true + )] + x: i32, + + #[clap(short = 'l', long = "level", aliases = &["set-level", "lvl"])] + level: String, + + #[clap(long("values"))] + values: Vec, + + #[clap(name = "FILE", requires_if("FILE", "values"))] + files: Vec, +} + +#[test] +fn test_slice() { + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::try_parse_from(&["test", "-l", "1"]).unwrap() + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::try_parse_from(&["test", "--level", "1"]).unwrap() + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::try_parse_from(&["test", "--set-level", "1"]).unwrap() + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: Vec::new(), + values: vec![], + }, + Opt::try_parse_from(&["test", "--lvl", "1"]).unwrap() + ); +} + +#[test] +fn test_multi_args() { + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: vec!["file".to_string()], + values: vec![], + }, + Opt::try_parse_from(&["test", "-l", "1", "file"]).unwrap() + ); + assert_eq!( + Opt { + x: 0, + level: "1".to_string(), + files: vec!["FILE".to_string()], + values: vec![1], + }, + Opt::try_parse_from(&["test", "-l", "1", "--values", "1", "--", "FILE"]).unwrap() + ); +} + +#[test] +fn test_multi_args_fail() { + let result = Opt::try_parse_from(&["test", "-l", "1", "--", "FILE"]); + assert!(result.is_err()); +} + +#[test] +fn test_bool() { + assert_eq!( + Opt { + x: 1, + level: "1".to_string(), + files: vec![], + values: vec![], + }, + Opt::try_parse_from(&["test", "-l", "1", "--x=1"]).unwrap() + ); + let result = Opt::try_parse_from(&["test", "-l", "1", "--x", "1"]); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), ErrorKind::NoEquals); +} + +fn parse_hex(input: &str) -> Result { + u64::from_str_radix(input, 16) +} + +#[derive(Parser, PartialEq, Debug)] +struct HexOpt { + #[clap(short, parse(try_from_str = parse_hex))] + number: u64, +} + +#[test] +fn test_parse_hex_function_path() { + assert_eq!( + HexOpt { number: 5 }, + HexOpt::try_parse_from(&["test", "-n", "5"]).unwrap() + ); + assert_eq!( + HexOpt { + number: 0x00ab_cdef + }, + HexOpt::try_parse_from(&["test", "-n", "abcdef"]).unwrap() + ); + + let err = HexOpt::try_parse_from(&["test", "-n", "gg"]).unwrap_err(); + assert!( + err.to_string().contains("invalid digit found in string"), + "{}", + err + ); +} diff --git a/tests/derive/legacy/options.rs b/tests/derive/legacy/options.rs new file mode 100644 index 00000000..88507eb1 --- /dev/null +++ b/tests/derive/legacy/options.rs @@ -0,0 +1,422 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +#![allow(clippy::option_option)] + +use crate::utils; + +use clap::{Parser, Subcommand}; + +#[test] +fn required_option() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long)] + arg: i32, + } + assert_eq!( + Opt { arg: 42 }, + Opt::try_parse_from(&["test", "-a42"]).unwrap() + ); + assert_eq!( + Opt { arg: 42 }, + Opt::try_parse_from(&["test", "-a", "42"]).unwrap() + ); + assert_eq!( + Opt { arg: 42 }, + Opt::try_parse_from(&["test", "--arg", "42"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert!(Opt::try_parse_from(&["test", "-a42", "-a24"]).is_err()); +} + +#[test] +fn option_with_default() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, default_value = "42")] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); + assert_eq!(Opt { arg: 42 }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "-a42", "-a24"]).is_err()); +} + +#[test] +fn option_with_raw_default() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, default_value = "42")] + arg: i32, + } + assert_eq!( + Opt { arg: 24 }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); + assert_eq!(Opt { arg: 42 }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "-a42", "-a24"]).is_err()); +} + +#[test] +fn option_from_str() { + #[derive(Debug, PartialEq)] + struct A; + + impl<'a> From<&'a str> for A { + fn from(_: &str) -> A { + A + } + } + + #[derive(Debug, Parser, PartialEq)] + struct Opt { + #[clap(parse(from_str))] + a: Option, + } + + assert_eq!(Opt { a: None }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!( + Opt { a: Some(A) }, + Opt::try_parse_from(&["test", "foo"]).unwrap() + ); +} + +#[test] +fn option_type_is_optional() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short)] + arg: Option, + } + assert_eq!( + Opt { arg: Some(42) }, + Opt::try_parse_from(&["test", "-a42"]).unwrap() + ); + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "-a42", "-a24"]).is_err()); +} + +#[test] +fn required_with_option_type() { + #[derive(Debug, PartialEq, Eq, Parser)] + #[clap(subcommand_negates_reqs = true)] + struct Opt { + #[clap(required = true)] + req_str: Option, + + #[clap(subcommand)] + cmd: Option, + } + + #[derive(Debug, PartialEq, Eq, Subcommand)] + enum SubCommands { + ExSub { + #[clap(short, long, parse(from_occurrences))] + verbose: u8, + }, + } + + assert_eq!( + Opt { + req_str: Some(("arg").into()), + cmd: None, + }, + Opt::try_parse_from(&["test", "arg"]).unwrap() + ); + + assert_eq!( + Opt { + req_str: None, + cmd: Some(SubCommands::ExSub { verbose: 1 }), + }, + Opt::try_parse_from(&["test", "ex-sub", "-v"]).unwrap() + ); + + assert!(Opt::try_parse_from(&["test"]).is_err()); +} + +#[test] +fn ignore_qualified_option_type() { + fn parser(s: &str) -> Option { + Some(s.to_string()) + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(parse(from_str = parser))] + arg: ::std::option::Option, + } + + assert_eq!( + Opt { + arg: Some("success".into()) + }, + Opt::try_parse_from(&["test", "success"]).unwrap() + ); +} + +#[test] +fn option_option_type_is_optional_value() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, multiple_occurrences(true))] + #[allow(clippy::option_option)] + arg: Option>, + } + assert_eq!( + Opt { + arg: Some(Some(42)) + }, + Opt::try_parse_from(&["test", "-a42"]).unwrap() + ); + assert_eq!( + Opt { arg: Some(None) }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&["test"]).unwrap()); + assert!(Opt::try_parse_from(&["test", "-a42", "-a24"]).is_err()); +} + +#[test] +fn option_option_type_help() { + #[derive(Parser, Debug)] + struct Opt { + #[clap(long, value_name = "val")] + arg: Option>, + } + let help = utils::get_help::(); + assert!(help.contains("--arg []")); + assert!(!help.contains("--arg []...")); +} + +#[test] +fn two_option_option_types() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short)] + arg: Option>, + + #[clap(long)] + field: Option>, + } + assert_eq!( + Opt { + arg: Some(Some(42)), + field: Some(Some("f".into())) + }, + Opt::try_parse_from(&["test", "-a42", "--field", "f"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(Some(42)), + field: Some(None) + }, + Opt::try_parse_from(&["test", "-a42", "--field"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(None), + field: Some(None) + }, + Opt::try_parse_from(&["test", "-a", "--field"]).unwrap() + ); + assert_eq!( + Opt { + arg: Some(None), + field: Some(Some("f".into())) + }, + Opt::try_parse_from(&["test", "-a", "--field", "f"]).unwrap() + ); + assert_eq!( + Opt { + arg: None, + field: Some(None) + }, + Opt::try_parse_from(&["test", "--field"]).unwrap() + ); + assert_eq!( + Opt { + arg: None, + field: None + }, + Opt::try_parse_from(&["test"]).unwrap() + ); +} + +#[test] +fn vec_type_is_multiple_occurrences() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long)] + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); + assert_eq!(Opt { arg: vec![] }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::try_parse_from(&["test", "-a", "24", "-a", "42"]).unwrap() + ); +} + +#[test] +fn vec_type_with_required() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long, required = true)] + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); + assert!(Opt::try_parse_from(&["test"]).is_err()); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::try_parse_from(&["test", "-a", "24", "-a", "42"]).unwrap() + ); +} + +#[test] +fn vec_type_with_multiple_values_only() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long, multiple_values(true), multiple_occurrences(false))] + arg: Vec, + } + assert_eq!( + Opt { arg: vec![24] }, + Opt::try_parse_from(&["test", "-a24"]).unwrap() + ); + assert_eq!(Opt { arg: vec![] }, Opt::try_parse_from(&["test"]).unwrap()); + assert_eq!( + Opt { arg: vec![24, 42] }, + Opt::try_parse_from(&["test", "-a", "24", "42"]).unwrap() + ); +} + +#[test] +fn ignore_qualified_vec_type() { + fn parser(s: &str) -> Vec { + vec![s.to_string()] + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(parse(from_str = parser))] + arg: ::std::vec::Vec, + } + + assert_eq!( + Opt { + arg: vec!["success".into()] + }, + Opt::try_parse_from(&["test", "success"]).unwrap() + ); +} + +#[test] +fn option_vec_type() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short)] + arg: Option>, + } + assert_eq!( + Opt { arg: Some(vec![1]) }, + Opt::try_parse_from(&["test", "-a", "1"]).unwrap() + ); + + assert_eq!( + Opt { + arg: Some(vec![1, 2]) + }, + Opt::try_parse_from(&["test", "-a", "1", "-a", "2"]).unwrap() + ); + + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&["test"]).unwrap()); +} + +#[test] +fn option_vec_type_structopt_behavior() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short, long, multiple_values(true), min_values(0))] + arg: Option>, + } + assert_eq!( + Opt { arg: Some(vec![1]) }, + Opt::try_parse_from(&["test", "-a", "1"]).unwrap() + ); + + assert_eq!( + Opt { + arg: Some(vec![1, 2]) + }, + Opt::try_parse_from(&["test", "-a", "1", "2"]).unwrap() + ); + + assert_eq!( + Opt { arg: Some(vec![]) }, + Opt::try_parse_from(&["test", "-a"]).unwrap() + ); + + assert_eq!(Opt { arg: None }, Opt::try_parse_from(&["test"]).unwrap()); +} + +#[test] +fn two_option_vec_types() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(short)] + arg: Option>, + + #[clap(short)] + b: Option>, + } + + assert_eq!( + Opt { + arg: Some(vec![1]), + b: None, + }, + Opt::try_parse_from(&["test", "-a", "1"]).unwrap() + ); + + assert_eq!( + Opt { + arg: Some(vec![1]), + b: Some(vec![1]) + }, + Opt::try_parse_from(&["test", "-a", "1", "-b", "1"]).unwrap() + ); + + assert_eq!( + Opt { + arg: Some(vec![1, 2]), + b: Some(vec![1, 2]) + }, + Opt::try_parse_from(&["test", "-a", "1", "-a", "2", "-b", "1", "-b", "2"]).unwrap() + ); + + assert_eq!( + Opt { arg: None, b: None }, + Opt::try_parse_from(&["test"]).unwrap() + ); +} diff --git a/tests/derive/legacy/privacy.rs b/tests/derive/legacy/privacy.rs new file mode 100644 index 00000000..c8f66127 --- /dev/null +++ b/tests/derive/legacy/privacy.rs @@ -0,0 +1,36 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +mod options { + use clap::Parser; + + #[derive(Debug, Parser)] + pub struct Options { + #[clap(subcommand)] + pub subcommand: super::subcommands::SubCommand, + } +} + +mod subcommands { + use clap::Subcommand; + + #[derive(Debug, Subcommand)] + pub enum SubCommand { + /// foo + Foo { + /// foo + bars: String, + }, + } +} diff --git a/tests/derive/legacy/raw_bool_literal.rs b/tests/derive/legacy/raw_bool_literal.rs new file mode 100644 index 00000000..0e572aa8 --- /dev/null +++ b/tests/derive/legacy/raw_bool_literal.rs @@ -0,0 +1,29 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use clap::Parser; + +#[test] +fn raw_bool_literal() { + #[derive(Parser, Debug, PartialEq)] + #[clap(name = "raw_bool")] + struct Opt { + #[clap(raw(false))] + a: String, + #[clap(raw(true))] + b: String, + } + + assert_eq!( + Opt { + a: "one".into(), + b: "--help".into() + }, + Opt::try_parse_from(&["test", "one", "--", "--help"]).unwrap() + ); +} diff --git a/tests/derive/legacy/raw_idents.rs b/tests/derive/legacy/raw_idents.rs new file mode 100644 index 00000000..12c5d165 --- /dev/null +++ b/tests/derive/legacy/raw_idents.rs @@ -0,0 +1,24 @@ +use clap::Parser; + +#[test] +fn raw_idents() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(short, long)] + r#type: String, + } + + assert_eq!( + Opt { + r#type: "long".into() + }, + Opt::try_parse_from(&["test", "--type", "long"]).unwrap() + ); + + assert_eq!( + Opt { + r#type: "short".into() + }, + Opt::try_parse_from(&["test", "-t", "short"]).unwrap() + ); +} diff --git a/tests/derive/legacy/rename_all_env.rs b/tests/derive/legacy/rename_all_env.rs new file mode 100644 index 00000000..20d4f40c --- /dev/null +++ b/tests/derive/legacy/rename_all_env.rs @@ -0,0 +1,47 @@ +#![cfg(feature = "env")] + +use crate::utils; + +use clap::Parser; + +#[test] +fn it_works() { + #[derive(Debug, PartialEq, Parser)] + #[clap(rename_all_env = "kebab")] + struct BehaviorModel { + #[clap(env)] + be_nice: String, + } + + let help = utils::get_help::(); + assert!(help.contains("[env: be-nice=]")); +} + +#[test] +fn default_is_screaming() { + #[derive(Debug, PartialEq, Parser)] + struct BehaviorModel { + #[clap(env)] + be_nice: String, + } + + let help = utils::get_help::(); + assert!(help.contains("[env: BE_NICE=]")); +} + +#[test] +fn overridable() { + #[derive(Debug, PartialEq, Parser)] + #[clap(rename_all_env = "kebab")] + struct BehaviorModel { + #[clap(env)] + be_nice: String, + + #[clap(rename_all_env = "pascal", env)] + be_aggressive: String, + } + + let help = utils::get_help::(); + assert!(help.contains("[env: be-nice=]")); + assert!(help.contains("[env: BeAggressive=]")); +} diff --git a/tests/derive/legacy/skip.rs b/tests/derive/legacy/skip.rs new file mode 100644 index 00000000..85531343 --- /dev/null +++ b/tests/derive/legacy/skip.rs @@ -0,0 +1,155 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use clap::Parser; + +#[test] +fn skip_1() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(short)] + x: u32, + #[clap(skip)] + s: u32, + } + + assert!(Opt::try_parse_from(&["test", "-x", "10", "20"]).is_err()); + + let mut opt = Opt::try_parse_from(&["test", "-x", "10"]).unwrap(); + assert_eq!( + opt, + Opt { + x: 10, + s: 0, // default + } + ); + opt.s = 42; + + opt.update_from(&["test", "-x", "22"]); + + assert_eq!(opt, Opt { x: 22, s: 42 }); +} + +#[test] +fn skip_2() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[clap(short)] + x: u32, + #[clap(skip)] + ss: String, + #[clap(skip)] + sn: u8, + y: u32, + #[clap(skip)] + sz: u16, + t: u32, + } + + assert_eq!( + Opt::try_parse_from(&["test", "-x", "10", "20", "30"]).unwrap(), + Opt { + x: 10, + ss: String::from(""), + sn: 0, + y: 20, + sz: 0, + t: 30, + } + ); +} + +#[test] +fn skip_enum() { + #[derive(Debug, PartialEq)] + #[allow(unused)] + enum Kind { + A, + B, + } + + impl Default for Kind { + fn default() -> Self { + Kind::B + } + } + + #[derive(Parser, Debug, PartialEq)] + pub struct Opt { + #[clap(long, short)] + number: u32, + #[clap(skip)] + k: Kind, + #[clap(skip)] + v: Vec, + } + + assert_eq!( + Opt::try_parse_from(&["test", "-n", "10"]).unwrap(), + Opt { + number: 10, + k: Kind::B, + v: vec![], + } + ); +} + +#[test] +fn skip_help_doc_comments() { + #[derive(Parser, Debug, PartialEq)] + pub struct Opt { + #[clap(skip, help = "internal_stuff")] + a: u32, + + #[clap(skip, long_help = "internal_stuff\ndo not touch")] + b: u32, + + /// Not meant to be used by clap. + /// + /// I want a default here. + #[clap(skip)] + c: u32, + + #[clap(short, parse(try_from_str))] + n: u32, + } + + assert_eq!( + Opt::try_parse_from(&["test", "-n", "10"]).unwrap(), + Opt { + n: 10, + a: 0, + b: 0, + c: 0, + } + ); +} + +#[test] +fn skip_val() { + #[derive(Parser, Debug, PartialEq)] + pub struct Opt { + #[clap(long, short)] + number: u32, + + #[clap(skip = "key")] + k: String, + + #[clap(skip = vec![1, 2, 3])] + v: Vec, + } + + assert_eq!( + Opt::try_parse_from(&["test", "-n", "10"]).unwrap(), + Opt { + number: 10, + k: "key".to_string(), + v: vec![1, 2, 3] + } + ); +} diff --git a/tests/derive/legacy/structopt.rs b/tests/derive/legacy/structopt.rs new file mode 100644 index 00000000..56024856 --- /dev/null +++ b/tests/derive/legacy/structopt.rs @@ -0,0 +1,23 @@ +#![allow(deprecated)] + +use clap::{AppSettings, StructOpt}; + +#[test] +fn compatible() { + #[derive(StructOpt)] + #[structopt(author, version, about)] + #[structopt(global_setting(AppSettings::PropagateVersion))] + struct Cli { + #[structopt(subcommand)] + command: Commands, + } + + #[derive(StructOpt)] + #[structopt(setting(AppSettings::SubcommandRequiredElseHelp))] + enum Commands { + /// Adds files to myapp + Add { name: Option }, + } + + Cli::from_iter(["test", "add"]); +} diff --git a/tests/derive/legacy/subcommands.rs b/tests/derive/legacy/subcommands.rs new file mode 100644 index 00000000..7dad15f2 --- /dev/null +++ b/tests/derive/legacy/subcommands.rs @@ -0,0 +1,605 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) , +// Kevin Knapp (@kbknapp) , and +// Ana Hobden (@hoverbear) +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use crate::utils; + +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser, PartialEq, Debug)] +enum Opt { + /// Fetch stuff from GitHub + Fetch { + #[clap(long)] + all: bool, + #[clap(short, long)] + /// Overwrite local branches. + force: bool, + repo: String, + }, + + Add { + #[clap(short, long)] + interactive: bool, + #[clap(short, long)] + verbose: bool, + }, +} + +#[test] +fn test_fetch() { + assert_eq!( + Opt::Fetch { + all: true, + force: false, + repo: "origin".to_string() + }, + Opt::try_parse_from(&["test", "fetch", "--all", "origin"]).unwrap() + ); + assert_eq!( + Opt::Fetch { + all: false, + force: true, + repo: "origin".to_string() + }, + Opt::try_parse_from(&["test", "fetch", "-f", "origin"]).unwrap() + ); +} + +#[test] +fn test_add() { + assert_eq!( + Opt::Add { + interactive: false, + verbose: false + }, + Opt::try_parse_from(&["test", "add"]).unwrap() + ); + assert_eq!( + Opt::Add { + interactive: true, + verbose: true + }, + Opt::try_parse_from(&["test", "add", "-i", "-v"]).unwrap() + ); +} + +#[test] +fn test_no_parse() { + let result = Opt::try_parse_from(&["test", "badcmd", "-i", "-v"]); + assert!(result.is_err()); + + let result = Opt::try_parse_from(&["test", "add", "--badoption"]); + assert!(result.is_err()); + + let result = Opt::try_parse_from(&["test"]); + assert!(result.is_err()); +} + +#[derive(Parser, PartialEq, Debug)] +enum Opt2 { + DoSomething { arg: String }, +} + +#[test] +/// This test is specifically to make sure that hyphenated subcommands get +/// processed correctly. +fn test_hyphenated_subcommands() { + assert_eq!( + Opt2::DoSomething { + arg: "blah".to_string() + }, + Opt2::try_parse_from(&["test", "do-something", "blah"]).unwrap() + ); +} + +#[derive(Parser, PartialEq, Debug)] +enum Opt3 { + Add, + Init, + Fetch, +} + +#[test] +fn test_null_commands() { + assert_eq!(Opt3::Add, Opt3::try_parse_from(&["test", "add"]).unwrap()); + assert_eq!(Opt3::Init, Opt3::try_parse_from(&["test", "init"]).unwrap()); + assert_eq!( + Opt3::Fetch, + Opt3::try_parse_from(&["test", "fetch"]).unwrap() + ); +} + +#[derive(Parser, PartialEq, Debug)] +#[clap(about = "Not shown")] +struct Add { + file: String, +} +/// Not shown +#[derive(Parser, PartialEq, Debug)] +struct Fetch { + remote: String, +} +#[derive(Parser, PartialEq, Debug)] +enum Opt4 { + // Not shown + /// Add a file + Add(Add), + Init, + /// download history from remote + Fetch(Fetch), +} + +#[test] +fn test_tuple_commands() { + assert_eq!( + Opt4::Add(Add { + file: "f".to_string() + }), + Opt4::try_parse_from(&["test", "add", "f"]).unwrap() + ); + assert_eq!(Opt4::Init, Opt4::try_parse_from(&["test", "init"]).unwrap()); + assert_eq!( + Opt4::Fetch(Fetch { + remote: "origin".to_string() + }), + Opt4::try_parse_from(&["test", "fetch", "origin"]).unwrap() + ); + + let output = utils::get_long_help::(); + + assert!(output.contains("download history from remote")); + assert!(output.contains("Add a file")); + assert!(!output.contains("Not shown")); +} + +#[test] +fn global_passed_down() { + #[derive(Debug, PartialEq, Parser)] + struct Opt { + #[clap(global = true, long)] + other: bool, + #[clap(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, Subcommand)] + enum Subcommands { + Add, + Global(GlobalCmd), + } + + #[derive(Debug, PartialEq, Args)] + struct GlobalCmd { + #[clap(from_global)] + other: bool, + } + + assert_eq!( + Opt::try_parse_from(&["test", "global"]).unwrap(), + Opt { + other: false, + sub: Subcommands::Global(GlobalCmd { other: false }) + } + ); + + assert_eq!( + Opt::try_parse_from(&["test", "global", "--other"]).unwrap(), + Opt { + other: true, + sub: Subcommands::Global(GlobalCmd { other: true }) + } + ); +} + +#[test] +fn external_subcommand() { + #[derive(Debug, PartialEq, Parser)] + struct Opt { + #[clap(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, Subcommand)] + enum Subcommands { + Add, + Remove, + #[clap(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::try_parse_from(&["test", "add"]).unwrap(), + Opt { + sub: Subcommands::Add + } + ); + + assert_eq!( + Opt::try_parse_from(&["test", "remove"]).unwrap(), + Opt { + sub: Subcommands::Remove + } + ); + + assert!(Opt::try_parse_from(&["test"]).is_err()); + + assert_eq!( + Opt::try_parse_from(&["test", "git", "status"]).unwrap(), + Opt { + sub: Subcommands::Other(vec!["git".into(), "status".into()]) + } + ); +} + +#[test] +fn external_subcommand_os_string() { + use std::ffi::OsString; + + #[derive(Debug, PartialEq, Parser)] + struct Opt { + #[clap(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, Subcommand)] + enum Subcommands { + #[clap(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::try_parse_from(&["test", "git", "status"]).unwrap(), + Opt { + sub: Subcommands::Other(vec!["git".into(), "status".into()]) + } + ); + + assert!(Opt::try_parse_from(&["test"]).is_err()); +} + +#[test] +fn external_subcommand_optional() { + #[derive(Debug, PartialEq, Parser)] + struct Opt { + #[clap(subcommand)] + sub: Option, + } + + #[derive(Debug, PartialEq, Subcommand)] + enum Subcommands { + #[clap(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::try_parse_from(&["test", "git", "status"]).unwrap(), + Opt { + sub: Some(Subcommands::Other(vec!["git".into(), "status".into()])) + } + ); + + assert_eq!(Opt::try_parse_from(&["test"]).unwrap(), Opt { sub: None }); +} + +#[test] +fn enum_in_enum_subsubcommand() { + #[derive(Parser, Debug, PartialEq)] + pub enum Opt { + #[clap(alias = "l")] + List, + #[clap(subcommand, alias = "d")] + Daemon(DaemonCommand), + } + + #[derive(Subcommand, Debug, PartialEq)] + pub enum DaemonCommand { + Start, + Stop, + } + + let result = Opt::try_parse_from(&["test"]); + assert!(result.is_err()); + + let result = Opt::try_parse_from(&["test", "list"]).unwrap(); + assert_eq!(Opt::List, result); + + let result = Opt::try_parse_from(&["test", "l"]).unwrap(); + assert_eq!(Opt::List, result); + + let result = Opt::try_parse_from(&["test", "daemon"]); + assert!(result.is_err()); + + let result = Opt::try_parse_from(&["test", "daemon", "start"]).unwrap(); + assert_eq!(Opt::Daemon(DaemonCommand::Start), result); + + let result = Opt::try_parse_from(&["test", "d", "start"]).unwrap(); + assert_eq!(Opt::Daemon(DaemonCommand::Start), result); +} + +#[test] +fn update_subcommands() { + #[derive(Parser, PartialEq, Debug)] + enum Opt { + Command1(Command1), + Command2(Command2), + } + + #[derive(Parser, PartialEq, Debug)] + struct Command1 { + arg1: i32, + arg2: i32, + } + + #[derive(Parser, PartialEq, Debug)] + struct Command2 { + arg2: i32, + } + + // Full subcommand update + let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 }); + opt.try_update_from(&["test", "command1", "42", "44"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command1", "42", "44"]).unwrap(), + opt + ); + + // Partial subcommand update + let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 }); + opt.try_update_from(&["test", "command1", "42"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command1", "42", "14"]).unwrap(), + opt + ); + + // Change subcommand + let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 }); + opt.try_update_from(&["test", "command2", "43"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command2", "43"]).unwrap(), + opt + ); +} + +#[test] +fn update_sub_subcommands() { + #[derive(Parser, PartialEq, Debug)] + enum Opt { + #[clap(subcommand)] + Child1(Child1), + #[clap(subcommand)] + Child2(Child2), + } + + #[derive(Subcommand, PartialEq, Debug)] + enum Child1 { + Command1(Command1), + Command2(Command2), + } + + #[derive(Subcommand, PartialEq, Debug)] + enum Child2 { + Command1(Command1), + Command2(Command2), + } + + #[derive(Args, PartialEq, Debug)] + struct Command1 { + arg1: i32, + arg2: i32, + } + + #[derive(Args, PartialEq, Debug)] + struct Command2 { + arg2: i32, + } + + // Full subcommand update + let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "child1", "command1", "42", "44"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "child1", "command1", "42", "44"]).unwrap(), + opt + ); + + // Partial subcommand update + let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "child1", "command1", "42"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "child1", "command1", "42", "14"]).unwrap(), + opt + ); + + // Partial subcommand update + let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "child1", "command2", "43"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "child1", "command2", "43"]).unwrap(), + opt + ); + + // Change subcommand + let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 })); + opt.try_update_from(&["test", "child2", "command2", "43"]) + .unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "child2", "command2", "43"]).unwrap(), + opt + ); +} + +#[test] +fn update_ext_subcommand() { + #[derive(Parser, PartialEq, Debug)] + enum Opt { + Command1(Command1), + Command2(Command2), + #[clap(external_subcommand)] + Ext(Vec), + } + + #[derive(Args, PartialEq, Debug)] + struct Command1 { + arg1: i32, + arg2: i32, + } + + #[derive(Args, PartialEq, Debug)] + struct Command2 { + arg2: i32, + } + + // Full subcommand update + let mut opt = Opt::Ext(vec!["12".into(), "14".into()]); + opt.try_update_from(&["test", "ext", "42", "44"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "ext", "42", "44"]).unwrap(), + opt + ); + + // No partial subcommand update + let mut opt = Opt::Ext(vec!["12".into(), "14".into()]); + opt.try_update_from(&["test", "ext", "42"]).unwrap(); + assert_eq!(Opt::try_parse_from(&["test", "ext", "42"]).unwrap(), opt); + + // Change subcommand + let mut opt = Opt::Ext(vec!["12".into(), "14".into()]); + opt.try_update_from(&["test", "command2", "43"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "command2", "43"]).unwrap(), + opt + ); + + let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 }); + opt.try_update_from(&["test", "ext", "42", "44"]).unwrap(); + assert_eq!( + Opt::try_parse_from(&["test", "ext", "42", "44"]).unwrap(), + opt + ); +} +#[test] +fn subcommand_name_not_literal() { + fn get_name() -> &'static str { + "renamed" + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(subcommand)] + subcmd: SubCmd, + } + + #[derive(Subcommand, PartialEq, Debug)] + enum SubCmd { + #[clap(name = get_name())] + SubCmd1, + } + + assert!(Opt::try_parse_from(&["test", "renamed"]).is_ok()); +} + +#[test] +fn skip_subcommand() { + #[derive(Debug, PartialEq, Parser)] + struct Opt { + #[clap(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, Subcommand)] + enum Subcommands { + Add, + Remove, + + #[allow(dead_code)] + #[clap(skip)] + Skip, + } + + assert_eq!( + Opt::try_parse_from(&["test", "add"]).unwrap(), + Opt { + sub: Subcommands::Add + } + ); + + assert_eq!( + Opt::try_parse_from(&["test", "remove"]).unwrap(), + Opt { + sub: Subcommands::Remove + } + ); + + let res = Opt::try_parse_from(&["test", "skip"]); + assert_eq!(res.unwrap_err().kind(), clap::ErrorKind::UnknownArgument,); +} + +#[test] +#[cfg(feature = "unstable-v4")] +fn built_in_subcommand_escaped() { + #[derive(Debug, PartialEq, Parser)] + enum Command { + Install { + arg: Option, + }, + #[clap(external_subcommand)] + Custom(Vec), + } + + assert_eq!( + Command::try_parse_from(&["test", "install", "arg"]).unwrap(), + Command::Install { + arg: Some(String::from("arg")) + } + ); + assert_eq!( + Command::try_parse_from(&["test", "--", "install"]).unwrap(), + Command::Custom(vec![String::from("install")]) + ); + assert_eq!( + Command::try_parse_from(&["test", "--", "install", "arg"]).unwrap(), + Command::Custom(vec![String::from("install"), String::from("arg")]) + ); +} + +#[test] +#[cfg(not(feature = "unstable-v4"))] +fn built_in_subcommand_escaped() { + #[derive(Debug, PartialEq, Parser)] + enum Command { + Install { + arg: Option, + }, + #[clap(external_subcommand)] + Custom(Vec), + } + + assert_eq!( + Command::try_parse_from(&["test", "install", "arg"]).unwrap(), + Command::Install { + arg: Some(String::from("arg")) + } + ); + assert_eq!( + Command::try_parse_from(&["test", "--", "install"]).unwrap(), + Command::Install { arg: None } + ); + assert_eq!( + Command::try_parse_from(&["test", "--", "install", "arg"]).unwrap(), + Command::Install { arg: None } + ); +} diff --git a/tests/derive/legacy/type_alias_regressions.rs b/tests/derive/legacy/type_alias_regressions.rs new file mode 100644 index 00000000..1c70936b --- /dev/null +++ b/tests/derive/legacy/type_alias_regressions.rs @@ -0,0 +1,47 @@ +/// Regression test to ensure that type aliases do not cause compilation failures. +use std::str::FromStr; + +use clap::{ArgEnum, Parser, Subcommand}; + +// Result type alias +#[allow(dead_code)] +type Result = std::result::Result>; + +// Wrapper to use for Option type alias +#[derive(Debug, PartialEq, Eq)] +struct Wrapper(T); + +impl FromStr for Wrapper { + type Err = ::Err; + + fn from_str(s: &str) -> std::result::Result { + T::from_str(s).map(Wrapper) + } +} + +type Option = std::option::Option>; + +#[derive(Parser)] +pub struct Opts { + another_string: String, + #[clap(subcommand)] + command: Command, + #[clap(short, long, arg_enum)] + choice: ArgChoice, +} + +#[derive(Subcommand, PartialEq, Debug)] +enum Command { + DoSomething { arg: Option }, +} + +#[derive(ArgEnum, PartialEq, Debug, Clone)] +enum ArgChoice { + Foo, + Bar, +} + +#[test] +fn type_alias_regressions() { + Opts::try_parse_from(["test", "value", "--choice=foo", "do-something"]).unwrap(); +} diff --git a/tests/derive/legacy/utf8.rs b/tests/derive/legacy/utf8.rs new file mode 100644 index 00000000..0f9fd15d --- /dev/null +++ b/tests/derive/legacy/utf8.rs @@ -0,0 +1,226 @@ +#![cfg(not(windows))] + +use clap::{ErrorKind, Parser}; +use std::ffi::OsString; +use std::os::unix::ffi::OsStringExt; + +#[derive(Parser, Debug, PartialEq, Eq)] +struct Positional { + arg: String, +} + +#[derive(Parser, Debug, PartialEq, Eq)] +struct Named { + #[clap(short, long)] + arg: String, +} + +#[test] +fn invalid_utf8_strict_positional() { + let m = Positional::try_parse_from(vec![OsString::from(""), OsString::from_vec(vec![0xe9])]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn invalid_utf8_strict_option_short_space() { + let m = Named::try_parse_from(vec![ + OsString::from(""), + OsString::from("-a"), + OsString::from_vec(vec![0xe9]), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn invalid_utf8_strict_option_short_equals() { + let m = Named::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x61, 0x3d, 0xe9]), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn invalid_utf8_strict_option_short_no_space() { + let m = Named::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x61, 0xe9]), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn invalid_utf8_strict_option_long_space() { + let m = Named::try_parse_from(vec![ + OsString::from(""), + OsString::from("--arg"), + OsString::from_vec(vec![0xe9]), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn invalid_utf8_strict_option_long_equals() { + let m = Named::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x2d, 0x61, 0x72, 0x67, 0x3d, 0xe9]), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[derive(Parser, Debug, PartialEq, Eq)] +struct PositionalOs { + #[clap(parse(from_os_str))] + arg: OsString, +} + +#[derive(Parser, Debug, PartialEq, Eq)] +struct NamedOs { + #[clap(short, long, parse(from_os_str))] + arg: OsString, +} + +#[test] +fn invalid_utf8_positional() { + let r = PositionalOs::try_parse_from(vec![OsString::from(""), OsString::from_vec(vec![0xe9])]); + assert_eq!( + r.unwrap(), + PositionalOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[test] +fn invalid_utf8_option_short_space() { + let r = NamedOs::try_parse_from(vec![ + OsString::from(""), + OsString::from("-a"), + OsString::from_vec(vec![0xe9]), + ]); + assert_eq!( + r.unwrap(), + NamedOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[test] +fn invalid_utf8_option_short_equals() { + let r = NamedOs::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x61, 0x3d, 0xe9]), + ]); + assert_eq!( + r.unwrap(), + NamedOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[test] +fn invalid_utf8_option_short_no_space() { + let r = NamedOs::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x61, 0xe9]), + ]); + assert_eq!( + r.unwrap(), + NamedOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[test] +fn invalid_utf8_option_long_space() { + let r = NamedOs::try_parse_from(vec![ + OsString::from(""), + OsString::from("--arg"), + OsString::from_vec(vec![0xe9]), + ]); + assert_eq!( + r.unwrap(), + NamedOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[test] +fn invalid_utf8_option_long_equals() { + let r = NamedOs::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0x2d, 0x2d, 0x61, 0x72, 0x67, 0x3d, 0xe9]), + ]); + assert_eq!( + r.unwrap(), + NamedOs { + arg: OsString::from_vec(vec![0xe9]) + } + ); +} + +#[derive(Debug, PartialEq, Parser)] +enum External { + #[clap(external_subcommand)] + Other(Vec), +} + +#[test] +fn refuse_invalid_utf8_subcommand_with_allow_external_subcommands() { + let m = External::try_parse_from(vec![ + OsString::from(""), + OsString::from_vec(vec![0xe9]), + OsString::from("normal"), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[test] +fn refuse_invalid_utf8_subcommand_args_with_allow_external_subcommands() { + let m = External::try_parse_from(vec![ + OsString::from(""), + OsString::from("subcommand"), + OsString::from("normal"), + OsString::from_vec(vec![0xe9]), + OsString::from("--another_normal"), + ]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidUtf8); +} + +#[derive(Debug, PartialEq, Parser)] +enum ExternalOs { + #[clap(external_subcommand)] + Other(Vec), +} + +#[test] +fn allow_invalid_utf8_subcommand_args_with_allow_external_subcommands() { + let m = ExternalOs::try_parse_from(vec![ + OsString::from(""), + OsString::from("subcommand"), + OsString::from("normal"), + OsString::from_vec(vec![0xe9]), + OsString::from("--another_normal"), + ]); + assert_eq!( + m.unwrap(), + ExternalOs::Other(vec![ + OsString::from("subcommand"), + OsString::from("normal"), + OsString::from_vec(vec![0xe9]), + OsString::from("--another_normal"), + ]) + ); +} diff --git a/tests/derive/legacy/utils.rs b/tests/derive/legacy/utils.rs new file mode 100644 index 00000000..6b649c3e --- /dev/null +++ b/tests/derive/legacy/utils.rs @@ -0,0 +1,56 @@ +// Hi, future me (or whoever you are)! +// +// Yes, we do need this attr. +// No, the warnings cannot be fixed otherwise. +// Accept and endure. Do not touch. +#![allow(unused)] + +use clap::CommandFactory; + +pub fn get_help() -> String { + let mut output = Vec::new(); + ::command() + .write_help(&mut output) + .unwrap(); + let output = String::from_utf8(output).unwrap(); + + eprintln!("\n%%% HELP %%%:=====\n{}\n=====\n", output); + eprintln!("\n%%% HELP (DEBUG) %%%:=====\n{:?}\n=====\n", output); + + output +} + +pub fn get_long_help() -> String { + let mut output = Vec::new(); + ::command() + .write_long_help(&mut output) + .unwrap(); + let output = String::from_utf8(output).unwrap(); + + eprintln!("\n%%% LONG_HELP %%%:=====\n{}\n=====\n", output); + eprintln!("\n%%% LONG_HELP (DEBUG) %%%:=====\n{:?}\n=====\n", output); + + output +} + +pub fn get_subcommand_long_help(subcmd: &str) -> String { + let mut output = Vec::new(); + ::command() + .get_subcommands_mut() + .find(|s| s.get_name() == subcmd) + .unwrap() + .write_long_help(&mut output) + .unwrap(); + let output = String::from_utf8(output).unwrap(); + + eprintln!( + "\n%%% SUBCOMMAND `{}` HELP %%%:=====\n{}\n=====\n", + subcmd, output + ); + eprintln!( + "\n%%% SUBCOMMAND `{}` HELP (DEBUG) %%%:=====\n{:?}\n=====\n", + subcmd, output + ); + + output +} diff --git a/tests/derive/main.rs b/tests/derive/main.rs index 3e2c10e3..f352244f 100644 --- a/tests/derive/main.rs +++ b/tests/derive/main.rs @@ -16,6 +16,7 @@ mod flatten; mod generic; mod help; mod issues; +mod legacy; mod macros; mod naming; mod nested_subcommands;