murarth / gumdrop

Rust option parser with custom derive support

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Deeper integration of `--help`, especially for printing subcommand help

Arnavion opened this issue · comments

I noticed the subcommands example talks about using a dedicated help subcommand, and it helps that the command_usage API is tailored for it. However I also want to support --help for every subcommand to do the same thing which unfortunately requires lots of verbose code:

Toggle
extern crate gumdrop;
#[macro_use] extern crate gumdrop_derive;

use std::io::Write;
use gumdrop::Options;

#[derive(Debug, Default, Options)]
struct MainOptions {
	#[options(help = "Print the help message")]
	help: bool,

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

#[derive(Debug, Options)]
enum SubCommand {
	#[options(help = "Does foo")]
	Foo(FooOptions),

	#[options(help = "Does bar")]
	Bar(BarOptions),
}

#[derive(Debug, Default, Options)]
struct FooOptions {
	#[options(help = "Print the help message")]
	help: bool,

	#[options(help = "The thing to foo")]
	thing: String,
}

#[derive(Debug, Default, Options)]
struct BarOptions {
	#[options(help = "Print the help message")]
	help: bool,

	#[options(help = "The thing to bar")]
	thing: String,
}

fn main() {
	let args: Vec<_> = std::env::args().collect();
	let options: MainOptions = gumdrop::parse_args_default(&args[1..]).unwrap_or_else(|err| {
		// Invalid args
		writeln!(std::io::stderr(), "{}", err);
		print_usage(std::io::stderr());
		std::process::exit(1);
	});

	if options.help {
		// Explicitly asked for help
		print_usage(std::io::stdout());
		return;
	}

	let subcommand = options.subcommand.unwrap_or_else(|| {
		// Did not provide subcommand (required)
		print_usage(std::io::stderr());
		std::process::exit(1);
	});

	let subcommand_usage = match subcommand {
		SubCommand::Foo(FooOptions { help: true, .. }) => Some(("foo", FooOptions::usage())),
		SubCommand::Bar(BarOptions { help: true, .. }) => Some(("bar", BarOptions::usage())),
		_ => None,
	};

	if let Some((subcommand_name, subcommand_usage)) = subcommand_usage {
		// Explicitly asked for subcommand help
		print_subcommand_usage(std::io::stdout(), subcommand_name, subcommand_usage);
		return;
	}

	// Run the subcommand
	match subcommand {
		SubCommand::Foo(FooOptions { thing, .. }) => println!("Foo the thing {}", thing),
		SubCommand::Bar(BarOptions { thing, .. }) => {
			if thing.is_empty() {
				writeln!(std::io::stderr(), "Invalid thing");
				print_subcommand_usage(std::io::stderr(), "bar", BarOptions::usage());
				std::process::exit(1);
			}
			println!("Bar the thing {}", thing)
		},
	}
}

fn print_usage<W: Write>(mut w: W) {
	writeln!(w, "{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
	writeln!(w, "{}", env!("CARGO_PKG_AUTHORS"));
	writeln!(w, "{}", env!("CARGO_PKG_DESCRIPTION"));
	writeln!(w);
	writeln!(w, "USAGE:");
	writeln!(w, "  {} <SUBCOMMAND>", env!("CARGO_PKG_NAME"));
	writeln!(w);
	writeln!(w, "FLAGS");
	writeln!(w, "{}", MainOptions::usage());
	writeln!(w);
	writeln!(w, "SUBCOMMANDS:");
	writeln!(w, "{}", SubCommand::usage());
}

fn print_subcommand_usage<W: Write>(mut w: W, subcommand_name: &'static str, subcommand_usage: &'static str) {
	writeln!(w, "USAGE:");
	writeln!(w, "  {} {} [ARGUMENTS]", env!("CARGO_PKG_NAME"), subcommand_name);
	writeln!(w);
	writeln!(w, "FLAGS:");
	writeln!(w, "{}", subcommand_usage);
}

The expectations are that:

  1. cargo run -- should print the top-level usage text, and exit with 1.

  2. cargo run -- --help should print the top-level usage text, and exit with 0.

  3. cargo run -- foo --help should print the usage text for the foo subcommand, and exit with 0.

  4. cargo run -- bar should get an Invalid thing error, print the usage text for the bar subcommand, and exit with 1.

  5. cargo run -- baz should get an unrecognized command error, print the top-level usage text, and exit with 1.

There are two bad things about this code:

  1. I have to see if the user asked for subcommand help by matching over every subcommand variant and extracting its help field.

  2. For printing the usage of a matched subcommand, I want to print the name of the matched subcommand, but I can't get it. Instead I have to repeat it myself (the "foo" in Some(("foo", FooOptions::usage()))). gumdrop already knows the subcommand name since it matched on it in SubCommand::parse_command, but it doesn't give a way to get it from the parsed value.

It would be nice if:

  1. the struct member holding the subcommand enum (MainOptions::subcommand in the above example) could also know the name of the subcommand it has matched. Perhaps gumdrop could see if it's Option<SubCommand> or Option<(SubCommand, &'static str)>, and additionally put the name in the latter case.

