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
Can you explain how derive attributes work?
Why would you want to use the derive vs builder API?
What are the differences between the Clap output and the output from Bitcoin-CLI? What are the limitations of Clap?
How might you modify the help output using the clap-help crate to more closely mirror the Bitcoin-CLI help output?
Extra Practice
Create a method that calls the Bitcoin Core RPC server and returns the blockhash for a given height and return it from this CLI
Add another subcommand getblock that returns a JSON object with information about the block