RustArena

Step 3 of 6

Exit codes and stderr

Every program ends with an exit code, a small number it hands back to whoever started it. 0 means success, anything else means failure. Humans rarely see it, but everything else does: shell scripts (mytool && deploy), CI pipelines (a non-zero code fails the build), and other programs. A tool that prints "error" but exits with 0 tells the script everything went fine.

$ grep crab notes.txt; echo "exit code: $?"
the crab sleeps
exit code: 0
$ grep lobster notes.txt; echo "exit code: $?"
exit code: 1
$ grep lobster missing.txt; echo "exit code: $?"
grep: missing.txt: No such file or directory
exit code: 2

Common conventions: 0 success, 1 a general failure (or "nothing found", for search tools), 2 the tool was called wrong (bad arguments). Codes go from 0 to 255.

Returning an exit code from main

use std::process::ExitCode;

fn main() -> ExitCode {
    let words = ["alpha", "beta", "gamma"];
    for w in words {
        println!("{w}");
    }
    eprintln!("debug: printed {} words", words.len());
    ExitCode::SUCCESS
}
alpha
beta
gamma

Here is a typical main for a tool that can be called wrong:

use std::process::ExitCode;

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.is_empty() {
        eprintln!("usage: shout <word>...");
        return ExitCode::from(2);
    }
    println!("{}", args.join(" ").to_uppercase());
    ExitCode::SUCCESS
}
$ shout hello crab
HELLO CRAB
$ shout; echo $?
usage: shout <word>...
2

std::process::exit

std::process::exit(2) ends the program immediately from anywhere. It's handy in small tools, but it doesn't return: values in scope are never dropped (their Drop code doesn't run), and a function that might call it is hard to test. Prefer returning ExitCode (or a Result) up to main, and keep exit for the top level.

(Recall from the last module: a main that returns Err exits with 1, and a panic exits with 101.)

Common mistakes

Your task

Write average: print the average of the numeric arguments with two decimals. With no arguments, print usage: average <number>... to stderr and exit with code 2. If an argument isn't a number, print error: "x" is not a number to stderr and exit with code 1 (print nothing to stdout in both cases).

Challenge 2

Write contains <word> <text>... with grep's exit codes: if one of the text arguments equals word, print found at position N (N counts the text arguments from 1, first match) and exit 0. If none does, print nothing and exit 1. With fewer than two arguments, print usage: contains <word> <text>... to stderr and exit 2.

Challenge 3

Write temp, a converter: temp 100C prints 212.0F, temp -40F prints -40.0C (one decimal). Structure it the way real tools are built: a run(args: &[String]) -> Result<String, CliError> that does the work, and a main that prints the result or the error and picks the exit code. CliError::Usage (wrong number of arguments) exits 2 with usage: temp <value><C|F>; CliError::Invalid(String) exits 1 with error: <message>, for a unit other than C/F (unknown unit in "30K") or a value that isn't a number (bad number in "abcC"). Errors go to stderr.

Previous: Flags and options Next: Reading files