From 9c6234880683f301861c43859f3c5948067aa184 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 30 Aug 2020 00:08:04 +0200 Subject: [PATCH 1/3] expose a HelpMessage trait implemented for structs --- argh_derive/src/help.rs | 14 +++++++++----- argh_derive/src/lib.rs | 8 +++++--- src/lib.rs | 11 +++++++++++ tests/lib.rs | 8 +------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 80ce961..30461ab 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -21,7 +21,7 @@ const SECTION_SEPARATOR: &str = "\n\n"; /// in favor of the `subcommand` argument. pub(crate) fn help( errors: &Errors, - cmd_name_str_array_ident: syn::Ident, + ty_ident: &syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, @@ -98,10 +98,14 @@ pub(crate) fn help( format_lit.push_str("\n"); - quote! { { - #subcommand_calculation - format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg) - } } + quote! { + impl ::argh::HelpMessage for #ty_ident { + fn help_message(command_name: &[&str]) -> String { + #subcommand_calculation + format!(#format_lit, command_name = command_name.join(" "), #subcommand_format_arg) + } + } + } } /// A section composed of exactly just the literals provided to the program. diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 8d15871..f181c6d 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -304,7 +304,7 @@ fn impl_from_args_struct( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span.clone()); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_impl = help::help(errors, name, type_attrs, &fields, subcommand); let trait_impl = quote_spanned! { impl_span => impl argh::FromArgs for #name { @@ -377,8 +377,9 @@ fn impl_from_args_struct( } if __help { + let __help_message = ::help_message(#cmd_name_str_array_ident); return std::result::Result::Err(argh::EarlyExit { - output: #help, + output: __help_message, status: std::result::Result::Ok(()), }); } @@ -396,7 +397,8 @@ fn impl_from_args_struct( } #top_or_sub_cmd_impl - }; + #help_impl + }; trait_impl.into() } diff --git a/src/lib.rs b/src/lib.rs index ae16a6a..a39ead5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,6 +207,17 @@ impl SubCommands for T { const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND]; } +/// A `HelpMessage` implementation that provides a help/usage message corresponding +/// to the type's `FromArgs` implementation. +pub trait HelpMessage: FromArgs { + /// The help/usage message. + /// + /// The first argument `command_name` is the identifier for the current + /// command, treating each segment as space-separated. This will be used + /// in the help message. + fn help_message(command_name: &[&str]) -> String; +} + /// Information to display to the user about why a `FromArgs` construction exited early. /// /// This can occur due to either failed parsing or a flag like `--help`. diff --git a/tests/lib.rs b/tests/lib.rs index a19ed5a..e2b1537 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -317,13 +317,7 @@ Options: #[test] fn mixed_with_option() { - assert_output( - &["first", "--b", "foo"], - WithOption { - a: "first".into(), - b: "foo".into(), - }, - ); + assert_output(&["first", "--b", "foo"], WithOption { a: "first".into(), b: "foo".into() }); assert_error::( &[], From 2b0f173d1d610d25ede84b4da24d26e92a3f4542 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 5 Sep 2020 19:14:22 +0200 Subject: [PATCH 2/3] add a disable_help flag --- argh_derive/src/errors.rs | 1 + argh_derive/src/help.rs | 6 ++++-- argh_derive/src/lib.rs | 3 ++- argh_derive/src/parse_attrs.rs | 14 +++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/argh_derive/src/errors.rs b/argh_derive/src/errors.rs index a5b69e6..ac43b3b 100644 --- a/argh_derive/src/errors.rs +++ b/argh_derive/src/errors.rs @@ -91,6 +91,7 @@ impl Errors { (expect_lit_str, LitStr, Str, "string"), (expect_lit_char, LitChar, Char, "character"), (expect_lit_int, LitInt, Int, "integer"), + (expect_lit_bool, LitBool, Bool, "boolean"), ]; expect_meta_fn![ diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 30461ab..815392e 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -62,8 +62,10 @@ pub(crate) fn help( for option in options { option_description(errors, &mut format_lit, option); } - // Also include "help" - option_description_format(&mut format_lit, None, "--help", "display usage information"); + // Also include "help" unless it has been disabled + if !ty_attrs.disable_help.as_ref().map(|lit_bool| lit_bool.value).unwrap_or(false) { + option_description_format(&mut format_lit, None, "--help", "display usage information"); + } let subcommand_calculation; let subcommand_format_arg; diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index f181c6d..19a15cc 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -305,6 +305,7 @@ fn impl_from_args_struct( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span.clone()); let help_impl = help::help(errors, name, type_attrs, &fields, subcommand); + let disable_help = type_attrs.disable_help.clone().unwrap_or_else(|| syn::LitBool { value: false, span: impl_span.clone() }); let trait_impl = quote_spanned! { impl_span => impl argh::FromArgs for #name { @@ -328,7 +329,7 @@ fn impl_from_args_struct( let mut __positional_index = 0; 'parse_args: while let Some(&__next_arg) = __remaining_args.get(0) { __remaining_args = &__remaining_args[1..]; - if __next_arg == "--help" || __next_arg == "help" { + if !#disable_help && __next_arg == "--help" || __next_arg == "help" { __help = true; continue; } diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs index 3fb79de..3500c2c 100644 --- a/argh_derive/src/parse_attrs.rs +++ b/argh_derive/src/parse_attrs.rs @@ -268,6 +268,7 @@ pub struct TypeAttrs { pub is_subcommand: Option, pub name: Option, pub description: Option, + pub disable_help: Option, pub examples: Vec, pub notes: Vec, pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, @@ -298,6 +299,10 @@ impl TypeAttrs { if let Some(m) = errors.expect_meta_name_value(&meta) { parse_attr_description(errors, m, &mut this.description); } + } else if name.is_ident("disable_help") { + if let Some(m) = errors.expect_meta_name_value(&meta) { + this.parse_attr_disable_help(errors, m); + } } else if name.is_ident("error_code") { if let Some(m) = errors.expect_meta_list(&meta) { this.parse_attr_error_code(errors, m); @@ -369,6 +374,13 @@ impl TypeAttrs { } } + fn parse_attr_disable_help(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + if let Some(first) = self.disable_help.as_ref() { + errors.duplicate_attrs("disable_help", &first, m); + } + self.disable_help = errors.expect_lit_bool(&m.lit).cloned(); + } + fn parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList) { if ml.nested.len() != 2 { errors.err(&ml, "Expected two arguments, an error number and a string description"); @@ -484,7 +496,7 @@ fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Op /// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]` /// attribute and that it does not have any other type-level `#[argh(...)]` attributes. pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) { - let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes } = type_attrs; + let TypeAttrs { is_subcommand, name, description, disable_help: _, examples, notes, error_codes } = type_attrs; // Ensure that `#[argh(subcommand)]` is present. if is_subcommand.is_none() { From 6c5067c24e56917b34ef571468808e08de5a742a Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 5 Sep 2020 19:26:30 +0200 Subject: [PATCH 3/3] add tests --- argh_derive/src/lib.rs | 8 ++++---- tests/lib.rs | 28 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 19a15cc..b26532a 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -115,7 +115,7 @@ struct StructField<'a> { impl<'a> StructField<'a> { /// Attempts to parse a field of a `#[derive(FromArgs)]` struct, pulling out the /// fields required for code generation. - fn new(errors: &Errors, field: &'a syn::Field, attrs: FieldAttrs) -> Option { + fn new(errors: &Errors, ty_attrs: &TypeAttrs, field: &'a syn::Field, attrs: FieldAttrs) -> Option { let name = field.ident.as_ref().expect("missing ident for named field"); // Ensure that one "kind" is present (switch, option, subcommand, positional) @@ -193,8 +193,8 @@ impl<'a> StructField<'a> { .as_ref() .map(syn::LitStr::value) .unwrap_or_else(|| heck::KebabCase::to_kebab_case(&*name.to_string())); - if long_name == "help" { - errors.err(field, "Custom `--help` flags are not supported."); + if long_name == "help" && !ty_attrs.disable_help.as_ref().map(|lit_bool| lit_bool.value).unwrap_or(false) { + errors.err(field, "Custom `--help` flags are not supported unless `#[argh(disable_help = true)]` is specified."); } let long_name = format!("--{}", long_name); Some(long_name) @@ -233,7 +233,7 @@ fn impl_from_args_struct( .iter() .filter_map(|field| { let attrs = FieldAttrs::parse(errors, field); - StructField::new(errors, field, attrs) + StructField::new(errors, &type_attrs, field, attrs) }) .collect(); diff --git a/tests/lib.rs b/tests/lib.rs index e2b1537..178949f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -3,7 +3,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -use {argh::FromArgs, std::fmt::Debug}; +use { + argh::{FromArgs, HelpMessage}, + std::fmt::Debug, +}; #[test] fn basic_example() { @@ -45,6 +48,22 @@ fn custom_from_str_example() { assert_eq!(f.five, 5); } +#[test] +fn custom_help_flag_example() { + #[derive(FromArgs)] + #[argh(disable_help = true)] + /// Yay, `-?` will work. + struct OnlyQuestionmark { + /// show this help message + #[argh(switch, short = '?')] + help: bool, + } + + let oq = OnlyQuestionmark::from_args(&["cmdname"], &["-?"]).expect("failed custom help flag"); + assert_eq!(oq.help, true); + assert_help_message::("Usage: test_arg_0 [-?]\n\nYay, `-?` will work.\n\nOptions:\n -?, --help show this help message\n"); +} + #[test] fn subcommand_example() { #[derive(FromArgs, PartialEq, Debug)] @@ -185,7 +204,8 @@ fn missing_option_value() { assert!(e.status.is_err()); } -fn assert_help_string(help_str: &str) { +fn assert_help_string(help_str: &str) { + assert_help_message::(help_str); match T::from_args(&["test_arg_0"], &["--help"]) { Ok(_) => panic!("help was parsed as args"), Err(e) => { @@ -195,6 +215,10 @@ fn assert_help_string(help_str: &str) { } } +fn assert_help_message(help_str: &str) { + assert_eq!(help_str, T::help_message(&["test_arg_0"])); +} + fn assert_output(args: &[&str], expected: T) { let t = T::from_args(&["cmd"], args).expect("failed to parse"); assert_eq!(t, expected);