JSTAcademy
0 XP
Dashboard
Crash Courses
CLI Tool with clap
60 min
PhD+200 XP
Crash Courses · PhD

CLI Tool with clap

Parse arguments, read files, and ship a polished command-line app
60 min read+200 XP on completionCert: Rust Crash Course
Tap any word in the text below to start reading from there.

CLI Tool with clap

Setup

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

Complete Word-Count CLI (wc clone)

use clap::Parser;
use std::{
    fs::File,
    io::{self, BufRead, BufReader},
    path::PathBuf,
    process,
};

/// A minimal wc clone — count lines, words, and chars in a file.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// File to analyse (reads stdin if omitted)
    #[arg(value_name = "FILE")]
    file: Option<PathBuf>,

    /// Count lines
    #[arg(short = 'l', long)]
    lines: bool,

    /// Count words
    #[arg(short = 'w', long)]
    words: bool,

    /// Count characters
    #[arg(short = 'c', long)]
    chars: bool,
}

struct Counts {
    lines: usize,
    words: usize,
    chars: usize,
}

fn count(reader: impl BufRead) -> Counts {
    let mut counts = Counts { lines: 0, words: 0, chars: 0 };

    for line in reader.lines() {
        match line {
            Ok(l) => {
                counts.lines += 1;
                counts.words += l.split_whitespace().count();
                counts.chars += l.chars().count() + 1; // +1 for newline
            }
            Err(e) => {
                eprintln!("Read error: {}", e);
                process::exit(1);
            }
        }
    }
    counts
}

fn main() {
    let args = Args::parse();

    let counts = if let Some(path) = &args.file {
        match File::open(path) {
            Ok(f)  => count(BufReader::new(f)),
            Err(e) => {
                eprintln!("Error opening {:?}: {}", path, e);
                process::exit(1);
            }
        }
    } else {
        count(BufReader::new(io::stdin()))
    };

    // If no flags, show all
    let show_all = !args.lines && !args.words && !args.chars;

    if args.lines || show_all { print!("{:>8}", counts.lines); }
    if args.words || show_all { print!("{:>8}", counts.words); }
    if args.chars || show_all { print!("{:>8}", counts.chars); }

    if let Some(path) = &args.file {
        println!(" {}", path.display());
    } else {
        println!();
    }
}

Build and Run

cargo build --release

# Count everything in a file
./target/release/wc Cargo.toml

# Only word count
./target/release/wc -w Cargo.toml

# From stdin
echo "hello world foo" | ./target/release/wc

# Built-in help (clap generates this automatically)
./target/release/wc --help

Adding Subcommands

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Count words in a file
    Count { file: PathBuf },
    /// Reverse a string
    Reverse { text: String },
}

Writing to Files

use std::io::Write;
use std::fs::File;

let mut file = File::create("output.txt")?;
writeln!(file, "Hello, file!")?;
0%