RustArena

Step 6 of 6

Putting it together: Config, run() and clap

Every well-built Rust CLI ends up with the same shape:

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let command = match parse_args(&args) {      // 1. arguments -> a typed Config/Command
        Ok(c) => c,
        Err(msg) => {
            eprintln!("{msg}\n{USAGE}");
            return ExitCode::from(2);            //    usage errors: exit 2
        }
    };
    match run(command) {                         // 2. do the work, returning Result
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("error: {e}");             // 3. report on stderr
            ExitCode::FAILURE                    //    failures: exit 1
        }
    }
}

clap: what teams use for real

Hand-written parsing is fine for small tools. For anything bigger, teams use the clap crate. You describe the arguments as a struct, and #[derive(Parser)] generates the parser, --help, --version, clear error messages (including suggestions for misspelled flags), and exit code 2 for usage errors. clap isn't available on this site, so here's what it looks like (not compiled):

[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::{Parser, Subcommand};

/// A tiny todo list
#[derive(Parser)]
#[command(version)]
struct Cli {
    /// File that stores the tasks
    #[arg(short, long, default_value = "todo.txt")]
    file: String,

    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Show all tasks
    List,
    /// Add a task
    Add { text: Vec<String> },
    /// Mark task N as done
    Done { n: usize },
}

fn main() {
    let cli = Cli::parse(); // on bad arguments: prints the error and exits with 2
    match cli.command {
        Command::List => { /* ... */ }
        Command::Add { text } => { /* ... */ }
        Command::Done { n } => { /* ... */ }
    }
}

The /// doc comments become the help text:

$ todo --help
A tiny todo list

Usage: todo [OPTIONS] <COMMAND>

Commands:
  list  Show all tasks
  add   Add a task
  done  Mark task N as done
  help  Print this message or the help of the given subcommand(s)

Options:
  -f, --file <FILE>  File that stores the tasks [default: todo.txt]
  -h, --help         Print help
  -V, --version      Print version

Notice it's the same design as this module's: a typed struct, an enum of subcommands, match in main. The Pro Track builds a complete CLI with clap, anyhow and integration tests; everything you practiced here carries over directly.

Common mistakes

Your task

Build todo, a todo list stored in todo.txt, one task per line written as [ ] text or [x] text (done). Commands:

Challenge 2

Test the hard part without files: write fn mark_done(contents: &str, n: usize) -> Result<String, String> that takes the whole todo.txt text and returns the new text with task n (1-based) marked [x], each line ending in \n. A task that's already done stays done. A missing task gives Err("no task 7").

Challenge 3

Model the arguments the way clap does: write fn parse_command(args: &[String]) -> Result<Command, String> for a key-value store CLI with subcommands get <key>, set <key> <value>, del <key>... (one or more keys) and list [--limit N]. Errors: no subcommand gives missing command; an unknown one gives unknown command "x"; anything else wrong with a subcommand's arguments gives usage: <subcommand> ... with that subcommand's usage from the list below.

Previous: Reading stdin when no file is given