[] markdown.page

Building a CLI Tool in Rust

A quick guide to building a fast, ergonomic command-line tool using Rust and clap.

Setup

Create a new project:

cargo new greet-cli
cd greet-cli

Add dependencies to Cargo.toml:

[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"

Defining the CLI

Use clap's derive macros to define your arguments:

use clap::Parser;

#[derive(Parser)]
#[command(name = "greet", about = "A friendly greeter")]
struct Args {
    /// Name of the person to greet
    name: String,

    /// Number of times to greet
    #[arg(short, long, default_value_t = 1)]
    count: u8,

    /// Use uppercase
    #[arg(short, long)]
    uppercase: bool,
}

The main function

use anyhow::Result;

fn main() -> Result<()> {
    let args = Args::parse();

    for _ in 0..args.count {
        let greeting = format!("Hello, {}!", args.name);
        if args.uppercase {
            println!("{}", greeting.to_uppercase());
        } else {
            println!("{greeting}");
        }
    }

    Ok(())
}

Usage

$ cargo run -- world
Hello, world!

$ cargo run -- --count 3 --uppercase Rust
HELLO, RUST!
HELLO, RUST!
HELLO, RUST!

Error handling

anyhow gives you ergonomic error handling. If you read a file:

use std::fs;

fn read_config(path: &str) -> Result<String> {
    let content = fs::read_to_string(path)?;
    Ok(content)
}

The ? operator propagates errors with full context. No unwrap, no panic.

Testing

Add tests in the same file:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Args::command().debug_assert();
    }
}

Run them:

cargo test

Distribution

Build a release binary:

cargo build --release

The binary is at target/release/greet. It's a single static file — no runtime, no dependencies. Copy it anywhere and it works.


That's it. A complete CLI in under 50 lines of Rust. Fast to compile, fast to run, and pleasant to maintain.