Rust 13. Build a Small CLI Project
Summary
If you have followed the Rust series through setup, debugging, syntax basics, ownership, modules, testing, file I/O, and serde, the next step is to combine those ideas into one small result. Without that step, each concept can stay isolated instead of turning into a working development flow.
This post uses a small word counter CLI as the example and connects project structure, file input, string processing, HashMap, tests, and output formatting inside one program. The practical conclusion is that a first mini project works best when the problem stays small, core logic lives in lib.rs, input and output stay in main.rs, and verification is handled with tests.
Document Information
- Written on: 2026-04-15
- Verification date: 2026-04-16
- Document type: tutorial
- Test environment: Windows 11 Pro, Windows PowerShell, Cargo CLI examples
- Test version: rustc 1.94.0, cargo 1.94.0
- Source quality: only official documentation is used.
- Note: this post focuses on a standard-library-based mini project flow and leaves out argument parsers like
clapand more advanced text preprocessing.
Problem Definition
A common problem while learning Rust is this: each concept makes sense on its own, but it is still unclear how to combine them into one complete program. The difficulty usually becomes visible once beginners need to handle all of the following together:
- accept a file path from the command line
- read a file
- process the string data and count words
- sort and print the result
- test the core logic
The goal of this post is not to introduce new syntax, but to connect previously learned pieces into one small CLI project.
Verified Facts
- According to the official Rust Book, a Cargo package can contain both a binary crate and a library crate, which makes it possible to keep reusable logic in the library layer. Evidence: Packages and Crates
- According to the standard library docs,
HashMapis Rust’s standard key-value collection. Evidence: HashMap in std::collections - According to the standard library docs, combining
std::env::argsandstd::fs::read_to_stringprovides the simplest file-based CLI input flow. Evidence: std::env::args, std::fs::read_to_string - According to the official Rust Book, tests can be organized with
#[cfg(test)]or in atests/directory. Evidence: Test Organization
For a beginner mini project, this layout is already enough:
word-counter/
Cargo.toml
src/
main.rs
lib.rs
tests/
word_count.rs
Keep the core logic in src/lib.rs.
use std::collections::HashMap;
pub fn count_words(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
let normalized = word.to_lowercase();
*counts.entry(normalized).or_insert(0) += 1;
}
counts
}
pub fn sort_counts(counts: HashMap<String, usize>) -> Vec<(String, usize)> {
let mut items: Vec<_> = counts.into_iter().collect();
items.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
items
}
Let src/main.rs focus on input and output.
use std::{env, error::Error, fs, io};
use word_counter::{count_words, sort_counts};
fn main() -> Result<(), Box<dyn Error>> {
let path = env::args().nth(1).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "usage: cargo run -- <file-path>")
})?;
let text = fs::read_to_string(&path)?;
let ranked = sort_counts(count_words(&text));
for (word, count) in ranked.into_iter().take(10) {
println!("{}\t{}", word, count);
}
Ok(())
}
Then verify the public API through tests/word_count.rs.
use word_counter::{count_words, sort_counts};
#[test]
fn counts_words_case_insensitively() {
let counts = count_words("Rust rust RUST safety");
assert_eq!(counts.get("rust"), Some(&3));
assert_eq!(counts.get("safety"), Some(&1));
}
#[test]
fn sorts_by_frequency_descending() {
let sorted = sort_counts(count_words("b a a c c c"));
assert_eq!(sorted[0], ("c".to_string(), 3));
assert_eq!(sorted[1], ("a".to_string(), 2));
}
That example shows how the earlier concepts connect:
env::args: accept the input pathread_to_string: read the filesplit_whitespaceandto_lowercase: process stringsHashMap: accumulate word countslib.rsandmain.rs: separate structure and behaviortests/: add verification
Directly Confirmed Results
- Directly confirmed result: the Rust toolchain versions available in the current writing environment were:
rustc --version
cargo --version
- Observed output:
rustc 1.94.0 (4a4ef493e 2026-03-02)
cargo 1.94.0 (85eff7c80 2026-01-15)
- Directly confirmed result: when I placed the following
sample.txtnext to the mini project and ran it, the output was:
Rust rust safety safety safety tools
cargo run --quiet -- sample.txt
- Observed output:
safety 3
rust 2
tools 1
- Directly confirmed result: when I ran
cargo test --quietwith the same project structure, the key output was:
cargo test --quiet
- Observed output:
running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
- Limitation of direct reproduction: I verified the representative run and tests in a temporary Cargo project, but I did not validate larger inputs, punctuation handling, or extended requirements such as stop-word filtering.
Interpretation / Opinion
- My view is that the point of a first mini project is not impressive functionality, but getting used to connecting concepts through one file structure.
- Opinion: beginners usually gain more from finishing one complete tool with the standard library and a few pure functions than from adding many crates too early.
- Opinion: input and output often change, while the core calculation logic tends to stay stable, so even a mini project benefits from centering the design on
lib.rs.
Limits and Exceptions
- This example uses whitespace-based tokenization only, so it does not cover punctuation handling, morphological analysis, or Unicode normalization.
- For large files or tighter memory requirements, buffered reading may be a better fit than reading everything at once.
- A production-quality CLI would likely need more work around argument parsing, output formats, exit codes, and logging.
- The sorting rule and whether to remove stop words depend on the actual problem you want to solve.
댓글남기기