Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose HelpMessage and allow for custom help switches #53

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions argh_derive/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![
Expand Down
20 changes: 13 additions & 7 deletions argh_derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<'_>>,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -98,10 +100,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.
Expand Down
19 changes: 11 additions & 8 deletions argh_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
fn new(errors: &Errors, ty_attrs: &TypeAttrs, field: &'a syn::Field, attrs: FieldAttrs) -> Option<Self> {
let name = field.ident.as_ref().expect("missing ident for named field");

// Ensure that one "kind" is present (switch, option, subcommand, positional)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -304,7 +304,8 @@ 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 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 {
Expand All @@ -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;
}
Expand Down Expand Up @@ -377,8 +378,9 @@ fn impl_from_args_struct(
}

if __help {
let __help_message = <Self as ::argh::HelpMessage>::help_message(#cmd_name_str_array_ident);
return std::result::Result::Err(argh::EarlyExit {
output: #help,
output: __help_message,
status: std::result::Result::Ok(()),
});
}
Expand All @@ -396,7 +398,8 @@ fn impl_from_args_struct(
}

#top_or_sub_cmd_impl
};
#help_impl
};

trait_impl.into()
}
Expand Down
14 changes: 13 additions & 1 deletion argh_derive/src/parse_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ pub struct TypeAttrs {
pub is_subcommand: Option<syn::Ident>,
pub name: Option<syn::LitStr>,
pub description: Option<Description>,
pub disable_help: Option<syn::LitBool>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistically, I think flipping this to "enable_help" reads a bit better. That avoids devs having to mentally parse expressions like if !disable_help { ... }.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose disable_help because as a dev I usually assume optional boolean flags to default to false, but I can change that if you want

pub examples: Vec<syn::LitStr>,
pub notes: Vec<syn::LitStr>,
pub error_codes: Vec<(syn::LitInt, syn::LitStr)>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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() {
Expand Down
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ impl<T: SubCommand> 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an additional trait instead of adding a method to FromArgs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help message only exists for structs, not for enums, so it's not implemented for every type that implements 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`.
Expand Down
36 changes: 27 additions & 9 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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::<OnlyQuestionmark>("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)]
Expand Down Expand Up @@ -185,7 +204,8 @@ fn missing_option_value() {
assert!(e.status.is_err());
}

fn assert_help_string<T: FromArgs>(help_str: &str) {
fn assert_help_string<T: HelpMessage>(help_str: &str) {
assert_help_message::<T>(help_str);
match T::from_args(&["test_arg_0"], &["--help"]) {
Ok(_) => panic!("help was parsed as args"),
Err(e) => {
Expand All @@ -195,6 +215,10 @@ fn assert_help_string<T: FromArgs>(help_str: &str) {
}
}

fn assert_help_message<T: HelpMessage>(help_str: &str) {
assert_eq!(help_str, T::help_message(&["test_arg_0"]));
}

fn assert_output<T: FromArgs + Debug + PartialEq>(args: &[&str], expected: T) {
let t = T::from_args(&["cmd"], args).expect("failed to parse");
assert_eq!(t, expected);
Expand Down Expand Up @@ -317,13 +341,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::<WithOption>(
&[],
Expand Down