Module 10 • Lesson 51

Building CLI Tools with clap

📚 10 min💻 Free🦀 nixus.pro

Building CLI Tools with clap

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

use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "rustool")]
#[command(about = "A Rust CLI tool", long_about = None)]
#[command(version)]
struct Cli {
    /// Optional verbosity flag
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,

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

#[derive(Subcommand, Debug)]
enum Commands {
    /// Count words in a file
    Count {
        /// File to count
        #[arg(value_name = "FILE")]
        file: PathBuf,

        /// Count characters instead of words
        #[arg(short, long)]
        chars: bool,
    },
    /// Search for a pattern in a file
    Search {
        /// Pattern to search for
        pattern: String,

        /// Files to search
        #[arg(value_name = "FILES")]
        files: Vec,

        /// Case-insensitive search
        #[arg(short, long)]
        ignore_case: bool,
    },
}

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

    if cli.verbose > 0 { println!("Verbosity: {}", cli.verbose); }

    match cli.command {
        Commands::Count { file, chars } => {
            let content = std::fs::read_to_string(&file)
                .unwrap_or_else(|e| { eprintln!("Error: {}", e); std::process::exit(1); });
            if chars {
                println!("{} characters in {:?}", content.chars().count(), file);
            } else {
                println!("{} words in {:?}", content.split_whitespace().count(), file);
            }
        }
        Commands::Search { pattern, files, ignore_case } => {
            for file in files {
                let content = std::fs::read_to_string(&file).unwrap_or_default();
                for (i, line) in content.lines().enumerate() {
                    let matches = if ignore_case {
                        line.to_lowercase().contains(&pattern.to_lowercase())
                    } else {
                        line.contains(&pattern)
                    };
                    if matches { println!("{:?}:{}: {}", file, i+1, line); }
                }
            }
        }
    }
}

🎯 Practice

  1. Add a third subcommand: Stats that shows line count, word count, and character count
  2. Add a --output FILE argument to write results to a file instead of stdout
  3. Add a --format [text|json|csv] argument and implement JSON output for the count command

🎉 Key Takeaways