DaleSchool

Building CLI Tools With clap

Intermediate15min

Learning Objectives

  • Define CLI arguments with clap's derive approach
  • Implement subcommands
  • Provide automatic help and version information

Working Code

The clap crate provides derive macros that let you define even complex CLIs declaratively:

use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(name = "todo", version, about = "A small, fast CLI TODO app", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Add a new task
    Add {
        #[arg(value_name = "TEXT")]
        text: String,
    },
    /// Mark a task as done
    Done {
        #[arg(value_name = "ID")]
        id: u32,
    },
    /// List tasks
    List {
        #[arg(long, help = "Include completed tasks")]
        all: bool,
    },
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Add { text } => println!("Adding: {text}"),
        Commands::Done { id } => println!("Marking done: #{id}"),
        Commands::List { all } => println!("Listing (all={all})"),
    }
}
  • Cli::parse() automatically reads std::env::args(), validates them, and fills the struct.
  • Adding #[command(version)] gives you a --version flag; filling in about adds a description to --help.

Help and Error Messages

$ todo --help
Usage: todo <COMMAND>

Commands:
  add   Add a new task
  done  Mark a task as done
  list  List tasks
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help (see more with '--help')
  -V, --version  Print version

Invalid input produces a helpful message and a non-zero exit code:

$ todo add
? failed to parse argument: TEXT is required but missing

Try It Yourself

  1. Test that todo list --all sets all to true and omitting it sets it to false.
  2. Add another subcommand that mixes long and short options, like todo done --id 3.
  3. Use clap::Parser::command().long_about("...") to add a detailed README-level help description.
  1. Environment variables — Use #[arg(env = "TODO_PATH")] so the storage path can also be set via an environment variable.
  2. Exit codes — Add a pattern that prints a message with eprintln! and exits with std::process::exit(1) on abnormal conditions.
  3. Config subcommand — Design a todo config set --key theme --value dark subcommand and connect it to serde-based config file storage.