Skip to content
Learni
View all tutorials
Développement Système

How to Build a Performant CLI Application in Rust in 2026

18 minINTERMEDIATE
Lire en français

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

terminal
cargo new text-stats --bin
cd text-stats
cargo add clap --features derive

We initialize a binary project and add clap for argument parsing. This lays the foundation for a well-structured and maintainable CLI.

Argument Definition

src/main.rs
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

src/stats.rs
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

src/main.rs
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

src/stats.rs
#[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