Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default subcommand #975

Closed
hcpl opened this issue May 29, 2017 · 21 comments
Closed

Default subcommand #975

hcpl opened this issue May 29, 2017 · 21 comments

Comments

@hcpl
Copy link
Contributor

hcpl commented May 29, 2017

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
Copy link
Contributor Author

hcpl commented May 29, 2017

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().

@kbknapp
Copy link
Member

kbknapp commented May 30, 2017

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

@keirlawson
Copy link

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.

@timboudreau
Copy link

timboudreau commented May 5, 2021

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.

@pksunkara
Copy link
Member

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
Copy link

SWHes commented May 16, 2021

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

@pksunkara
Copy link
Member

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
Copy link

SWHes commented May 18, 2021

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

@pksunkara
Copy link
Member

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

@misalcedo
Copy link

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

@ljw1004
Copy link

ljw1004 commented Feb 10, 2023

@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
Copy link
Member

epage commented Feb 10, 2023

FYI the git cookbook entry includes support for git stash which has a default subcommand of push.

@ljw1004
Copy link

ljw1004 commented Feb 10, 2023

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
Copy link
Member

epage commented Feb 10, 2023

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.

@ljw1004
Copy link

ljw1004 commented Feb 10, 2023

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
Copy link
Member

epage commented Feb 11, 2023

One option is to put the options under a custom help heading so it says "info options" or something like that

@ljw1004
Copy link

ljw1004 commented Feb 19, 2023

@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
Copy link
Member

epage commented Feb 20, 2023

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.

@ljw1004
Copy link

ljw1004 commented Feb 20, 2023

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.

elasticdog added a commit to elasticdog/jj that referenced this issue Apr 14, 2023
This is a convenience optimization for the default user experience, since
`jj log` is a frequently run command. Accessing the help information
explicitly still follows normal CLI conventions, and instructions are displayed
appropriately if the user happens to make a mistake. Discoverability should not
be adversely harmed.

Since clap does not natively support setting a default subcommand [1], it will
only parse global options when invoking `jj` in this way. This limitation is
also why we have to create the LogArgs struct literal (with derived default
values) to pass through to the cmd_log function.

Note that this behavior (and limitation) mirrors what Sapling does [2], where
`sl` will display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 14, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Since clap does not natively support setting a default subcommand [1],
it will only parse global options when invoking `jj` in this way. This
limitation is also why we have to create the LogArgs struct literal
(with derived default values) to pass through to the cmd_log function.

Note that this behavior (and limitation) mirrors what Sapling does [2],
where `sl` will display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 14, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Since clap does not natively support setting a default subcommand [1],
it will only parse global options when invoking `jj` in this way. This
limitation is also why we have to create the LogArgs struct literal
(with derived default values) to pass through to the cmd_log function.

Note that this behavior (and limitation) mirrors what Sapling does [2],
where `sl` will display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 14, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Since clap does not natively support setting a default subcommand [1],
it will only parse global options when invoking `jj` in this way. This
limitation is also why we have to create the LogArgs struct literal
(with derived default values) to pass through to the cmd_log function.

Note that this behavior (and limitation) mirrors what Sapling does [2],
where `sl` will display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 15, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 16, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 16, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 16, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 17, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 17, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 17, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to elasticdog/jj that referenced this issue Apr 17, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
elasticdog added a commit to martinvonz/jj that referenced this issue Apr 17, 2023
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
@opeolluwa
Copy link

opeolluwa commented Sep 22, 2023

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

@MatheusRich
Copy link

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)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests