parasyte / onlyargs

Only argument parsing! Nothing more.
MIT License
13 stars 2 forks source link

Derive Subcommands #15

Open bheylin opened 1 year ago

bheylin commented 1 year ago

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

parasyte commented 1 year ago

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.

bheylin commented 1 year ago

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 commented 3 months ago

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/