Skip to content

Commit

Permalink
Tweaked output for default subcommands.
Browse files Browse the repository at this point in the history
Addressed various PR feedback.
  • Loading branch information
qhua948 committed Jun 18, 2022
1 parent 1e52d3b commit d4f6952
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 90 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ struct SubCommandTwo {
}
```

Is it possible to specify a optional subcommand, just like specifing
It is possible to specify a default subcommand, just like specifying
a default for an optional argument.

```rust
Expand Down
94 changes: 74 additions & 20 deletions argh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,18 @@ pub trait FromArgs: Sized {
/// },
/// );
/// ```
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>;
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit> {
Self::from_args_show_command_usage(command_name, args, true)
}

/// Just like `from_args`, but with an additional parameter to specify whether or not
/// to print the first line of command_usage.
/// Internal use only.
fn from_args_show_command_usage(
command_name: &[&str],
args: &[&str],
show_command_usage: bool,
) -> Result<Self, EarlyExit>;

/// Get a String with just the argument names, e.g., options, flags, subcommands, etc, but
/// without the values of the options and arguments. This can be useful as a means to capture
Expand Down Expand Up @@ -432,6 +443,17 @@ pub trait FromArgs: Sized {
/// );
/// ```
fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result<Vec<String>, EarlyExit> {
Self::redact_arg_values_show_command_usage(_command_name, _args, true)
}

/// Just like `redact_arg_values`, but with an additional parameter to specify whether or not
/// to print the first line of command_usage.
/// Internal use only.
fn redact_arg_values_show_command_usage(
_command_name: &[&str],
_args: &[&str],
_show_command_usage: bool,
) -> Result<Vec<String>, EarlyExit> {
Ok(vec!["<<REDACTED>>".into()])
}
}
Expand Down Expand Up @@ -667,27 +689,23 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,];
/// `parse_positionals`: Helper to parse positional arguments.
/// `parse_subcommand`: Helper to parse a subcommand.
/// `help_func`: Generate a help message.
/// `show_command_usage`: The bool value `help_func` is called with.
#[doc(hidden)]
pub fn parse_struct_args(
pub fn parse_struct_args_show_command_usage(
cmd_name: &[&str],
args: &[&str],
mut parse_options: ParseStructOptions<'_>,
mut parse_positionals: ParseStructPositionals<'_>,
mut parse_subcommand: Option<ParseStructSubCommand<'_>>,
help_func: &dyn Fn() -> String,
help_func: &dyn Fn(bool) -> String,
show_command_usage: bool,
) -> Result<(), EarlyExit> {
let mut help = false;
let mut remaining_args = args;
let mut positional_index = 0;
let mut options_ended = false;
let (has_default_subcommand_parse, subcmd_str) = match parse_subcommand {
Some(ref subcmd) => {
if let Some(subcmd_str) = subcmd.default {
(true, subcmd_str)
} else {
(false, "")
}
}
Some(ParseStructSubCommand { default: Some(subcmd_str), .. }) => (true, subcmd_str),
_ => (false, ""),
};

Expand Down Expand Up @@ -721,7 +739,7 @@ pub fn parse_struct_args(
}

if let Some(ref mut parse_subcommand) = parse_subcommand {
if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args)? {
if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args, true)? {
// Unset `help`, since we handled it in the subcommand
help = false;
break 'parse_args;
Expand All @@ -740,23 +758,30 @@ pub fn parse_struct_args(
}

if help {
// If there is also a subcommand, we'd like to print the help for that subcommand as well.
// If there is also a default subcommand, we'd like to print the help for that subcommand as well.
if has_default_subcommand_parse {
let sub_parse = (&mut parse_subcommand.unwrap().parse_func)(
let mut unwrapped_parse_subcommand = parse_subcommand.unwrap();
// Do not print usage again for the subcommand.
let sub_parse = (&mut unwrapped_parse_subcommand.parse_func)(
&[cmd_name, &[subcmd_str]].concat(),
args,
false,
)
.expect_err("bad parse func");
Err(EarlyExit { output: help_func() + &"\n" + &sub_parse.output, status: Ok(()) })
let default_subcommand_help_msg = format!("When no subcommand is given, the command `{}` is used as the default. Allowing the additional:", unwrapped_parse_subcommand.default.unwrap());
Err(EarlyExit {
output: format!("{}\n{}\n\n{}", help_func(show_command_usage), default_subcommand_help_msg, &sub_parse.output),
status: Ok(()),
})
} else {
Err(EarlyExit { output: help_func(), status: Ok(()) })
Err(EarlyExit { output: help_func(show_command_usage), status: Ok(()) })
}
} else if let Some(ref mut parse_subcommand) = parse_subcommand {
// If we have a do have a potential subcommand and it not parsed.
// If we have a potential subcommand and it is not parsed.
if let Some(default_subcommand) = parse_subcommand.default {
// Pass the args again to the default args.
// TODO(qhua948, #130): make this better, pass args without re-parsing.
parse_subcommand.parse(help, cmd_name, default_subcommand, args).map(|_| ())
parse_subcommand.parse(help, cmd_name, default_subcommand, args, true).map(|_| ())
} else {
Ok(())
}
Expand All @@ -765,6 +790,34 @@ pub fn parse_struct_args(
}
}

/// This function implements argument parsing for structs.
///
/// `cmd_name`: The identifier for the current command.
/// `args`: The command line arguments.
/// `parse_options`: Helper to parse optional arguments.
/// `parse_positionals`: Helper to parse positional arguments.
/// `parse_subcommand`: Helper to parse a subcommand.
/// `help_func`: Generate a help message.
#[doc(hidden)]
pub fn parse_struct_args(
cmd_name: &[&str],
args: &[&str],
parse_options: ParseStructOptions<'_>,
parse_positionals: ParseStructPositionals<'_>,
parse_subcommand: Option<ParseStructSubCommand<'_>>,
help_func: &dyn Fn(bool) -> String,
) -> Result<(), EarlyExit> {
parse_struct_args_show_command_usage(
cmd_name,
args,
parse_options,
parse_positionals,
parse_subcommand,
help_func,
true,
)
}

#[doc(hidden)]
pub struct ParseStructOptions<'a> {
/// A mapping from option string literals to the entry
Expand Down Expand Up @@ -887,9 +940,9 @@ pub struct ParseStructSubCommand<'a> {
pub subcommands: &'static [&'static CommandInfo],

// The function to parse the subcommand arguments.
pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
pub parse_func: &'a mut dyn FnMut(&[&str], &[&str], bool) -> Result<(), EarlyExit>,

// The default subcommand as a literal string, which should matches one of the subcommand's "name" attribute.
// The default subcommand as a literal string, which should match one of the subcommand's "name" attribute.
pub default: Option<&'static str>,
}

Expand All @@ -900,6 +953,7 @@ impl<'a> ParseStructSubCommand<'a> {
cmd_name: &[&str],
arg: &str,
remaining_args: &[&str],
show_command_usage: bool,
) -> Result<bool, EarlyExit> {
for subcommand in self.subcommands {
if subcommand.name == arg {
Expand All @@ -913,7 +967,7 @@ impl<'a> ParseStructSubCommand<'a> {
remaining_args
};

(self.parse_func)(&command, remaining_args)?;
(self.parse_func)(&command, remaining_args, show_command_usage)?;

return Ok(true);
}
Expand Down
89 changes: 88 additions & 1 deletion argh/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ Commands:
one First subcommand.
two Second subcommand.
Usage: cmdname one --x <x>
When no subcommand is given, the command `one` is used as the default. Allowing the additional:
First subcommand.
Expand Down Expand Up @@ -419,6 +419,92 @@ fn option_subcommand_nested_multiple() {
);
}

#[test]
fn option_subcommand_nested_multiple_show_help_both() {
#[derive(FromArgs, PartialEq, Debug)]
/// Top-level command.
struct TopLevel {
#[argh(subcommand, default = "my")]
nested: Option<MySubCommandStruct>,
}

#[derive(FromArgs, PartialEq, Debug)]
// Nested command.
#[argh(subcommand, name = "my", description = "My Subcmd")]
struct MySubCommandStruct {
#[argh(subcommand, default = "one")]
/// nested command.
nested_again: Option<MySubCommandEnum>,

#[argh(option, default = "69", description = "delta")]
/// the delta
delta: i32,
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum MySubCommandEnum {
One(SubCommandOne),
Two(SubCommandTwo),
}

#[derive(FromArgs, PartialEq, Debug)]
/// First subcommand.
#[argh(subcommand, name = "one")]
struct SubCommandOne {
#[argh(option, default = "1")]
/// how many x
x: usize,
}

#[derive(FromArgs, PartialEq, Debug)]
/// Second subcommand.
#[argh(subcommand, name = "two")]
struct SubCommandTwo {
#[argh(switch)]
/// whether to fooey
fooey: bool,
}
let help_str = r###"Usage: cmdname [<command>] [<args>]
Top-level command.
Options:
--help display usage information
Commands:
my My Subcmd
When no subcommand is given, the command `my` is used as the default. Allowing the additional:
My Subcmd
Options:
--delta delta
--help display usage information
Commands:
one First subcommand.
two Second subcommand.
When no subcommand is given, the command `one` is used as the default. Allowing the additional:
First subcommand.
Options:
--x how many x
--help display usage information
"###;

match TopLevel::from_args(&["cmdname"], &["--help"]) {
Ok(_) => panic!("help was parsed as args"),
Err(e) => {
assert_eq!(help_str, e.output);
e.status.expect("help returned an error");
}
}
}

#[test]
fn option_subcommand_with_default_and_args() {
#[derive(FromArgs, PartialEq, Debug)]
Expand Down Expand Up @@ -1127,6 +1213,7 @@ Options:
e.status.expect_err("should be an error");
}

// TODO(qhua948): undocumented feature of format! templating for {command_name}
#[derive(FromArgs, PartialEq, Debug)]
#[argh(
description = "Destroy the contents of <file>.",
Expand Down
47 changes: 32 additions & 15 deletions argh_derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,36 @@ pub(crate) fn help(
fields: &[StructField<'_>],
subcommand: Option<&StructField<'_>>,
) -> TokenStream {
let mut format_lit = "Usage: {command_name}".to_string();
let mut usage_section = "Usage: {command_name}".to_string();

let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional);
let mut has_positional = false;
for arg in positional.clone() {
has_positional = true;
format_lit.push(' ');
positional_usage(&mut format_lit, arg);
usage_section.push(' ');
positional_usage(&mut usage_section, arg);
}

let options = fields.iter().filter(|f| f.long_name.is_some());
for option in options.clone() {
format_lit.push(' ');
option_usage(&mut format_lit, option);
usage_section.push(' ');
option_usage(&mut usage_section, option);
}

if let Some(subcommand) = subcommand {
format_lit.push(' ');
usage_section.push(' ');
if !subcommand.optionality.is_required() {
format_lit.push('[');
usage_section.push('[');
}
format_lit.push_str("<command>");
usage_section.push_str("<command>");
if !subcommand.optionality.is_required() {
format_lit.push(']');
usage_section.push(']');
}
format_lit.push_str(" [<args>]");
usage_section.push_str(" [<args>]");
}

format_lit.push_str(SECTION_SEPARATOR);
usage_section.push_str(SECTION_SEPARATOR);
let mut format_lit = "".to_string();

let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type");
format_lit.push_str(&description);
Expand Down Expand Up @@ -108,10 +109,26 @@ pub(crate) fn help(

format_lit.push('\n');

quote! { {
#subcommand_calculation
format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg)
} }
let has_format_template_command_name = format_lit.contains("{command_name}");
let command_name_format_arg = quote! { command_name = #cmd_name_str_array_ident.join(" ") };
let together = format!("{}{}", usage_section, format_lit);

// We also allow user to template a `{command_name}` in their Notes/Examples.
let second_section_format_tokens = if has_format_template_command_name {
quote! { format!(#format_lit, #command_name_format_arg, #subcommand_format_arg) }
} else {
quote! { format!(#format_lit, #subcommand_format_arg) }
};

quote! { & |show_command_usage: bool| {
#subcommand_calculation
if show_command_usage{
format!(#together, #command_name_format_arg, #subcommand_format_arg)
} else {
#second_section_format_tokens
}
}
}
}

/// A section composed of exactly just the literals provided to the program.
Expand Down

0 comments on commit d4f6952

Please sign in to comment.