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

Derive Subcommands #15

Open
bheylin opened this issue Jun 14, 2023 · 3 comments
Open

Derive Subcommands #15

bheylin opened this issue Jun 14, 2023 · 3 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@bheylin
Copy link

bheylin commented Jun 14, 2023

Is there a plan to add subcommands to the derive macro?

@parasyte
Copy link
Owner

parasyte commented Jun 18, 2023

I have thought about adding support for deriving the trait for enums, which would allow a limited form of subcommand parsing. The reason it hasn't been done is because there are some ambiguities to resolve between subcommands and positional arguments. Consider a naive example:

#[derive(Debug, OnlyArgs)]
struct Args {
    /// Some subcommand.
    cmd: SubCommand,

    /// Put everything else into a vector.
    comment: Vec<String>,
}

#[derive(Debug, OnlyArgs)]
enum SubCommand {
    /// Addition.
    Add(i32, i32),

    /// Subtraction.
    Sub(i32, i32),
}

Passing "unknown" or "non-argument" strings to the CLI should push the strings to the comment field, and it currently doesn't matter where these positional arguments are placed:

$ ./cli this is a comment add 2 2
Args {
    cmd: SubCommand::Add(2, 2),
    comment: ["this", "is", "a", "comment"],
}

$ ./cli add 1 3 this is also a comment
Args {
    cmd: SubCommand::Add(1, 3),
    comment: ["this", "is", "also", "a", "comment"],
}

$ ./cli this will add the numbers add 2 2
Error: `add` expects `(i32, i32)`

Using a hand-written parser doesn't have this problem because the parsing rules are explicitly encoded in the [hand-written] trait implementation. The author decides how to resolve the ambiguity, possibly with a fallback mechanism to push unparseable strings into the comment field with back-tracking as necessary. Or by requiring positional args to only appear at the end of the command line. Etc.

I don't think there is a good way to provide this kind of CLI with the macro because the "best" ambiguity resolution depends on what the application author wants. The most conservative solution is that the macro should just disallow ambiguous command lines by construction. For instance, declaring an enum and a Vec<T> (subcommand and positional args) together in the same command line parser would be a compile error.


I also have not thought about how to handle recursive types or structs with more than one enum field (which could themselves have ambiguous variant names between them). And so forth.

In short, it needs a good design to put this feature into the derive macro.

@parasyte parasyte added enhancement New feature or request help wanted Extra attention is needed labels Jun 18, 2023
@bheylin
Copy link
Author

bheylin commented Jun 18, 2023

Yea I've had a think about how to solve the issue too.

FWIW Clap compiles fine, but complains at runtime about an unexpected argument:

use clap::Parser;

#[derive(clap::Parser, Debug, Clone)]
#[command(name = "test_cmd")]
pub struct Args {
    #[command(subcommand)]
    pub cmd: SubCommand,

    pub comment: Vec<String>,
}

#[derive(Clone, Debug, clap::Subcommand)]
#[command(about = "A subcommand")]
pub enum SubCommand {
    Add { opa: i32, opb: i32 },

    Sub { opa: i32, opb: i32 },
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    let result = match args.cmd {
        SubCommand::Add { opa, opb } => opa + opb,
        SubCommand::Sub { opa, opb } => opa - opb,
    };

    println!("result: {result}");
    println!("comment: {}", args.comment.join(", "));

    Ok(())
}
❯ cargo run --bin play -q -- sub 1 2 hello this is a comment
error: unexpected argument 'hello' found
                                                                                                                                                                            
Usage: play sub <OPA> <OPB>

@parasyte
Copy link
Owner

There was a recent blog post about difficulties with subcommands in clap, which is relevant to this issue: https://gribnau.dev/posts/puzzle-sharing-declarative-args-between-top-level-and-subcommand/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants