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

Built-in default subcommand support #4442

Closed
2 tasks done
9999years opened this issue Nov 2, 2022 · 4 comments
Closed
2 tasks done

Built-in default subcommand support #4442

9999years opened this issue Nov 2, 2022 · 4 comments
Labels
C-enhancement Category: Raise on the bar on expectations S-duplicate Status: Closed as Duplicate

Comments

@9999years
Copy link
Contributor

9999years commented Nov 2, 2022

Please complete the following tasks

Clap Version

4.0.18

Describe your use case

I have a command which supports subcommands for different operations, but also has a sensible 'default' operation which I'd like to run if a subcommand is omitted. Ideally, I'd like to be able to specify arguments for the default command as well.

Describe the solution you'd like

I think my ideal solution looks something like this, with a command attribute like #[command(default)] to declare the default variant to use for parsing:

#[derive(Debug, Clone, Parser)]
pub struct Opts {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Clone, clap::Subcommand)]
pub enum Command {
    /// This will be run for `my-exe my-default-command` and also `my-exe`.
    #[command(default)]
    MyDefaultCommand,

    /// This will be run for `my-exe other-command`.
    OtherCommand,
}

Additional context

Following the directions in discussion #4134, I produced the following solution:

/// Produce this value with `ParserOpts::parse().into()`. This abstracts over
/// the internal parser to always provide a value for the `command` field.
#[derive(Debug, Clone)]
pub struct Opts {
    pub common: CommonOpts,
    pub command: Command,
}

impl From<ParserOpts> for Opts {
    fn from(opts: ParserOpts) -> Self {
        Self {
            common: opts.common,
            command: opts.command.unwrap_or(Command::DefaultCommand(opts.default_command)),
        }
    }
}

/// Here is the actual parser struct.
#[derive(Debug, Clone, clap::Parser)]
#[command(args_conflicts_with_subcommands = true)]
pub struct ParserOpts {
    /// Options common to all subcommands.
    #[command(flatten)]
    pub common: CommonOpts,

    /// An optional subcommand.
    #[command(subcommand)]
    pub command: Option<Command>,

    /// Arguments for the default subcommand.
    #[command(flatten)]
    pub default_command: DefaultOpts,
}

/// Options valid for any subcommand.
#[derive(Debug, Clone, clap::Args)]
pub struct CommonOpts {
    #[arg(long, default_value = "info")]
    pub tracing_filter: String,
}

/// A subcommand.
#[derive(Debug, Clone, clap::Subcommand)]
pub enum Command {
    DefaultCommand(DefaultOpts),

    OtherCommand,
}

/// Options for the default subcommand.
#[derive(Debug, Clone, clap::Args)]
pub struct DefaultOpts {
    #[arg(long)]
    pub default_arg: Option<String>,
}

This workaround has the following deficiencies, most of which I suspect relate to the necessary args_conflicts_with_subcommands call:

  1. The --help text prints like this:

    Usage: my-exe [OPTIONS]
           my-exe <COMMAND>
    

    I would prefer Usage: my-exe [OPTIONS] [COMMAND] to reflect that the options can always be given and that the command is optional.

  2. my-exe default-command --help doesn't display the common options, even though they (should be) valid.

  3. Arguments before the subcommand fail to parse:

    $ my-exe --tracing-filter=trace default-command --default-arg foo
    error: The subcommand 'default-command' wasn't recognized
    
      Did you mean 'default-command'?
    
      If you believe you received this message in error, try re-running with 'my-exe -- default-command'
    
    Usage: my-exe [OPTIONS]
           my-exe <COMMAND>
    
    For more information try '--help'

    Note the weird message that suggests the command we typed. Following the suggestion to add -- before the subcommand name gives the same message.

  4. Arguments common to all subcommands can't be mixed before/after the subcommand name.

    my-exe default-command --tracing-filter=trace gives an error saying that --tracing-filter is not expected/invalid in this context.

  5. It's unwieldy. Defining a wrapper type, using flatten like this, and needing to convert between types or unwrap_or fields manually all feels rather janky.

@9999years 9999years added the C-enhancement Category: Raise on the bar on expectations label Nov 2, 2022
@epage epage changed the title Tracking issue: default subcommands Default subcommands support Nov 2, 2022
@epage
Copy link
Member

epage commented Nov 2, 2022

#4350 added support for Option when flattening; I'm not entirely sure how this relates but supposedly it helps a bit

This helps when there are required arguments. Instead of making them Option and explicitly setting required, you can just make the entire group only present when mentioned.

The --help text prints like this:

I would prefer Usage: my-exe [OPTIONS] [COMMAND] to reflect that the options can always be given and that the command is optional.

The usage that clap is generating is more accurate. It is saying you can specify a subcommand or specify the arguments directly.

my-exe default-command --help doesn't display the common options, even though they (should be) valid.

Arguments common to all subcommands can't be mixed before/after the subcommand name.

This seems unrelated to default subcommands. If they aren't defined on the subcommand, then they won't show up. Should these be marked #[arg(global = true)]?

Arguments before the subcommand fail to parse:

#3135 m might be relevant for this

Note the weird message that suggests the command we typed. Following the suggestion to add -- before the subcommand name gives the same message.

Feel free to create a separate issue for the error message

@epage epage changed the title Default subcommands support Built-in default subcommand support Nov 2, 2022
@epage
Copy link
Member

epage commented Nov 2, 2022

It's unwieldy. Defining a wrapper type, using flatten like this, and needing to convert between types or unwrap_or fields manually all feels rather janky.

For how uncommon default subcommands seem to be, the level of machinery seems to be a fair match. We are trying to balance with ease of use with binary size / build times. We do this by providing lower level building blocks that allow people to build what they need while providing an easy-to-use path for the common cases.

If there seems to be enough interest and someone creates a plan for this, then it can move forward. This is unlikely to be a focus area for maintainers.

In coming up with a plan, keep in mind that the derive API delegates all of the important machinery to the builder API, so we'll need a proposal that works at the builder level. It will need to analyze the different cases to make sure they work well with each other.

@epage
Copy link
Member

epage commented Nov 2, 2022

Closing in favor of #3857 so we keep the conversation in one place. We can re-open if there is a design and interst.

@epage epage closed this as not planned Won't fix, can't repro, duplicate, stale Nov 2, 2022
@epage epage added the S-duplicate Status: Closed as Duplicate label Nov 2, 2022
@9999years
Copy link
Contributor Author

Thanks, I didn't see #3857 when searching

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: Raise on the bar on expectations S-duplicate Status: Closed as Duplicate
Projects
None yet
Development

No branches or pull requests

2 participants