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
fn main() -> ExitCodeletsmainchoose the code:ExitCode::SUCCESS(0),ExitCode::FAILURE(1), or anyu8withExitCode::from(2).- The
debug:line isn't in the output above becauseeprintln!writes to standard error (stderr), a second output stream. Messages for humans (errors, warnings, progress) go to stderr; the tool's real results go to stdout. That waymytool > results.txtputs only results in the file while errors still appear on screen, and a pipe likemytool | sortdoesn't sort your error messages.
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
- Errors on stdout with
println!: they end up in the output file or the next program in the pipe. - Exiting with 0 after an error. Check: does
mytool bad-input && echo okprintok? It shouldn't. exit(256): codes are taken modulo 256 on Unix, so 256 becomes 0, which means success.ExitCode::fromtakes au8, which avoids that.
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.