diff --git a/gumdrop_derive/src/lib.rs b/gumdrop_derive/src/lib.rs index 06c39b7..b187c98 100644 --- a/gumdrop_derive/src/lib.rs +++ b/gumdrop_derive/src/lib.rs @@ -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); @@ -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>( @@ -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 } @@ -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(::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( @@ -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!{ @@ -584,6 +635,26 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) } }; + let self_usage_impl = match &command { + None => quote!{ ::usage() }, + Some(field) => quote!{ + ::std::option::Option::map_or_else( + ::std::option::Option::as_ref(&self.#field), + ::usage, + ::gumdrop::Options::self_usage) + } + }; + + let self_command_list_impl = match &command { + None => quote!{ ::command_list() }, + Some(field) => quote!{ + ::std::option::Option::map_or_else( + ::std::option::Option::as_ref(&self.#field), + ::command_list, + ::gumdrop::Options::self_command_list) + } + }; + let required = &required; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); @@ -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 } @@ -643,6 +718,10 @@ 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 } @@ -650,6 +729,10 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) 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 + } } }) } diff --git a/src/lib.rs b/src/lib.rs index f784b5f..af7bb66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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>(parser: &mut Parser) -> Result; + fn parse>(parser: &mut Parser) -> Result 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. /// @@ -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>(args: &[S], style: ParsingStyle) -> Result { + fn parse_args>(args: &[S], style: ParsingStyle) -> Result + where Self: Sized { Self::parse(&mut Parser::new(args, style)) } @@ -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; @@ -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); } @@ -309,7 +322,7 @@ 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()) } @@ -317,18 +330,27 @@ pub trait Options: Sized { /// using the default parsing style. /// /// The first argument (the program name) should be omitted. - fn parse_args_default>(args: &[S]) -> Result { + fn parse_args_default>(args: &[S]) -> Result where Self: Sized { Self::parse(&mut Parser::new(args, ParsingStyle::default())) } /// Parses options for the named command. - fn parse_command>(name: &str, parser: &mut Parser) -> Result; + fn parse_command>(name: &str, parser: &mut Parser) -> Result 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. /// @@ -336,7 +358,7 @@ pub trait Options: Sized { /// /// 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. /// @@ -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` diff --git a/tests/options.rs b/tests/options.rs index b98812a..b27ada7 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -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, + } + + #[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, + } + + #[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)]