Skip to content

Commit

Permalink
Improve Options usage reporting
Browse files Browse the repository at this point in the history
* Adds methods `self_usage` and `self_command_list` to `Options`,
  returning usage or command list for an instance subcommand.
  • Loading branch information
murarth committed Feb 21, 2020
1 parent f314431 commit f427481
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 27 deletions.
83 changes: 83 additions & 0 deletions gumdrop_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum)

let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

let command_impl = {
let name = repeat(name);

quote!{
match self {
#( #name::#variant(cmd) => ::gumdrop::Options::command(cmd), )*
}
}
};

let command_name_impl = {
let name = repeat(name);

Expand All @@ -192,6 +202,26 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum)
}
};

let self_usage_impl = {
let name = repeat(name);

quote!{
match self {
#( #name::#variant(sub) => ::gumdrop::Options::self_usage(sub), )*
}
}
};

let self_command_list_impl = {
let name = repeat(name);

quote!{
match self {
#( #name::#variant(sub) => ::gumdrop::Options::self_command_list(sub), )*
}
}
};

Ok(quote!{
impl #impl_generics ::gumdrop::Options for #name #ty_generics #where_clause {
fn parse<__S: ::std::convert::AsRef<str>>(
Expand All @@ -203,6 +233,10 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum)
Self::parse_command(_arg, _parser)
}

fn command(&self) -> ::std::option::Option<&dyn ::gumdrop::Options> {
#command_impl
}

fn command_name(&self) -> ::std::option::Option<&'static str> {
#command_name_impl
}
Expand All @@ -229,10 +263,18 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum)
#usage
}

fn self_usage(&self) -> &'static str {
#self_usage_impl
}

fn command_list() -> ::std::option::Option<&'static str> {
::std::option::Option::Some(<Self as ::gumdrop::Options>::usage())
}

fn self_command_list(&self) -> ::std::option::Option<&'static str> {
#self_command_list_impl
}

fn command_usage(name: &str) -> ::std::option::Option<&'static str> {
match name {
#( #command => ::std::option::Option::Some(
Expand Down Expand Up @@ -540,6 +582,15 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields)
}
};

let command_impl = match &command {
None => quote!{ ::std::option::Option::None },
Some(field) => quote!{
::std::option::Option::map(
::std::option::Option::as_ref(&self.#field),
|sub| sub as _)
}
};

let command_name_impl = match &command {
None => quote!{ ::std::option::Option::None },
Some(field) => quote!{
Expand Down Expand Up @@ -584,6 +635,26 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields)
}
};

let self_usage_impl = match &command {
None => quote!{ <Self as ::gumdrop::Options>::usage() },
Some(field) => quote!{
::std::option::Option::map_or_else(
::std::option::Option::as_ref(&self.#field),
<Self as ::gumdrop::Options>::usage,
::gumdrop::Options::self_usage)
}
};

let self_command_list_impl = match &command {
None => quote!{ <Self as ::gumdrop::Options>::command_list() },
Some(field) => quote!{
::std::option::Option::map_or_else(
::std::option::Option::as_ref(&self.#field),
<Self as ::gumdrop::Options>::command_list,
::gumdrop::Options::self_command_list)
}
};

let required = &required;

let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Expand Down Expand Up @@ -626,6 +697,10 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields)
::std::result::Result::Ok(_result)
}

fn command(&self) -> ::std::option::Option<&dyn ::gumdrop::Options> {
#command_impl
}

fn command_name(&self) -> ::std::option::Option<&'static str> {
#command_name_impl
}
Expand All @@ -643,13 +718,21 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields)
#usage
}

fn self_usage(&self) -> &'static str {
#self_usage_impl
}

fn command_list() -> ::std::option::Option<&'static str> {
#command_list
}

fn command_usage(_name: &str) -> ::std::option::Option<&'static str> {
#command_usage
}