  2. there was some built-in handling for detecting the presence of --help and determining whose usage text should be printed (top-level or a subcommand). I'm not asking for something that actually prints the usage, ie, I'm not asking for a replacement for the print_usage and print_subcommand_usage functions. Rather I'm asking for a replacement for the match block that checks for help: true in every subcommand.

So all together, it could be used like this:

#[derive(Debug, Default, Options)]
struct MainOptions {
	#[options(help = "Print the help message")]
	help: bool,

	#[options(command)]
	subcommand: Option<(SubCommand, &'static str)>, // Tuple here
}

and

if options.help_requested() { // New function on gumdrop::Options
	// Explicitly asked for help
	print_usage(std::io::stdout());
	return;
}

let (subcommand, subcommand_name) = options.subcommand.unwrap_or_else(|| {
	// Did not provide subcommand (required)
	print_usage(std::io::stderr());
	std::process::exit(1);
});

if subcommand.help_requested() { // New function on gumdrop::Options
	// Explicitly asked for subcommand help
	print_subcommand_usage(std::io::stdout(), subcommand_name, subcommand.command_usage(subcommand_name).unwrap());
	return;
}

where the first fn help_requested(&self) -> bool function checks for self.help == true and the second fn help_requested(&self) -> bool function checks for help == true in any of the variants of the subcommand.

the struct member holding the subcommand enum (MainOptions::subcommand in the above example) could also know the name of the subcommand

I agree that the command name could be made available, but I don't particularly like the idea of using a tuple for the #[options(command)] field. It's completely counter-intuitive for a tuple to get that kind of special behavior.

I would prefer allowing another field to be added, say, #[options(command_name)] cmd_name: Option<&'static str>, which would hold the command name whenever a command has been selected. I think this would work just as well in your example above.

some built-in handling for detecting the presence of --help

I don't like idea of hard-coding special behavior for any option named help, which would be required in order to support this. I rather like that the generated code considers help to be just another dumb boolean flag.

The help_requested method does clean up the main option-handling routine, but it could be manually implemented as an inherent SubCommand method and I don't feel like the trouble of implementing it is sufficient to warrant implementing this in gumdrop.

I would prefer allowing another field to be added

Sure, that is fine too. I thought the tuple approach would be better to reduce the number of "special" fields, ie fields that don't actually correspond to CLI options.

I don't like idea of hard-coding special behavior for any option named help

But help is special - the only use of an option named --help is to provide usage text.

but it could be manually implemented as an inherent SubCommand method

It could, but implementing boilerplate for the user is the good thing about custom derives.

I've given this issue some more thought and I've come around to your position. The behavior isn't all that special and the implementation was simpler than I anticipated. It works well with the existing relationship between main Options and subcommand Options types.

I've pushed implementations of your suggestions (and edited the commands example to use the new method). If it all looks satisfactory to you, I'll go ahead and publish a new version on crates.io.

(nvm, forgot replace both crates in Cargo.toml)

Yes, I think this works well. Thanks!

And thank you for the issue. 0.3.0 of each crate is now published.