Building a CLI Tool in Rust

A deep dive into building command-line tools with Rust — from argument parsing with clap to colored output and error handling.

·2 min read

Building a CLI Tool in Rust#

Rust is arguably the best language for building CLI tools today. The combination of zero-cost abstractions, great error handling, and an ecosystem of libraries like clap and colored makes it a joy to work with.

Why Rust for CLIs?#

  • Single binary — no runtime dependencies, just ship the executable
  • Fast startup — no JIT warmup, no garbage collector pause
  • Cross-platform — compile for Linux, macOS, and Windows from one codebase
  • Memory safe — no segfaults in production

Setting up the project#

cargo init my-cli
cd my-cli
cargo add clap --features derive
cargo add anyhow colored

Argument parsing with clap#

The derive macro approach makes argument parsing declarative:

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(name = "my-cli", about = "A demo CLI tool")]
struct Args {
    /// Input file to process
    #[arg(short, long)]
    input: String,
 
    /// Enable verbose output
    #[arg(short, long, default_value_t = false)]
    verbose: bool,
 
    /// Output format
    #[arg(short, long, default_value = "json")]
    format: String,
}

Processing input#

Here's where the real logic lives. We read the input file, process it, and write the output:

use anyhow::{Context, Result};
use std::fs;
 
fn process_file(path: &str, verbose: bool) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read file: {}", path))?;
 
    if verbose {
        eprintln!("Read {} bytes from {}", content.len(), path);
    }
 
    // Your processing logic here
    Ok(content.to_uppercase())
}

Notice how we use anyhow::Context to add meaningful error messages. This pattern is covered in more detail in my post on Rust error handling.

Colored output#

Making your CLI output readable with colored:

use colored::Colorize;
 
fn print_result(result: &str, format: &str) {
    println!("{} Processing complete", "✓".green().bold());
    println!("{}: {}", "Format".cyan(), format);
    println!("{}: {} bytes", "Output size".cyan(), result.len());
}

Putting it all together#

fn main() -> Result<()> {
    let args = Args::parse();
    let result = process_file(&args.input, args.verbose)?;
    print_result(&result, &args.format);
    Ok(())
}

What's next#

This is just the foundation. From here you can add:

  • Subcommands for different operations
  • Progress bars with indicatif
  • Configuration files with serde and toml
  • Shell completions generated by clap

The Rust CLI ecosystem is rich and well-documented. Start small, ship fast, iterate.

Want more like this?