Arguments and exit codes

A command line program reads its arguments from std::env::args, which yields the program name first and the rest after it. Every argument is a string, and the program decides what a missing one means, what an unknown one means, and which status to leave behind.

This lesson reads a name and a count, prints a usage message with a non-zero status, parses options with values, dispatches a subcommand, and ends with the compiler refusing to index the argument iterator.

env::args

args() is an iterator, and the first item is the program path, so a program that wants its own arguments skips one. next returns Option<String>, and the defaults for a missing or unparsable value come from unwrap_or and and_then.

The program greets a name a number of times. Without arguments it greets the world once.

use std::env; fn main() { let mut arguments = env::args().skip(1); let name = arguments.next().unwrap_or_else(|| String::from("world")); let times: u32 = arguments .next() .and_then(|value| value.parse().ok()) .unwrap_or(1); for _ in 0..times { println!("hello {name}"); } }

rustc --edition 2024 args.rs
./args rust 2
./args

hello rust
hello rust
hello world

The second run has no arguments at all, and the two defaults produce one greeting. An argument that is not a number is treated like a missing one by and_then and ok, which is a decision the program makes and not a rule of the library. The lesson on Option is the combinators.

A usage message and an exit code

A wrong invocation should say what the program wants and leave a status the shell can see. main may return an ExitCode, and ExitCode::from(2) is the conventional status for a usage error.

The program adds two numbers. It prints the usage to the standard error when there are no arguments, a different message for one argument, and a third for a value that is not a number.

use std::env; use std::process::ExitCode; fn main() -> ExitCode { let arguments: Vec<String> = env::args().skip(1).collect(); if arguments.is_empty() { eprintln!("usage: add NUMBER NUMBER"); return ExitCode::from(2); } if arguments.len() != 2 { eprintln!("expected two numbers, got {}", arguments.len()); return ExitCode::from(2); } match (arguments[0].parse::<i32>(), arguments[1].parse::<i32>()) { (Ok(left), Ok(right)) => { println!("{}", left + right); ExitCode::SUCCESS } _ => { eprintln!("both arguments must be numbers"); ExitCode::from(2) } } }

rustc --edition 2024 usage.rs
./usage 20 22
echo "status: $?"
./usage
echo "status: $?"
./usage x 1
echo "status: $?"

42
status: 0
usage: add NUMBER NUMBER
status: 2
both arguments must be numbers
status: 2

The messages go to the standard error and the numbers to the standard output, which is what lets a pipeline keep the result and still show the complaint. The status of the process is the value main returns, and the lesson on panics contrasts it with the 101 of a crash.

A small option parser

An option with a value is two arguments, and the loop reads the value with the same iterator that produced the flag. A flag without a value sets a boolean, and anything the program does not know is reported instead of ignored silently.

The program reads -n with a number and -o with a file name, and prints the two settings after the loop. The extra word is reported.

use std::env; fn main() { let mut count = 1; let mut file = String::from("out.txt"); let mut arguments = env::args().skip(1); while let Some(argument) = arguments.next() { match argument.as_str() { "-n" => count = arguments.next().and_then(|value| value.parse().ok()).unwrap_or(1), "-o" => file = arguments.next().unwrap_or(file), other => println!("ignored: {other}"), } } println!("count {count} file {file}"); }

rustc --edition 2024 opts.rs
./opts -n 3 -o data.txt extra

ignored: extra
count 3 file data.txt

The value is read from the same iterator, so the loop continues after it without a second index. The unwrap_or(file) moves the old string out and keeps it when the option has no value, which is valid because the match arm owns the field. A real program uses the clap crate for this, and the loop above is what it generates.

An argument is not always valid Unicode. The operating system hands over bytes, and env::args panics on a value that is not Unicode while env::args_os gives the bytes as an OsString. A program that passes a file name through untouched uses the second and converts only the arguments it has to parse.

Subcommands

