Labs ICT
Pro Login

Building a CLI Tool

Putting it all together in a real project.

Building a CLI Tool

Let's build a practical project: a word counter CLI tool. This will teach you how to structure a real Rust project, handle errors, and use external crates.

We'll use clap for argument parsing and handle files gracefully with proper error handling.

// This is what our final project structure looks like:
// word_counter/
// ├── Cargo.toml
// ├── src/
// │   └── main.rs
// └── tests/
//     └── integration_test.rs

fn main() {
    println!("We'll build a word counter CLI tool!");
}
Try it Yourself ->

Cargo.toml Configuration

Start by setting up your Cargo.toml with the dependencies we need. We'll use clap for argument parsing.

// Cargo.toml content:
// [package]
// name = "word_counter"
// version = "0.1.0"
// edition = "2021"
//
// [dependencies]
// clap = { version = "4.0", features = ["derive"] }

fn main() {
    println!("Set up Cargo.toml with clap dependency");
}
Try it Yourself ->

Argument Parsing with Clap

Clap makes it easy to define command-line arguments. We'll use the derive macro for clean, declarative argument definitions.

use clap::Parser;

#[derive(Parser)]
#[command(name = "word_counter")]
#[command(about = "Count words, lines, and characters in files")]
struct Args {
    /// File path to analyze
    file: String,

    /// Count words only
    #[arg(short, long)]
    words: bool,

    /// Count lines only
    #[arg(short, long)]
    lines: bool,
}

fn main() {
    let args = Args::parse();
    println!("File: {}", args.file);
    println!("Words only: {}", args.words);
    println!("Lines only: {}", args.lines);
}
Try it Yourself ->

Reading Files

We'll use std::fs to read files. The read_to_string function is perfect for our word counter since we need the entire file content.

use std::fs;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(path)
}

fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

fn count_lines(text: &str) -> usize {
    text.lines().count()
}

fn count_chars(text: &str) -> usize {
    text.chars().count()
}

fn main() {
    let text = "Hello world\nThis is a test\nRust is amazing";
    println!("Words: {}", count_words(text));
    println!("Lines: {}", count_lines(text));
    println!("Chars: {}", count_chars(text));
}
Try it Yourself ->

Error Handling in Real Projects

Real projects need proper error handling. We'll create custom error types and use the ? operator for clean error propagation.

use std::fmt;

#[derive(Debug)]
enum AppError {
    FileNotFound(String),
    IoError(std::io::Error),
    EmptyFile,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::FileNotFound(path) => write!(f, "File not found: {}", path),
            AppError::IoError(e) => write!(f, "IO error: {}", e),
            AppError::EmptyFile => write!(f, "File is empty"),
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::IoError(e)
    }
}

fn process_file(path: &str) -> Result<String, AppError> {
    let content = std::fs::read_to_string(path)?;
    if content.is_empty() {
        return Err(AppError::EmptyFile);
    }
    Ok(content)
}
Try it Yourself ->

Putting It All Together

Now we combine everything into a working CLI tool. The main function ties together argument parsing, file reading, and result display.

use std::fs;

fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

fn count_lines(text: &str) -> usize {
    text.lines().count()
}

fn count_chars(text: &str) -> usize {
    text.chars().count()
}

fn analyze_file(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;

    println!("Analysis of: {}", path);
    println!("─────────────────────────");
    println!("Lines: {}", count_lines(&content));
    println!("Words: {}", count_words(&content));
    println!("Characters: {}", count_chars(&content));

    Ok(())
}

fn main() {
    match analyze_file("example.txt") {
        Ok(()) => println!("Analysis complete!"),
        Err(e) => eprintln!("Error: {}", e),
    }
}
Try it Yourself ->

Building Your Project

Use cargo build to compile your project and cargo run to execute it. For release builds with optimizations, use cargo build --release.

This project teaches you the fundamentals of building real Rust applications: project structure, dependencies, error handling, and command-line interfaces.