Introduction
Rust has become the go-to language for command-line tools thanks to its memory safety and performance. In this intermediate tutorial, we will build a CLI that analyzes text files and generates statistics. You will learn how to structure a project, manage arguments with clap, implement robust business logic, and write tests. This guide will enable you to produce production-ready code.
Prerequisites
- Rust 1.75+ and Cargo installed
- Basic knowledge of ownership and traits
- Terminal and code editor (VS Code recommended)
Project Initialization
cargo new text-stats --bin
cd text-stats
cargo add clap --features deriveWe initialize a binary project and add clap for argument parsing. This lays the foundation for a well-structured and maintainable CLI.
Argument Definition
use clap::Parser;
#[derive(Parser)]
#[command(name = "text-stats")]
struct Args {
#[arg(short, long)]
file: String,
}
fn main() {
let args = Args::parse();
println!("Analyse du fichier : {}", args.file);
}Clap's derive macro automatically generates the parser. This avoids boilerplate code and makes arguments type-safe.
File Reading and Analysis
use std::fs;
pub fn analyze_file(path: &str) -> Result<(usize, usize), Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;
let word_count = content.split_whitespace().count();
let char_count = content.chars().count();
Ok((word_count, char_count))
}This function reads the file and calculates statistics. Using Result enables clean handling of I/O errors.
Integration in main
use clap::Parser;
mod stats;
#[derive(Parser)]
#[command(name = "text-stats")]
struct Args {
#[arg(short, long)]
file: String,
}
fn main() {
let args = Args::parse();
match stats::analyze_file(&args.file) {
Ok((words, chars)) => println!("Mots: {}, Caractères: {}", words, chars),
Err(e) => eprintln!("Erreur: {}", e),
}
}We connect argument parsing to the analysis logic. The match pattern ensures all errors are handled explicitly.
Adding Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_empty() {
let result = analyze_file("tests/empty.txt");
assert!(result.is_ok());
}
}Tests are placed in the same file for better readability. They verify behavior on edge cases such as an empty file.
Best Practices
- Always use Result for fallible operations
- Structure your code into modules as early as possible
- Add tests for every public function
- Prefer custom errors with thiserror for larger projects
Common Mistakes to Avoid
- Ignoring errors with unwrap() in production
- Forgetting to declare modules with mod
- Not handling non-existent file cases
- Using raw strings instead of strong types for paths
Going Further
Discover our advanced courses on Rust and software architectures: https://learni-group.com/formations