How to Build Classic Unix Tools in Rust: A Step‑by‑Step Guide
This article walks through a Rust‑based implementation of classic Unix command‑line utilities—hello world, echo, cat, head, uniq, wc, find, grep, comm, ls, and more—showing project setup, Cargo usage, argument parsing with clap, error handling with Result, testing strategies, and code snippets for each tool.
Overview
This tutorial implements classic Unix command‑line utilities in Rust, demonstrating project setup, argument parsing, error handling, testing, and idiomatic Rust patterns. The source code is available at kyclark/command-line-rust ( https://github.com/kyclark/command-line-rust ).
Chapter 1 – Hello, world!
Creates a new Cargo project, explains the standard directory layout, and compiles a minimal fn main() { println!("Hello, world!"); } program. Integration testing uses Command::cargo_bin("hello").unwrap() and asserts with cmd.assert().success().stdout("Hello, world!\n"). Modifying src/main.rs demonstrates test failure. Introduces std::process::exit for explicit exit codes and std::process::abort for non‑zero termination. Shows the Result type with Ok and Err variants.
Chapter 2 – Echo
Builds an echo clone using the clap crate. Defines a positional argument text (multiple, required) and a flag omit_newline ( -n). Retrieves values with matches.values_of_lossy("text").unwrap() and checks the flag with matches.is_present("omit_newline"). Prints the joined text and conditionally adds a newline:
use clap::{App, Arg};
let matches = App::new("echo")
.arg(Arg::with_name("text").multiple(true).required(true))
.arg(Arg::with_name("omit_newline").short("n"))
.get_matches();
let text = matches.values_of_lossy("text").unwrap();
let omit_newline = matches.is_present("omit_newline");
print!("{}{}", text.join(" "), if omit_newline { "" } else { "
" });Tests use assert_eq! and propagate errors with Result<(), Box<dyn std::error::Error>>.
Chapter 3 – Cat
Implements cat to concatenate files and optionally number lines. Defines a custom MyResult alias for Result<(), Box<dyn std::error::Error>>. Parses files and a number flag with clap. Uses Iterator::enumerate to generate line numbers and BufRead::read_line to read input. Demonstrates the placeholder _ and explicit turbofish syntax ::<> for generic type annotation.
Chapter 4 – Head
Creates a head clone that prints the first N lines or bytes. Provides unit tests for a parse_positive_int helper. Uses unimplemented! and panic! as placeholders. Command‑line arguments are defined with App::new and Arg::with_name, including mutually exclusive lines and bytes options via conflicts_with. Reads input with BufRead::read_line and limits output with take.
Chapter 5 – Wc
Implements wc to count lines, words, bytes, and characters. Parses arguments with if let and Result, exiting on error via std::process::exit. Defines a Config struct holding flags files, words, bytes, chars, and lines. Uses clap options multiple and default_value. If no counters are specified, sets all to true with if [lines, words, bytes, chars].iter().all(|v| !v) { … }. Reads data with BufRead, implements generic readers via impl BufRead for R, and tests with an in‑memory std::io::Cursor.
Chapter 6 – Uniq
Implements uniq to remove consecutive duplicate lines. Writes to either stdout or a file using the Write trait and macros write! / writeln!. Captures variables in a closure to format optional line numbers and count occurrences:
let mut print = |num: u64, text: &str| -> Result<()> {
if num > 0 {
if args.count {
write!(out_file, "{num:>4} {text}")?;
} else {
write!(out_file, "{text}")?;
}
}
Ok(())
};Chapter 7 – Find
Builds a recursive find utility. Defines an enum for entry types (directory, file, symlink) and a Config struct for search paths, name patterns, and type filters. Explains the difference between . and * in glob patterns. Uses Regex::new with ^ and $ anchors for exact matching. Traverses directories with WalkDir, handling errors via filter_map, then applies filter for type and name, finally collecting paths:
for path in &args.paths {
let entries = WalkDir::new(path)
.into_iter()
.filter_map(|e| match e {
Err(e) => { eprintln!("{e}"); None }
Ok(entry) => Some(entry),
})
.filter(type_filter)
.filter(name_filter)
.map(|entry| entry.path().display().to_string())
.collect::<Vec<_>>();
println!("{}", entries.join("
"));
}Chapter 8 – Cut
Implements cut to extract fields, bytes, or characters. Parses position specifications with a parse_pos function that supports comma‑separated lists and ranges; unit tests use assert! and assert_eq! . Declares conflicting arguments with Arg::conflicts_with_all . Extraction logic for fields uses the csv crate: <code>match extract { Extract::Fields(field_pos) => { let mut reader = ReaderBuilder::new() .delimiter(delimiter) .has_headers(false) .from_reader(file); let mut wtr = WriterBuilder::new() .delimiter(delimiter) .from_writer(io::stdout()); for record in reader.records() { wtr.write_record(extract_fields(&record?, field_pos))?; } } // Bytes and Chars variants omitted for brevity } </code>
Chapter 9 – Grep
Implements grep with regular‑expression matching. Builds the regex via RegexBuilder and enables case‑insensitive matching with .case_insensitive(true) . Constructs error messages using Result::map_err . Demonstrates two equivalent matching expressions: a verbose logical combination and a concise bitwise XOR: <code>if (pattern.is_match(&line) && !invert_match) || (!pattern.is_match(&line) && invert_match) { matches.push(line.clone()); } // equivalent to if pattern.is_match(&line) ^ !invert_match { matches.push(line.clone()); } </code>
Chapter 10 – Comm
Implements comm to compare two sorted files. Defines a Config struct with flags for column suppression and case sensitivity. Parses arguments with App::new and Arg::with_name , marking inputs as required(true) . Reads each file with BufReader , counts lines/bytes, and prints selected columns. Replaces double‑negative checks ( if !config.suppress_col1 ) with positively named flags for clarity.
Chapter 11 – Tail
Implements tail to output the last N lines or bytes. Uses an enum ( PlusZero , TakeNum ) to represent counting modes and stores offsets in i64 . Parses numeric arguments with a compiled regex stored in a OnceCell . Error handling uses anyhow! . Example parsing logic: <code>let lines = parse_num(args.lines).map_err(|e| anyhow!("illegal line count -- {e}"))?; let bytes = args.bytes.map(parse_num).transpose().map_err(|e| anyhow!("illegal byte count -- {e}"))?; </code>
Chapter 12 – Fortune
Implements a fortune program that selects a random quote from a text database. Creates an index file with strfile and inspects it with head . Builds a regex with RegexBuilder to filter quotes. The Config struct holds resources, patterns, and an optional seed. Random selection uses SliceRandom and a reproducible RNG via StdRng::seed_from_u64 or StdRng::from_entropy :
use rand::prelude::SliceRandom;
use rand::{rngs::StdRng, SeedableRng};
fn pick_fortune(fortunes: &[Fortune], seed: Option<u64>) -> Option<String> {
let mut rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::from_entropy(),
};
fortunes.choose(&mut rng).map(|f| f.text.clone())
}Chapter 13 – Cal
Implements a cal clone that displays a calendar. Uses the chrono crate ( NaiveDate , Local::now() ) to compute month layouts. Parses optional month and year arguments, declaring mutual exclusivity with App::conflicts_with_all . Highlights the current day using Style::reverse and formats output with format! . The core rendering function builds a vector of month strings and prints either a single month or the full year in three‑column layout.
Chapter 14 – Ls
Implements an ls clone that lists files and directories. Defines an enum for permission categories (owner, group, others) and uses bit masks via metadata::mode to test file mode bits. An Owner enum represents the three ownership classes; an impl block provides a masks method returning the corresponding octal masks. Generates long‑format output with the tabular::Table crate. Documentation comments are written with triple slashes ( /// ) and rendered via cargo doc . Reference: kyclark/command-line-rust – https://github.com/kyclark/command-line-rust
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
