Building a Command-Line Application

Everything so far has been pieces — ownership, traits, error handling, modules. Now we put them together into the kind of thing you'd actually ship: a small command-line application. Building a real program reveals how Rust's features combine in practice — structuring the code, parsing arguments, handling errors gracefully with `Result` and `?`, separating logic from `main` for testability, and returning proper exit codes. It's where the language stops being a set of concepts and becomes a tool for building software.

This post assembles Module 4’s advanced features and the whole curriculum into building a command-line application — a small but real Rust program. It covers structuring a CLI app, parsing arguments, error handling (with Result and ?), separating logic from main (for testability), and exit codes. Rather than new features, it applies what you’ve learned to a real program — showing how Rust’s pieces combine. This is where the language becomes a practical tool.

Structuring a CLI application

A well-structured CLI app separates concerns — argument parsing, the core logic, and error handling — and keeps main thin:

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

// Configuration parsed from arguments.
struct Config {
    query: String,
    filename: String,
}

impl Config {
    // Parse args into a Config, returning a Result (errors are data).
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        Ok(Config {
            query: args[1].clone(),
            filename: args[2].clone(),
        })
    }
}

A well-structured CLI app separates argument parsing (into a validated config type, returning Result), core logic (a run function), and error handling — keeping main thin (just wiring). This structure is clear and testable. The core logic goes in a run function.

The run function and error handling

The core logic goes in a run function that returns a Result — so errors propagate (via ?) and are handled at the top, gracefully:

use std::error::Error;
use std::fs;

// Core logic in run(), returning Result so errors propagate with `?`.
fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(&config.filename)?; // `?` propagates errors

    for line in contents.lines() {
        if line.contains(&config.query) {
            println!("{line}");
        }
    }
    Ok(())
}

The core logic goes in a run function returning Result<(), Box<dyn Error>> (flexible error type) — so errors propagate via ? (cleanly, not panicking) to be handled at the top, and run being separate and fallible makes it testable. main then wires it together with proper error handling and exit codes.

Wiring main with exit codes

main ties it together — parsing args, running the logic, and handling errors with proper exit codes (so the program behaves correctly as a CLI tool):

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

    // Parse args; on error, print to stderr and exit with a non-zero code.
    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // Run the logic; on error, print to stderr and exit non-zero.
    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

main wires it together — parsing args, running the logic, and handling errors properly (printing to stderr via eprintln!, exiting with a non-zero code on failure) — making a well-behaved CLI tool that fits the Unix ecosystem (correct streams, exit codes, scriptable). This assembles the pieces into a real program.

Bringing it all together

Building this CLI app combines the whole curriculum — showing how Rust’s pieces work together in a real program:

Building a command-line application assembles the whole curriculum into a real program — separating parsing (config), core logic (a fallible run with ? and Box<dyn Error>), and top-level error handling (main with stderr and exit codes) — an idiomatic, testable structure that combines ownership, traits, error handling, iterators, modules, and testing. This is where Rust becomes a tool for building software. Next: the crate ecosystem and tooling.

Key takeaways

Further reading

Sources & References