clap-rs/clap

Default subcommand

Closed this issue ยท 21 comments

hcpl commented

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    
hcpl commented

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.

SWHes commented

@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.

SWHes commented

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?

epage commented

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:

  1. 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
  2. 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"
  3. 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".

epage commented

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.

epage commented

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,

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=43b93b114af6ff20434544ef26deb702

epage commented

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,

  1. 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>,
}
  1. 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)
    }
}