fn self_command_list(&self) -> ::std::option::Option<&'static str> {
#self_command_list_impl
}
}
})
}
Expand Down
85 changes: 58 additions & 27 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,17 @@ pub enum Opt<'a> {
/// Implements a set of options parsed from command line arguments.
///
/// An implementation of this trait can be generated with `#[derive(Options)]`.
pub trait Options: Sized {
pub trait Options {
/// Parses arguments until the given parser is exhausted or until
/// an error is encountered.
fn parse<S: AsRef<str>>(parser: &mut Parser<S>) -> Result<Self, Error>;
fn parse<S: AsRef<str>>(parser: &mut Parser<S>) -> Result<Self, Error> where Self: Sized;

/// Returns the subcommand instance, if present.
///
/// This method **must never** return `self` or otherwise return a `&dyn Options` instance
/// which would create a cycle. Doing so may cause other methods or `gumdrop` functions
/// to loop infinitely or overflow the runtime stack.
fn command(&self) -> Option<&dyn Options>;

/// Returns the name of a parsed command, if present.
///
Expand All @@ -248,7 +255,8 @@ pub trait Options: Sized {
/// Parses arguments received from the command line.
///
/// The first argument (the program name) should be omitted.
fn parse_args<S: AsRef<str>>(args: &[S], style: ParsingStyle) -> Result<Self, Error> {
fn parse_args<S: AsRef<str>>(args: &[S], style: ParsingStyle) -> Result<Self, Error>
where Self: Sized {
Self::parse(&mut Parser::new(args, style))
}

Expand All @@ -261,7 +269,7 @@ pub trait Options: Sized {
/// `stdout` and the process will exit with status code `0`.
///
/// Otherwise, the parsed options are returned.
fn parse_args_or_exit(style: ParsingStyle) -> Self {
fn parse_args_or_exit(style: ParsingStyle) -> Self where Self: Sized {
use std::env::args;
use std::process::exit;

Expand All @@ -273,27 +281,32 @@ pub trait Options: Sized {
});

if opts.help_requested() {
match opts.command_name() {
None => {
println!("Usage: {} [OPTIONS]", args[0]);
println!();
println!("{}", Self::usage());

if let Some(cmds) = Self::command_list() {
println!();
println!("Available commands:");
println!();
println!("{}", cmds);
let mut command = &opts as &dyn Options;
let mut command_str = String::new();

loop {
if let Some(new_command) = command.command() {
command = new_command;

if let Some(name) = new_command.command_name() {
command_str.push(' ');
command_str.push_str(name);
}
} else {
break;
}
Some(cmd) => {
let help = Self::command_usage(cmd).unwrap_or_default();
}

println!("Usage: {} {} [OPTIONS]", args[0], cmd);
println!();
println!("{}", help);
}
println!("Usage: {}{} [OPTIONS]", args[0], command_str);
println!();
println!("{}", command.self_usage());

if let Some(cmds) = command.self_command_list() {
println!();
println!("Available commands:");
println!("{}", cmds);
}

exit(0);
}

Expand All @@ -309,34 +322,43 @@ pub trait Options: Sized {
/// `stdout` and the process will exit with status code `0`.
///
/// Otherwise, the parsed options are returned.
fn parse_args_default_or_exit() -> Self {
fn parse_args_default_or_exit() -> Self where Self: Sized {
Self::parse_args_or_exit(ParsingStyle::default())
}

/// Parses arguments received from the command line,
/// using the default parsing style.
///
/// The first argument (the program name) should be omitted.
fn parse_args_default<S: AsRef<str>>(args: &[S]) -> Result<Self, Error> {
fn parse_args_default<S: AsRef<str>>(args: &[S]) -> Result<Self, Error> where Self: Sized {
Self::parse(&mut Parser::new(args, ParsingStyle::default()))
}

/// Parses options for the named command.
fn parse_command<S: AsRef<str>>(name: &str, parser: &mut Parser<S>) -> Result<Self, Error>;
fn parse_command<S: AsRef<str>>(name: &str, parser: &mut Parser<S>) -> Result<Self, Error> where Self: Sized;

/// Returns a string showing usage and help for each supported option.
///
/// Option descriptions are separated by newlines. The returned string
/// should **not** end with a newline.
fn usage() -> &'static str;
fn usage() -> &'static str where Self: Sized;

/// Returns a string showing usage and help for this options instance.
///
/// In contrast to `usage`, this method will return usage for a subcommand,
/// if one is selected.
///
/// Option descriptions are separated by newlines. The returned string
/// should **not** end with a newline.
fn self_usage(&self) -> &'static str;

/// Returns a usage string for the named command.
///
/// If the named command does not exist, `None` is returned.
///
/// Command descriptions are separated by newlines. The returned string
/// should **not** end with a newline.
fn command_usage(command: &str) -> Option<&'static str>;
fn command_usage(command: &str) -> Option<&'static str> where Self: Sized;

/// Returns a string listing available commands and help text.
///
Expand All @@ -347,7 +369,16 @@ pub trait Options: Sized {
///
/// For `struct` types containing a field marked `#[options(command)]`,
/// `usage` is called on the command type.
fn command_list() -> Option<&'static str>;
fn command_list() -> Option<&'static str> where Self: Sized;

/// Returns a listing of available commands and help text.
///
/// In contrast to `usage`, this method will return command list for a subcommand,
/// if one is selected.
///
/// Commands are separated by newlines. The string should **not** end with
/// a newline.
fn self_command_list(&self) -> Option<&'static str>;
}

/// Controls behavior of free arguments in `Parser`
Expand Down
79 changes: 79 additions & 0 deletions tests/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,85 @@ fn test_command() {
"unrecognized command `baz`");
}

#[test]
fn test_nested_command() {
#[derive(Debug, Options)]
struct Main {
#[options(help = "main help")]
help: bool,

#[options(command)]
command: Option<Command>,
}

#[derive(Debug, Options)]
enum Command {
#[options(help = "alpha help")]
Alpha(Alpha),
#[options(help = "bravo help")]
Bravo(Bravo),
}

#[derive(Debug, Options)]
struct Alpha {
#[options(help = "alpha command help")]
help: bool,

#[options(command)]
command: Option<AlphaCommand>,
}

#[derive(Debug, Options)]
struct Bravo {
#[options(help = "bravo command help")]
help: bool,

#[options(help = "bravo option help")]
option: u32,
}

#[derive(Debug, Options)]
enum AlphaCommand {
#[options(help = "alpha foo help")]
Foo(Foo),
#[options(help = "alpha bar help")]
Bar(Bar),
}

#[derive(Debug, Options)]
struct Foo {
#[options(help = "alpha foo command help")]
help: bool,

#[options(help = "alpha foo beep help")]
beep: u32,
}

#[derive(Debug, Options)]
struct Bar {
#[options(help = "alpha bar command help")]
help: bool,

#[options(help = "alpha bar boop help")]
boop: u32,
}

let opts = Main::parse_args_default(&["-h"]).unwrap();
assert_eq!(opts.self_usage(), Main::usage());

let opts = Main::parse_args_default(&["-h", "alpha"]).unwrap();
assert_eq!(opts.self_usage(), Alpha::usage());

let opts = Main::parse_args_default(&["-h", "bravo"]).unwrap();
assert_eq!(opts.self_usage(), Bravo::usage());

let opts = Main::parse_args_default(&["-h", "alpha", "foo"]).unwrap();
assert_eq!(opts.self_usage(), Foo::usage());

let opts = Main::parse_args_default(&["-h", "alpha", "bar"]).unwrap();
assert_eq!(opts.self_usage(), Bar::usage());
}

#[test]
fn test_command_name() {
#[derive(Options)]
Expand Down

0 comments on commit f427481

Please sign in to comment.