Search code examples
ruststructcommand-line-interfaceclap

How to create a custom derive macro for clap structs in rust?


I'm trying to create some sort of simple billing CLI application in rust for practice. It is kind a database application. I have many operations in which I need the user to be able to filter by the values of the columns which lines will be operated on.

For an example follows below have the delete and show commands:

#[derive(Debug, Args)]
struct DeleteCommand {
    /// The ID of the bill
    #[clap(short, long, value_parser)]
    id: Option<String>,
    /// The name of the bill
    #[clap(short, long, value_parser)]
    name: Option<String>,
    /// The value of the bill
    #[clap(short, long, value_parser)]
    value: Option<String>,
    /// The amount of the billB
    #[clap(short, long, value_parser)]
    amount: Option<String>,
    /// Datetime
    #[clap(short, long, value_parser)]
    datetime: Option<String>,
    /// Currency
    #[clap(short, long, value_parser)]
    currency: Option<String>,
    /// Recipient
    #[clap(short, long, value_parser)]
    recipient: Option<String>,
    /// Situation
    #[clap(short, long, value_parser)]
    situation: Option<String>,
    /// Hard
    #[clap(short = 'H', long)]
    hard: bool,
}

#[derive(Debug, Args)]
struct ShowCommand {
    #[clap(subcommand)]
    subcommand: ShowSubcommand,
    /// The ID of the bill
    #[clap(short, long, value_parser)]
    id: Option<String>,
    /// The name of the bill
    #[clap(short, long, value_parser)]
    name: Option<String>,
    /// The value of the bill
    #[clap(short, long, value_parser)]
    value: Option<String>,
    /// The amount of the bill
    #[clap(short, long, value_parser)]
    amount: Option<String>,
    /// Datetime
    #[clap(short, long, value_parser)]
    datetime: Option<String>,
    /// Currency
    #[clap(short, long, value_parser)]
    currency: Option<String>,
    /// Datetime
    #[clap(short, long, value_parser)]
    recipient: Option<String>,
    /// Datetime
    #[clap(short, long, value_parser)]
    situation: Option<String>,
    /// Head
    #[clap(long, value_parser)]
    head: Option<u32>,
    /// Tail
    #[clap(long, value_parser)]
    tail: Option<u32>,
    /// Order by
    #[clap(short, long, value_parser)]
    orderby: Option<String>,
}

As you can see there are many common fields between both structs since for both is needed to filter items in the database. I just want to create a derive custom macro that allow me to simply repeat these rows inside the structs without need to write it again and again:

    /// The ID of the bill
    #[clap(short, long, value_parser)]
    id: Option<String>,
    /// The name of the bill
    #[clap(short, long, value_parser)]
    name: Option<String>,
    /// The value of the bill
    #[clap(short, long, value_parser)]
    value: Option<String>,
    /// The amount of the bill
    #[clap(short, long, value_parser)]
    amount: Option<String>,
    /// Datetime
    #[clap(short, long, value_parser)]
    datetime: Option<String>,
    /// Currency
    #[clap(short, long, value_parser)]
    currency: Option<String>,
    /// Recipient
    #[clap(short, long, value_parser)]
    recipient: Option<String>,
    /// Situation
    #[clap(short, long, value_parser)]
    situation: Option<String>,

I need to repeat the /// lines as well since it will become the description of each parameter in the CLI.

MINIMAL REPRODUCIBLE EXAMPLE:

This is what i have

use clap::{Parser, Subcommand, Args};

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

#[derive(Debug, Subcommand)]
enum Command {
    /// A command description
    A(CommandA),
    /// B command description
    B(CommandB),
}

#[derive(Debug, Args)]
struct CommandA {
    /// The value of column x to be filtered in the database
    #[clap(short, long, value_parser)]
    x_value: Option<String>,
    /// The value of column y to be filtered in the database
    #[clap(short, long, value_parser)]
    y_value: Option<String>,
    /// Some particular parameter of this command
    #[clap(short, long, value_parser)]
    particular: Option<String>,
}

#[derive(Debug, Args)]
struct CommandB {
    /// The value of x to be filtered in the database
    #[clap(short, long, value_parser)]
    x_value: Option<String>,
    /// The value of y to be filtered in the database
    #[clap(short, long, value_parser)]
    y_value: Option<String>,
    /// Some particular parameter of this command
    #[clap(short, long, value_parser)]
    particular: Option<String>,
}

fn main() {
    let user_input: UserInput = UserInput::parse();
    print!("{:#?}", user_input)
}

This is what I want:

use clap::{Parser, Subcommand, Args};

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

#[derive(Debug, Subcommand)]
enum Command {
    /// A command description
    A(CommandA),
    /// B command description
    B(CommandB),
}

#[derive(Debug, Args, Filter)]
struct CommandA {
    /// Some particular parameter command A
    #[clap(short, long, value_parser)]
    particular: Option<String>,
}

#[derive(Debug, Args, Filter)]
struct CommandB {
    /// Some particular parameter command B
    #[clap(short, long, value_parser)]
    particular: Option<String>,
}

fn main() {
    let user_input: UserInput = UserInput::parse();
    print!("{:#?}", user_input)
}

Waited behaviour:

[user@host appname]$ appname a --help
appname-a 
A command description

USAGE:
    appname a [OPTIONS]

OPTIONS:
    -x, --x-value <X_VALUE>          The value of column X to be filtered in the database
    -y, --y-value <Y_VALUE>          The value of column Y to be filtered in the database
    -h, --help                       Print help information
    -p, --particular <PARTICULAR>    Some particular parameter of command A

Edit: I'm using clap 3.2.22 but I will try to move my code to the latest version.


Solution

  • You can achieve pretty much what you want using composition along with #[clap(flatten)] like this:

    use clap::{Args, Parser, Subcommand};
    #[derive(Debug, Args)]
    struct CommonArgs {
        /// The value of column x to be filtered in the database
        #[clap(short, long, value_parser)]
        x_value: Option<String>,
        /// The value of column y to be filtered in the database
        #[clap(short, long, value_parser)]
        y_value: Option<String>,
    }
    
    #[derive(Debug, Args)]
    struct ArgsA {
        #[clap(flatten)]
        common_args: CommonArgs,
        /// Hard
        #[clap(short = 'H', long)]
        particular_a: bool,
    }
    
    #[derive(Debug, Args)]
    struct ArgsB {
        // #[clap(subcommand)]
        // subcommand: ShowSubcommand,
        #[clap(flatten)]
        commmon_args: CommonArgs,
        /// Head
        #[clap(long)]
        particular_b: Option<u32>,
    }
    
    #[derive(Debug, Parser)]
    pub struct UserInput {
        #[clap(subcommand)]
        command: MyCommand,
    }
    
    #[derive(Debug, Subcommand)]
    enum MyCommand {
        /// A command description
        A(ArgsA),
        /// B command description
        B(ArgsB),
    }
    
    fn main() {
        dbg!(UserInput::parse_from("playground b --help".split(' ')));
    }
    

    The only difference being that you have one more level of indirection to type on access.

    That 'disadvantage' comes with the advantage that you can pass parts of the arguments more easily and for example implement AsRef<CommonArgs> for ArgsA and ArgsB.