Default subcommand
Closed this issue ยท 21 comments
Feature request
This is a proposal to add a means to reduce verbosity of command-line calls.
The API presented here is App::default_subcommand()
method. It is used to show how this feature can be employed, so feel free to implement the functionality however you find suitable.
Description
Arguments of the default subcommand are merged into arguments of the enclosing app-like entity (subcommands can exploit this behavior regarding inner subcommands too).
Naming conflicts are resolved by preferring the arguments defined in the enclosing entity because otherwise they would be shadowed permanently. In other words, only merge the non-conflicting arguments into the scope, whilst preserving its own conflicting ones.
Inspecting the presence of these arguments is done unambiguously โ through the entity where they were defined.
Sample Code
extern crate clap;
use clap::{Arg, App, SubCommand};
fn main() {
let matches = App::new("test")
.subcommand(SubCommand::with_name("info")
.arg(Arg::with_name("verbose")
.short("v")
.long("verbose"))
.arg(Arg::with_name("PARAM")))
.subcommand(SubCommand::with_name("sync")
.arg(Arg::with_name("encrypt")
.short("e")
.long("encrypt")))
.default_subcommand("info") // new method
.get_matches();
if let Some(info) = matches.subcommand_matches("info") {
println!("{:?}", info.is_present("verbose"));
println!("{:?}", info.value_of("PARAM"));
}
if let Some(sync) = matches.subcommand_matches("sync") {
println!("{:?}", sync.is_present("encrypt"));
}
}
Expected Behavior Summary
$ ./clap-test # same as ./clap-test info
false
None
$ ./clap-test -v # same as ./clap-test info -v
true
None
$ ./clap-test foo # same as ./clap-test info foo
false
Some("foo")
$ ./clap-test sync -e # sync is not default, needs to be explicitly written
true
$ ./clap-test --help # help message is also affected
test
USAGE:
clap-test [SUBCOMMAND]
FLAGS:
-e, --encrypt
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s)
info
sync
This proposal can be extended to something like App::merge_subcommand()
and App::merge_subcommands()
methods which can be applied to many subcommands instead of one.
For this case, it should be noted that the order in which subcommands are merged matters. Otherwise, their descriptions are the same as the one for App::default_subcommand()
.
I think this is an interesting concept, but I'd be worried it could create confusion args from the "default" subcommand are intermixed with args from the parent subcommand such as program --parent --default --parent2
etc.
Using AppSettings::InferSubcommands
does almost the same thing, but with less confusion in my mind. As the verbosity is about as minimal as it gets where al these are the same as your examples:
$ ./clap-test i # same as ./clap-test info
false
None
$ ./clap-test i -v # same as ./clap-test info -v
true
None
$ ./clap-test i foo # same as ./clap-test info foo
false
Some("foo")
$ ./clap-test s -e # sync is not default, needs to be explicitly written
true
FWIW this feature would be useful to me, I'm porting and application from Python's click which supports this feature and InferSubcommands
doesn't quite cut it in my scenario as the default subcommand should be predetermined. In my scenario I am not concerned about confusion with the arguments of the parent command as I only have the default help/options there, though I appreciate in other scenarios this would be more confusing.
While AppSettings::InferSubcommands
is nice, where a default subcommand would really add value is when you are adding subcommands to an app that did not have any before and want existing scripts that call the app to still work.
The thing is default subcommand can be implemented in non clap code by simply running that subcommand logic when no subcommand is found. We do not want add complicated parsing logic when there is an easy and actually better work around.
@pksunkara I do not completely agree with your statement. A subcommand might have a complex set of arguments. It would be quite complicate to to re-implement the arg parsing and everything clap gives. Or is there an easy way to do so ?
To avoid misunderstanding, what I would like is for the user to avoid typing the subcommand's name but clap to parse the subcommand's fields.
The difference here is "default subcommand" (what I need) versus "default behavior" (what you suggested). I hope I correctly interpreted your message.
It would be quite complicate to to re-implement the arg parsing and everything clap gives
You don't need to parse it yourself. What you can do is abstract out that subcommand args and use them on both the subcommand and the main app.
Ok but then, how do you make these args mandatory when no subcommand is given but invalid with the wrong subcommand?
Example:
./app subcommand1 -arg1
is valid
./app -arg1
is valid because subcommand1
is implicit
./app subcommand2 -arg1
is invalid
./app -arg1 subcommand2
is invalid
where subcommand1
is the default one and -arg1
is specific to subcommand1
./app subcommand2 -arg1
I am not sure if you have even tried clap, but it is already invalid.
./app -arg1 subcommand2
is invalid
This might be a good argument for needing default subcommand.
But an earlier point raised by Kevin still stands:
I'd be worried it could create confusion args from the "default" subcommand are intermixed with args from the parent subcommand such as
program --parent --default --parent2
etc
For posterity,
you can achieve this with clap 3.X by using a combination of Arg::global
and AppSettings::ArgsNegateSubcommands
.
For example:
#[derive(Parser)]
#[clap(author, version, about)]
#[clap(global_setting(AppSettings::ArgsNegateSubcommands))]
pub struct Arguments {
#[clap(short, long, global(true), parse(from_occurrences))]
/// Make the subcommand more talkative.
pub verbose: usize,
/// The sub-command to execute.
#[clap(subcommand)]
pub command: Option<Commands>,
#[clap(short, long)]
/// The language that the fenced code blocks must match to be included in the output.
pub language: Option<String>,
#[clap(short, long, requires("language"))]
/// Require fenced code blocks have a language to be included in the output.
pub required: bool,
}
This allows binary --language foo --empty
but not binary command --language bar
@misalcedo Correct me if I'm wrong, but I think I see what your example does and it doesn't address the original problem statement. What @hcpl is after is something where
binary --language foo # allowed
binary command1 --language foo # allowed, and is the same as the above
binary command2 --language foo # disallowed
I think your approach would disallow the second line.
@pksunkara You suggested "abstract out that subcommand args and use them on both the subcommand and the main app." I don't understand what you're describing. If you have time and it's not too much trouble, I wonder if you'd be able to explain more or write out a little example?
FYI the git cookbook entry includes support for git stash
which has a default subcommand of push
.
Thank you @epage. I have extracted out the minimal parts of the git cookbook entry to answer the original question in this issue:
#[derive(Debug, clap::Parser)]
#[command(args_conflicts_with_subcommands = true)] // part 1/3 for emulating "default subcommand"
pub struct Arguments {
#[clap(subcommand)]
pub command: Option<Commands>,
// part 2/3 for emulating "default subcommand"
#[clap(flatten)]
pub info: InfoArgs,
}
#[derive(Debug, clap::Subcommand)]
pub enum Commands {
Info(InfoArgs),
Sync,
}
#[derive(Debug, clap::Args)]
pub struct InfoArgs {
#[clap(long)]
pub verbose: bool,
}
pub fn main() {
let args: Arguments = clap::Parser::parse();
let command = args.command.unwrap_or(Commands::Info(args.info)); // part 3/3 for emulating "default subcommand"
println!("{command:?}");
}
The solution requires three parts:
- We have
Option<Commands>
in case a subcommand such Info is specified, and#[clap(flatten)] InfoArgs
in case a subcommand isn't specified and we therefore need to get the InfoArgs directly - We have
args_conflicts_with_subcommands = true
so that only one of the two paths above is taken. In particular, if the user does--verbose info --verbose
then the first one counts as an arg, which conflicts with subcommands, and hence doesn't allow the subcommand "info" - The code uses
.unwrap_or
to pick whichever of the two paths was picked.
It's not an ideal answer because the help text isn't quite right:
$ cargo run -- --help
Usage: fiddle [OPTIONS]
fiddle <COMMAND>
Commands:
info
sync
help Print this message or the help of the given subcommand(s)
Options:
--verbose
-h, --help Print help
In an ideal world, it would only document the "--verbose" flag if you did "cargo run -- info --help".
In an ideal world, it would only document the "--verbose" flag if you did "cargo run -- info --help".
I don't think this is universal though: I personally prefer what it currently does as it documents how it can be used without a command.
as it documents how it can be used without a command.
Strictly, both options document how it can be used, (1) the message "if no command then info is assumed" and (2) the current behavior.
The difference is with (2) the user has no indication what the difference in meaning is between "--verbose" and "info --verbose", or indeed whether there is a difference. (We the programmer know there isn't a difference). Nor does the (2) tell the user what happens when they run the binary on its own without specifying any flags or commands. And (2) gives the impression that options are allowed with the command, while in fact they're not.
One option is to put the options under a custom help heading so it says "info options" or something like that
@epage Could you clarify what you mean, please? When I try to add a custom help heading then it gives a build-time message "error: methods are not allowed for flattened entry".
#[clap(flatten, help_heading = Some("OPTIONS-1"))]
pub info: InfoArgs,
We do not yet support help_heading
with flatten
. It is one of the last things left in #1807.
iirc you can set it on the struct
. If that doesn't work, then on each field.
iirc you can set it on the struct. If that doesn't work, then on each field.
Setting on the struct gives a build-time error "no method named help_heading
found for struct clap::Command
in the current scope"
Setting on each field doesn't give the desired effect because, even though it achieves the desired affect that "mybinary --help" shows the options for the info subcommand in a separate heading, "mybinary info --help" also shows them in a separate heading. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=fb338e3037a1a00e96e4c3d3ed82b530
I guess I'll wait for #1807. Thank you @epage for your continued assistance - much appreciated.
I visited this issue earlier but I could seem to get a fix here's what I did. I hope it helps someone in the future
Here's the root of my application
// mount clap parser here
#[derive(Parser)]
#[command(author, version, about ="Compilation of utility scripts for everyday use", long_about = None)]
#[command(propagate_version = true)]
pub struct Utils {
#[command(subcommand)]
pub command: Commands,
}
impl Utils {
pub async fn run() {
let utils = Utils::parse();
match utils.command {
Commands::Ignore(git_ignore) => git_ignore.parse(),
Commands::Mailto(email) => email.parse().await,
Commands::Readme(readme) => readme.parse(),
Commands::Store(store) => store.parse().await,
// _ => PrintColoredText::error("invalid command"),
}
}
}
#[derive(Subcommand)]
pub enum Commands {
/// store data as key value pair
Store(StoreCommands),
/// generate .gitignore
Ignore(GitIgnoreCommands),
/// send email from the command line
Mailto(EmailCommands),
/// add readme to a git software project
Readme(ReadmeCommands),
}
My help script look like this
Compilation of utility scripts for everyday use
Usage: utils <COMMAND>
Commands:
store store data as key value pair
ignore generate .gitignore
mailto send email from the command line
readme add a readme to a git software project
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
I wanted to implement a default subcommand for the store subcommand such that I can say
- `utils store key value" to store a key-value pair, this would be the default subcommand
utils store list
to list the stored key-value pair
To solve this,
- I implemented the fields as optional properties (argument and sub commands) thus
#[derive(Args, Debug, Serialize, Deserialize)]
pub struct StoreCommands {
#[clap(short, long, value_parser)]
pub key: Option<String>,
#[clap(short, long, value_parser)]
pub value: Option<String>,
#[command(subcommand)]
pub subcommands: Option<SubCommands>,
}
- I relied heavily on correct pattern-matching
pub async fn parse(&self) {
if let Some(command) = &self.subcommands {
match command {
SubCommands::List => Self::list().await,
SubCommands::Delete { key } => Self::delete(key).await,
SubCommands::Clear => Self::clear().await,
SubCommands::Update { key, value } => Self::update(key, value).await,
}
} else {
let Some(key) = &self.key else {
PrintColoredText::error("Invalid key");
return;
};
let Some(value) = &self.value else {
PrintColoredText::error("Invalid value");
return;
};
Store::new(key, value).save().await.unwrap();
let message = format!("{key} successfully stored");
PrintColoredText::success(&message);
}
}
The entirety of the source is as follows
use clap::{Args, Subcommand};
use serde::{Deserialize, Serialize};
use crate::{database::Store, style::PrintColoredText};
#[derive(Args, Debug, Serialize, Deserialize)]
pub struct StoreCommands {
#[clap(short, long, value_parser)]
pub key: Option<String>,
#[clap(short, long, value_parser)]
pub value: Option<String>,
#[command(subcommand)]
pub subcommands: Option<SubCommands>,
}
#[derive(Debug, Subcommand, Serialize, Deserialize)]
pub enum SubCommands {
/// list the stored data
List,
/// delete a key
Delete { key: String },
/// clear all stored data
Clear,
/// update the value of a key
Update { key: String, value: String },
}
impl StoreCommands {
pub async fn parse(&self) {
if let Some(command) = &self.subcommands {
match command {
SubCommands::List => Self::list().await,
SubCommands::Delete { key } => Self::delete(key).await,
SubCommands::Clear => Self::clear().await,
SubCommands::Update { key, value } => Self::update(key, value).await,
}
} else {
let Some(key) = &self.key else {
PrintColoredText::error("Invalid key");
return;
};
let Some(value) = &self.value else {
PrintColoredText::error("Invalid value");
return;
};
Store::new(key, value).save().await.unwrap();
let message = format!("{key} successfully stored");
PrintColoredText::success(&message);
}
}
async fn list() {
let data = crate::database::Store::find().await;
if data.is_empty() {
PrintColoredText::error("no data found");
return;
}
let data = crate::database::Database(data);
println!("{}", data);
}
async fn delete(key: &str) {
crate::database::Store::remove(key).await;
}
async fn update(key: &str, value: &str) {
let _ = crate::database::Store::update(key, value).await.ok();
let message = format!("{key} successfully updated");
PrintColoredText::success(&message);
}
async fn clear() {
crate::database::Store::clear().await;
}
}
I hope this helps someone. the project source code is available at https://github.com/opeolluwa/utils
While it's possible to mark the subcommand as optional and use pattern matching to use None
as the default command, I didn't like that it pushes that responsibility outside of the Cli.
A solution I've found was to keep the command
field private, and use a public method on Cli
to return it (or a default value). That way the implementation can all live inside the cli:
#[derive(Subcommand, Clone, Debug)]
pub enum Command {
Compile,
Format
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
impl Cli {
pub fn command(&self) -> Command {
self.command.clone().unwrap_or(Command::Compile)
}
}