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.