Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Your First CLI

If you only remember one thing: you already know enough Rust to write a real program that reads a file, counts something useful, and prints a result. This chapter is you doing it.

What we are building

A tiny command-line program called wordc. You give it a file path; it tells you how many words are in that file. This is three hundred lines of Python in a week-two bootcamp. It is thirty lines of Rust, and the end result is a standalone native binary you can hand to a colleague who does not have Rust installed.

$ cargo run -- Cargo.toml
Cargo.toml has 12 words.

Make the project

cargo new wordc
cd wordc

Open src/main.rs and replace its contents with this:

use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("usage: wordc <path>");
        process::exit(1);
    }

    let path = &args[1];

    let text = match fs::read_to_string(path) {
        Ok(t)  => t,
        Err(e) => {
            eprintln!("could not read {path}: {e}");
            process::exit(1);
        }
    };

    let count = text.split_whitespace().count();
    println!("{path} has {count} words.");
}

▶ Run this in the Rust Playground

Then run it against the project’s own manifest:

cargo run -- Cargo.toml

You should see Cargo.toml has 12 words. (the exact number will differ).

Let’s walk through the thirty lines

Shape of the program

Every idea from Part 0, in one file

OS args env::args() Vec<String> args.len() >= 2? Result<String, _> fs::read_to_string count words split_whitespace stdout Every arrow carries a type. Every branch is handled.

Line by line:

  • use std::env; pulls in the env module so we can write env::args() instead of std::env::args().
  • let args: Vec<String> = env::args().collect(); collects the command-line arguments into a growable list. Vec<T> is Rust’s Array/list. .collect() turns an iterator into a collection.
  • if args.len() < 2 { ... } is a plain guard. process::exit(1) ends the program with a non-zero status, the Unix convention for “something went wrong”.
  • let path = &args[1]; borrows the path. We do not move it. We just look at it. (Chapter 5.)
  • fs::read_to_string(path) returns a Result<String, io::Error>. We cannot touch the string until we have said what to do on error. The match does exactly that. (Chapter 6.)
  • text.split_whitespace().count() is Rust’s iterator pattern in action. split_whitespace does not allocate a vector of words — it returns an iterator, which count walks once. Zero allocations, one pass, fast.
  • println! prints to stdout; eprintln! prints to stderr (where errors belong).

A smaller, nicer version with ?

Once we have a function that can return a Result, we can use ? to remove the match and let errors bubble up:

use std::env;
use std::fs;
use std::process;

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    let path = args.get(1).ok_or("usage: wordc <path>")?;

    let text = fs::read_to_string(path)?;
    let count = text.split_whitespace().count();

    println!("{path} has {count} words.");
    Ok(())
}

fn main() {
    if let Err(e) = run() {
        eprintln!("{e}");
        process::exit(1);
    }
}

This is the shape most production Rust programs end up in: a run() that returns Result, and a main() that is just error formatting and exit-code plumbing.

Ship it

Make the release binary:

cargo build --release
./target/release/wordc Cargo.toml

That file — target/release/wordc — is a native, statically-compiled binary. You can scp it to another Linux machine without Rust installed and it will work. That is the end state of every Rust program.

Plain English

You just wrote a real program, using every idea in Part 0: variables, bindings, borrowing, Option, Result, match. You compiled it to a native binary. If you stopped reading here, you could already use Rust at work for small tools.

Here is your CLI’s memory while it runs — everything in this picture is something you learned in the last six chapters.

Interactive simulation (requires JavaScript): the CLI collects its arguments into a heap-backed Vec, borrows the path argument, reads the file into an owned String via a Result and ?, and counts lines through a zero-copy iterator — every Part 0 concept in one running program.

## Try this
Extend wordc
  1. Add a --lines flag (check args.contains(&"--lines".to_string())) that prints the line count instead of the word count.
  2. Support multiple file paths: print one line per file.
  3. Replace the argument parsing with the clap crate. Look at the Cargo.toml to see how dependencies are added.

You are done with the Part 0 core. Let’s talk about where to go next.

Where to next →