TechLead
Lesson 20 of 28
5 min read
Rust

Building CLI Tools in Rust

Build powerful command-line tools in Rust with clap for argument parsing, colored output, progress bars, file I/O, and binary distribution.

Why Rust for CLI Tools?

Rust is an excellent choice for CLI tools: fast startup, single binary distribution, cross-platform support, and excellent libraries. Tools like ripgrep, fd, bat, exa, and delta are all written in Rust and are faster than their traditional counterparts.

Argument Parsing with clap

// Cargo.toml:
// clap = { version = "4", features = ["derive"] }

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "mytool")]
#[command(version, about = "A powerful CLI tool built with Rust")]
struct Cli {
    /// Input file to process
    #[arg(short, long)]
    input: String,

    /// Output file (default: stdout)
    #[arg(short, long)]
    output: Option,

    /// Enable verbose logging
    #[arg(short, long, default_value_t = false)]
    verbose: bool,

    /// Number of threads to use
    #[arg(short = 'j', long, default_value_t = 4)]
    threads: usize,

    #[command(subcommand)]
    command: Option,
}

#[derive(Subcommand)]
enum Commands {
    /// Process a file
    Process {
        /// Processing mode
        #[arg(short, long, default_value = "fast")]
        mode: String,
    },
    /// Validate input
    Validate,
    /// Show statistics
    Stats {
        /// Show detailed statistics
        #[arg(short, long)]
        detailed: bool,
    },
}

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

    if cli.verbose {
        println!("Input: {}", cli.input);
        println!("Threads: {}", cli.threads);
    }

    match &cli.command {
        Some(Commands::Process { mode }) => {
            println!("Processing in {mode} mode...");
        }
        Some(Commands::Validate) => {
            println!("Validating...");
        }
        Some(Commands::Stats { detailed }) => {
            println!("Stats (detailed={})", detailed);
        }
        None => {
            println!("No subcommand — running default action");
        }
    }
}

// Usage:
// mytool --input data.txt --verbose process --mode fast
// mytool -i data.txt -j 8 validate
// mytool --help

Colored Output and Progress Bars

// Cargo.toml:
// colored = "2"
// indicatif = "0.17"
// console = "0.15"

use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;

fn main() {
    // Colored output
    println!("{}", "Success!".green().bold());
    println!("{}", "Warning: check config".yellow());
    println!("{}", "Error: file not found".red().bold());
    println!("{}", "Info: processing...".blue());
    println!("{}", "Debug: value=42".dimmed());

    // Progress bar
    let pb = ProgressBar::new(100);
    pb.set_style(ProgressStyle::with_template(
        "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})"
    ).unwrap().progress_chars("#>-"));

    for _ in 0..100 {
        pb.inc(1);
        std::thread::sleep(Duration::from_millis(30));
    }
    pb.finish_with_message("Done!");

    // Spinner for indeterminate tasks
    let spinner = ProgressBar::new_spinner();
    spinner.set_message("Connecting to server...");
    for _ in 0..50 {
        spinner.tick();
        std::thread::sleep(Duration::from_millis(50));
    }
    spinner.finish_with_message("Connected!");
}

File I/O and stdin/stdout

use std::fs;
use std::io::{self, BufRead, Write, BufWriter, BufReader};
use std::path::PathBuf;

fn process_file(input: &str, output: Option<&str>) -> io::Result<()> {
    // Read entire file
    let content = fs::read_to_string(input)?;
    println!("Read {} bytes", content.len());

    // Read line by line (memory efficient for large files)
    let file = fs::File::open(input)?;
    let reader = BufReader::new(file);

    let mut line_count = 0;
    for line in reader.lines() {
        let line = line?;
        line_count += 1;
        // Process each line
        if line.contains("ERROR") {
            eprintln!("Found error on line {line_count}: {line}");
        }
    }

    // Write output
    let mut writer: Box = match output {
        Some(path) => Box::new(BufWriter::new(fs::File::create(path)?)),
        None => Box::new(io::stdout().lock()),
    };

    writeln!(writer, "Processed {line_count} lines")?;

    // Read from stdin
    let stdin = io::stdin();
    for line in stdin.lock().lines().take(5) {
        let line = line?;
        println!("Got: {line}");
    }

    Ok(())
}

fn main() {
    if let Err(e) = process_file("input.txt", Some("output.txt")) {
        eprintln!("Error: {e}");
        std::process::exit(1);
    }
}

Cross-Platform Distribution

# Build for release (optimized)
cargo build --release

# Cross-compile (with cross)
cargo install cross
cross build --release --target x86_64-unknown-linux-musl  # Static Linux binary
cross build --release --target x86_64-pc-windows-gnu       # Windows
cross build --release --target aarch64-apple-darwin         # macOS ARM

# Install locally
cargo install --path .

# Publish to crates.io
cargo publish

# Reduce binary size
# In Cargo.toml:
# [profile.release]
# opt-level = "z"    # Optimize for size
# lto = true         # Link-time optimization
# strip = true       # Strip debug symbols
# codegen-units = 1  # Single codegen unit

Key Takeaways

  • ✅ Rust produces fast, single-binary CLI tools with no runtime dependencies
  • ✅ clap provides powerful argument parsing with derive macros and subcommands
  • ✅ colored and indicatif create beautiful terminal UIs with progress bars
  • ✅ Use BufReader/BufWriter for efficient large-file processing
  • ✅ Cross-compile to multiple platforms with the cross tool

Continue Learning