Skip to content

Build a CLI Tool with Clap ​

@Pawel Czerwinski

At work you often run into tasks that you must repeat again and again. The project schedule rarely leaves room to build a complex interface for them. When a polished user interface is not necessary, a developer usually has to run the task by hand. Tasks like these are a very good fit for a CLI that simplifies the flow. I recently had a need like this, so I wrote a small tool in Rust to work faster.

Why Rust? ​

Before I start, I want to share a short impression. I once wrote a CLI tool in C#, and the developer experience was not good. The part that parses the commands was awkward. Even with a framework, I had to write a lot of code that was hard to follow. What bothered me most was the setup: I had to declare every command, every argument and every option one by one. Those declarations had a weak connection to the code that does the real work.

Clap in Rust feels different. Its strength is that you can organise the commands and the features of a CLI tool as data structures. The approach is very natural. The credit goes to the type system of Rust, which is very expressive.

An Introduction to Clap ​

Clap is a well-known crate for CLI development in Rust. ripgrep, the fastest search tool, uses Clap. Here is a simple example:

rust
fn main() {
    let matches = Command::new("echor")
        .version("0.1.0")
        .author("marvinhsu")
        .about("Rust echo")
        .arg(
            Arg::new("text")
                .value_name("TEXT")
                .help("Input text")
                .required(true)
                .num_args(1..),
        )
        .arg(
            Arg::new("omit_newline")
                .short('n')
                .help("Do not print newline")
                .action(ArgAction::SetTrue),
        )
        .get_matches();
    
    todo!();
}

For the full code, see the practice project that I wrote earlier. That repository follows the exercises in Command-Line Rust. The example above uses the Builder API of Clap. Most CLI frameworks work in this style. You configure each command one by one. Then you write many conditional branches to parse the incoming command and to dispatch the correct implementation.

A later version of Clap added the Derive API, and that greatly improved the developer experience. With the Derive API you organise the commands as data structures in a simple and natural way. You then implement the code for each command as if it were an object. The official documentation says that you should normally use the Derive API. Choose the Builder API only when you have a strong performance requirement, or when you need finer control. The Derive API really is much easier to understand.

Clap With Derive API ​

Now let me introduce the Derive API. I use Git as the example, because everybody uses it. Git has commands such as checkout, branch, push and commit. The checkout command takes a branch name as an argument. It also has a -f flag that forces the switch. Here is the same structure expressed with the Derive API:

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

// The main command of Git
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Git {
    #[command(subcommand)]
    subcmd: SubCommand,
}

// Use an enum to describe the different subcommands
#[derive(Subcommand)]
enum SubCommand {
    // The Checkout command has its own data members
    Checkout(Checkout),
    Branch,
    Push,
    Commit
}

// The data that the Checkout command needs
#[derive(Args)]
struct Checkout {
    branch_name: String,
    
    #[arg(long, short)]
    force: bool,
}

// The Checkout command has an associated execute method. When you call it, the method reads the arguments of the command and runs the command.
impl Checkout {
    fn execute(&self) {
        println!("Checkout branch: {}", self.branch_name);
    }
}

fn main() {
    let git = Git::parse();

    // Use pattern matching to decide which command to run
    match git.subcmd {
        SubCommand::Checkout(checkout) => {
            checkout.execute();
        }
        SubCommand::Branch => todo!(),
        SubCommand::Push => todo!(),
        SubCommand::Commit => todo!(),
    }
}

I find this much clearer and easier to read than the Builder API, and it is very simple to implement. The one drawback is probably that you cannot easily customise the generated help text. Here is the top-level help text, and nothing looks wrong:

shell
❯ cargo run -- -h
Usage: clitest <COMMAND>

Commands:
  checkout  
  branch    
  push      
  commit    
  help      Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Now look at the help text for checkout:

shell
❯ cargo run -- checkout -h
Usage: clitest checkout [OPTIONS] <BRANCH_NAME>

Arguments:
  <BRANCH_NAME>  

Options:
  -f, --force  
  -h, --help   Print help

This does not stop you from using the tool. But CLI convention calls a true/false setting such as -f a flag, not an Option. Git's own documentation does not make that distinction. If you want to customise the help text, you must use the Builder API. That is because the Derive API exists to be simple and easy to use.

Summary ​

Clap gives you the Derive API for CLI development. When you describe the commands and the features with data structures, the work becomes simple and natural. I think the main reason is the type system of Rust, which makes it easier for a developer to describe a feature as a data structure.

Last updated:

Built with VitePress