Introduction to the Clap crate

Clap is a popular library used for creating a polished, feature-rich command line interface. This includes common argument behavior, help generation, suggested fixes for users, colored outputs, shell completion, etc.

Example: Replicating a command from Bitcoin-CLI

Let's replicate a command from the bitcoin-cli that comes packaged with a Bitcoin core node. If we run the following command:

$ bitcoin-cli help getblockhash

We'll get the following response:

getblockhash height

Returns hash of block in best-block-chain at height provided.

Arguments:
1. height    (numeric, required) The height index

Result:
"hex"    (string) The block hash

Examples:
> bitcoin-cli getblockhash 1000
> curl --user myusername --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockhash", "params": [1000]}' -H 'content-type: text/plain;' http://127.0.0.1:8332/

Let's re-create this using the Clap crate. There are two ways to do this. We can use the derive attributes that the crate offers or we can use the builder method.

Setup

Let's create a new project and install the Clap crate with the optional derive feature:

$ cargo new clap_example
$ cd clap_example
$ cargo add clap --features derive

Derive Example

We can create a command and accept the getblockhash subcommand like so using various derive attributes:

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "Bitcoin CLI")]
#[command(version = "1.0")]
#[command(about = "Bitcoin Core RPC Client", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>
}

#[derive(Subcommand)]
enum Commands {
    /// Returns hash of block in best-block-chain at height provided.
    Getblockhash { 
        #[arg(
            required = true,
            help = "(numeric, required) The height index",
        )]
        height: u64 
    },
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Some(Commands::Getblockhash { height }) => {
            println!("returns blockhash for height: {height:?}")
        }
        None => {
            eprintln!("Error: too few parameters")
        }
    }
}

Builder Example

use clap::{arg, Command};

fn main() {
    let matches = Command::new("Bitcoin CLI")
        .version("1.0")
        .about("Bitcoin Core RPC Client")
        .subcommand_required(true)
        .subcommand(
            Command::new("getblockhash")
                .about("Returns hash of block in best-block-chain at height provided.")
                .arg(
                    arg!([height]).required(true)
                )
        )
        .get_matches();

    match matches.subcommand() {
        Some(("getblockhash", sub_matches)) => {
            println!(
                "returns blockhash for height: {}",
                sub_matches.get_one::<String>("height").unwrap()
            )
        },
        _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`")
    }
}

Questions

  1. Can you explain how derive attributes work?

  2. Why would you want to use the derive vs builder API?

  3. What are the differences between the Clap output and the output from Bitcoin-CLI? What are the limitations of Clap?

  4. How might you modify the help output using the clap-help crate to more closely mirror the Bitcoin-CLI help output?

Extra Practice

  1. Create a method that calls the Bitcoin Core RPC server and returns the blockhash for a given height and return it from this CLI

  2. Add another subcommand getblock that returns a JSON object with information about the block

Last updated