A tool with several modes reads the first argument as a command and the rest as its arguments. A match over Option<&str> covers the known commands, the unknown one and the empty command line in one place.

The program sums the numbers after sum and counts the rest after count. An unknown command exits with status two.

use std::env; use std::process::ExitCode; fn main() -> ExitCode { let mut arguments = env::args().skip(1); match arguments.next().as_deref() { Some("sum") => { let total: i32 = arguments.filter_map(|value| value.parse::<i32>().ok()).sum(); println!("{total}"); ExitCode::SUCCESS } Some("count") => { println!("{}", arguments.count()); ExitCode::SUCCESS } Some(other) => { eprintln!("unknown command: {other}"); ExitCode::from(2) } None => { eprintln!("usage: tool <sum|count> [values]"); ExitCode::from(2) } } }

rustc --edition 2024 sub.rs
./sub sum 1 2 3
./sub count a b
./sub nope
echo "status: $?"

6
2
unknown command: nope
status: 2

as_deref turns the Option<String> into an Option<&str> so the match arms can be string literals, which is the difference between matching a value and matching a pattern. The lesson on patterns covers the match itself.

The argument list is not an array

env::args returns an iterator and not a slice, so indexing it is a compile error. The compiler reports E0608 and the fix is to collect or to use nth.

The program indexes the iterator for the first argument. The compiler points at the brackets.

fn main() { let first = std::env::args()[0]; println!("{first}"); }

rustc --edition 2024 index_err.rs

error[E0608]: cannot index into a value of type `Args`
--> index_err.rs:2:33
<|>br>
2 | let first = std::env::args()[0];
| ^^^

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0608`.

The value is a stream that ends, and an index would have to decide what happens past the end. Collecting into a Vec gives the slice and the length check that the previous sections use. The next lesson reads configuration from the environment instead of the command line.

Exercises

  1. Add a --verbose flag to the option parser and print one extra line when it is set.
  2. Make the subcommand program read its numbers from a file named after the command.
  3. Print the program name from args and use it in the usage message.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is the first item of env::args()?

    Answer

    The path of the program

    The program name comes first, which is why programs skip it.

  2. What is the type of a command line argument?

    Answer

    String

    The operating system hands over bytes, and the library gives a String for each.

  3. Which exit status means a usage error by convention?

    Answer

    2

    Two is the common status for a wrong invocation, and one is for a failed operation.

  4. How does an option with a value read the value?

    Answer

    From the same iterator that produced the option

    The iterator keeps the position, so next after the flag is the value.

  5. What does this program print? fn main() { let line = "alpha beta"; println!("{}", line.split_whitespace().count()); }

    Answer

    2

    The two words are two items for the iterator, and count consumes it.

  6. What does this program print? fn main() { let values = ["5", "7"]; let total: i32 = values.iter().filter_map(|value| value.parse::<i32>().ok()).sum(); println!("{total}"); }

    Answer

    12

    The two parsed numbers are added by the sum adaptor.

  7. What does this program print? fn main() { let values = ["1", "x", "3"]; let total: i32 = values.iter().filter_map(|value| value.parse::<i32>().ok()).sum(); println!("{total}"); }

    Answer

    4

    The value that does not parse is dropped, and one and three are added.

  8. What does this program print? fn main() { let command = Some("sum"); match command { Some("sum") => println!("adding"), Some(other) => println!("unknown {other}"), None => println!("usage"), } }

    Answer

    adding

    The first arm matches the string and prints its message.

  9. What is the first line of the compiler output for this program? fn main() { let first = std::env::args()[0]; println!("{first}"); }

    Answer

    error[E0608]: cannot index into a value of type Args

    The argument list is an iterator, and an iterator has no index.

  10. What does this program print? fn main() { let value: Option<u32> = "abc".parse().ok(); println!("{}", value.unwrap_or(1)); }

    Answer

    1

    The parse fails, the Option is None, and the default of one is printed.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev