Command-Line Arguments
Command-line arguments are the sequence of string tokens passed by the operating system or shell to a program when it starts. In Rust, std::env::args() returns an iterator over these arguments as owned String values, starting at index 0 with the program invocation path followed by user-supplied flags and positional arguments.
Shell invocation: ./backup --compress -o /tmp/out.tar src/ docs/
Index: 0 1 2 3 4 5
+------------+--------------+------+--------------+------+-------+
argv: | "./backup" | "--compress" | "-o" | "/tmp/out.tar| "src/"|"docs/"|
+------------+--------------+------+--------------+------+-------+
Program Boolean Flag Opt Option Value Positional Args
Inspecting Command-Line Arguments
Collecting env::args() into a Vec<String> allows random access by index and slice pattern matching.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("Total arguments (including program name): {}", args.len());
for (index, arg) in args.iter().enumerate() {
println!(" arg[{index}] = {arg:?}");
}
}
Total arguments (including program name): 1
arg[0] = "target/debug/app"
Note that arg[0] is controlled by the caller invoking execve and is not guaranteed to be a valid path—or even present if an empty argument vector was passed via Command. Always use .get(0) rather than unchecked indexing if args could be empty.
State-Machine Argument Parsing with an Iterator
For lightweight command-line tools that do not require an external crate, consuming an iterator directly with while let Some(arg) = iter.next() cleanly handles flags (--verbose), key-value options (--port 8080 or --port=8080), and positional arguments.
#[derive(Debug, Default)]
struct Config {
verbose: bool,
port: u16,
inputs: Vec<String>,
}
fn parse_args<I>(mut args: I) -> Result<Config, String>
where
I: Iterator<Item = String>,
{
let mut config = Config {
verbose: false,
port: 8080,
inputs: Vec::new(),
};
while let Some(arg) = args.next() {
match arg.as_str() {
"-v" | "--verbose" => config.verbose = true,
"-p" | "--port" => {
let raw = args
.next()
.ok_or_else(|| "missing value for --port".to_string())?;
config.port = raw
.parse::<u16>()
.map_err(|_| format!("invalid port number: {raw}"))?;
}
s if s.starts_with("--port=") => {
let raw = &s["--port=".len()..];
config.port = raw
.parse::<u16>()
.map_err(|_| format!("invalid port number: {raw}"))?;
}
"--" => {
// Everything after "--" is treated as a positional argument
config.inputs.extend(args);
break;
}
s if s.starts_with('-') => {
return Err(format!("unknown flag: {s}"));
}
positional => config.inputs.push(positional.to_string()),
}
}
Ok(config)
}
fn main() {
let simulated = vec![
"--verbose".to_string(),
"--port=9090".to_string(),
"input.txt".to_string(),
"--".to_string(),
"-literal-dash-file.txt".to_string(),
];
let config = parse_args(simulated.into_iter()).unwrap();
println!("{config:?}");
}
Config { verbose: true, port: 9090, inputs: ["input.txt", "-literal-dash-file.txt"] }
Subcommand Dispatch with Slice Patterns
Converting Vec<String> references to a slice of &str enables ergonomic subcommand routing with match:
fn dispatch(args: &[String]) -> Result<String, &'static str> {
let str_args: Vec<&str> = args.iter().map(String::as_str).collect();
match str_args.as_slice() {
["init", name] => Ok(format!("Initializing repository '{name}'")),
["remote", "add", alias, url] => {
Ok(format!("Added remote '{alias}' -> {url}"))
}
["status"] => Ok("Working tree clean".to_string()),
_ => Err("Usage: tool <init <name> | remote add <alias> <url> | status>"),
}
}
fn main() {
let cmd = vec![
"remote".to_string(),
"add".to_string(),
"origin".to_string(),
"https://example.com/repo.git".to_string(),
];
println!("{}", dispatch(&cmd).unwrap());
}
Added remote 'origin' -> https://example.com/repo.git