Jump to content

Rust: Clap + Derive Makes our Command Line Life Easy

From JOHNWICK

In my previous article Rust: Take Our CLI to the Next Level with Clap (https://medium.com/@itsuki.enjoy/rust-take-your-cli-to-the-next-level-with-clap-a0f05875ef45), we have focused on using the Builder API of the Clap crate to process command line arguments. However, as the number of subcommands and arguments increases, try to define the clap::Command struct and retrieve all those clap::ArgMatches with Command::get_matches MANUALLY becomes a hell! And!

That is where the derive feature comes in handy! In this article, please let me share with you really quick on how we can use it to construct and configure Command, Subcommand, Args, ArgGroup and ValueEnums! Let’s add the following to our Cargo.toml and start! clap = {version = "4.5.48", features = ["derive"]}


Basic

Let’s create a CommandLineApplication acting as our entry point, add some subcommands and some arguments to start off!

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

fn main() -> anyhow::Result<()> {

   let cli = CommandLineApplication::parse();
   println!("{:?}", cli);
   Ok(())

}

  1. [derive(Parser, Debug)]
  2. [command(about("Clap + Derive Demo."))]

pub struct CommandLineApplication {

   #[command(subcommand)]
   command: ApplicationSubcommands,

}

  1. [derive(Subcommand, Debug)]

enum ApplicationSubcommands {

   Task(TaskCommand),

}

  1. [derive(Args, Debug)]

pub struct TaskCommand {

   name: String,
   #[arg(short('d'), long("debug"), action = clap::ArgAction::SetTrue)]
   debug: bool,

}

If we run our main really quick and pass in couple arguments, here is what we get! Everything parsed automatically!

clap_derive task itsuki --debug // CommandLineApplication { command: Task(TaskCommand { name: "itsuki", debug: true }) }

Time for a little more details!

Derived Traits

Here, we have derived three traits. (Please ignore the Debug trait, that is just so that we can print out the structure to inspect it!)

  • Args to parse a set of arguments into a container, in our example above, struct.
  • Subcommand to parse a sub-command into a enum.
  • Parser to parse command-line arguments into a container. This is the most high-level one. Actually for Args and Subcommand, we can use #[derive(Parser)] as well.

In addition, we also have

  • CommandFactory to create a Command relevant for a container.
  • FromArgMatches to convert an instance of ArgMatches to a container.
  • ValueEnum to parse arguments into enums. We will take a look at this one in couple seconds to see how we can use it to define custom value for arguments!

Command Macro

The #[command()] is what we use to define Command attributes for both top-level parsers and when defining subcommands.

In our example above, we have used the about [= <expr>] to provide a custom description. If we want to use it to configure subcommands, exactly the same idea.

  1. [derive(Subcommand, Debug)]

enum ApplicationSubcommands {

   #[command(about("Itsuki created task."))]
   Task(TaskCommand),

}

Any Command method we used with Builder API can also be used as an attribute here. And in addition to those, we get couple derive-specific ones. rename_all

First of all, the rename_all that accept camelCase, kebab-case, PascalCase, SCREAMING_SNAKE_CASE, snake_case, lower, UPPER, verbatim to override default field / variant name case conversion for Command::name / Arg::id. Default to kebab-case. For example, let’s rename all our subcommands to use SCREAMING_SNAKE_CASE.

  1. [derive(Subcommand, Debug)]
  2. [command(rename_all = "SCREAMING_SNAKE_CASE")]

enum ApplicationSubcommands {

   Task(TaskCommand),

}

And if we print out the help here, as we expected, the command now is TASK instead. Commands:

 TASK  
 help  Print this message or the help of the given subcommand(s)

flatten

This attribute can be used either one containers that implement Subcommand or Args. When use with Subcommand , it delegates to the variant for more subcommands, and when used with Args , it delegates to the field for more arguments.

This can be really useful when we have common arguments that we want to reuse across multiple commands.

For example, let’s split out our debug above.

  1. [derive(Args, Debug)]

pub struct TaskCommand {

   name: String,
   #[command(flatten)]
   common_args: CommonArgs,

}

  1. [derive(Args, Debug)]

pub struct CommonArgs {

   #[arg(short('d'), long("debug"), action = clap::ArgAction::SetTrue)]
   debug: bool,

}

And of course, if we print out the help, the debug option is still at the same level as the name! Arguments:

 <NAME>  

Options:

 -d, --debug  
 -h, --help   Print help

Arg Macro

Just like the command macro above, any Arg method can also be used as an attribute on this #[arg()] macro!

num_args to define the number of arguments required, value_delimiter, default_value, you name! Now, just a tiny note on the required. Arguments are assumed to be required. To make an argument optional, instead of using required(false), we will wrap the field’s type in Option.

  1. [arg(short('r'))]

random: Option<String>,

ArgGroup Macro We can define ArgGroup with the #[group()] macro and of course, any ArgGroup method can also be used as an attribute.

I usually use this with the flatten option above to group out a number of attributes to make creating requirement and exclusion rules a little easier.

For example, let’s call out CommonArgs a group that should be presented simultaneously with the argument ids. We can achieve this by using the conflicts_with attributes.

pub struct TaskCommand {

   name: String,
   #[command(flatten)]
   common_args: CommonArgs,
   #[arg(short('i'), long("id"))]
   ids: Vec<u32>,

}

  1. [derive(Args, Debug)]
  2. [group(conflicts_with("ids"))]

pub struct CommonArgs {

   #[arg(short('d'), long("debug"), action = clap::ArgAction::SetTrue)]
   debug: bool,

}

Use Enum For Values Type

Last but not least, let me share with you really quick on how we can use custom enum for argument value type!

  1. [derive(ValueEnum, Debug, Clone)]

enum TaskName {

   Itsuki,

}

impl ToString for TaskName {

   fn to_string(&self) -> String {
       match self {
           TaskName::Itsuki => "itsuki",
       }
       .to_string()
   }

}

  1. [derive(Args, Debug)]

pub struct TaskCommand {

   #[arg(default_value_t=TaskName::Itsuki)]
   name: TaskName,
   #[command(flatten)]
   common_args: CommonArgs,
   #[arg(short('i'), long("id"))]
   ids: Vec<u32>,

}

Two steps.

  • Derive ValueEnum on the custom enum so that we can parse arguments into it.
  • Optionally If we want to use the enum to set the default_value, we will implement ToString on it.

If we want to rename the enum similar to what we have done above for subcommands, we can use value instead of command, a derive helper for ValueEnum (https://docs.rs/clap/latest/clap/trait.ValueEnum.html).

  1. [derive(ValueEnum, Debug, Clone)]
  2. [value(rename_all = "lower")]

enum TaskName {

   Itsuki,

}

Code Snippet

That’s it! Here is a little CLI for demonstration!

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

fn main() -> anyhow::Result<()> {

   let cli = CommandLineApplication::parse();
   println!("{:?}", cli);
   Ok(())

}

  1. [derive(Parser, Debug)]
  2. [command(name = "my-cli", about("Clap + Derive Demo."))]

pub struct CommandLineApplication {

   #[command(subcommand)]
   command: ApplicationSubcommands,

}

  1. [derive(Subcommand, Debug)]
  2. [command(rename_all = "SCREAMING_SNAKE_CASE")]

enum ApplicationSubcommands {

   #[command(about("Itsuki created task."))]
   Task(TaskCommand),

}

  1. [derive(ValueEnum, Debug, Clone)]

enum TaskName {

   Itsuki,

}

impl ToString for TaskName {

   fn to_string(&self) -> String {
       match self {
           TaskName::Itsuki => "itsuki",
       }
       .to_string()
   }

}

  1. [derive(Args, Debug)]

pub struct TaskCommand {

   #[arg(default_value_t=TaskName::Itsuki)]
   name: TaskName,
   #[command(flatten)]
   common_args: CommonArgs,
   #[arg(short('i'), long("id"))]
   ids: Vec<u32>,
   #[arg(short('r'))]
   random: Option<String>,

}

  1. [derive(Args, Debug)]
  2. [group(conflicts_with("ids"))]

pub struct CommonArgs {

   #[arg(short('d'), long("debug"), action = clap::ArgAction::SetTrue)]
   debug: bool,

}


Thank you for reading! I just saved hours of my time to try to get_matches!! (https://docs.rs/clap/latest/clap/struct.Command.html#method.get_matches) Happy deriving!

Read the full article here: https://levelup.gitconnected.com/rust-clap-derive-makes-our-command-line-life-easy-c6e868eb4acb