Command-Line Args
Reviewed & published by Brayan K
Command-line arguments are the values a user types after a program's name, read in Rust through std::env::args() as an iterator of Strings — with the program's own name always sitting at index 0.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll collect arguments into a Vec<String>, skip the program name, parse arguments into numbers with parse, read environment variables with env::var, and build a small argument-driven program.
What You'll Learn in This Lesson
- Reading arguments with std::env::args()
- Why args[0] is the program name
- Collecting args into a Vec<String>
- Parsing arguments into numbers with parse
- Reading env vars with env::var
- Building a tiny arg-driven program
1️⃣ Reading args() and the Program Name
std::env::args() gives an iterator of Strings; .collect() turns it into a Vec<String> you can index and count. Remember that args[0] is always the program's own name, so your real arguments begin at args[1]. A common idiom is .skip(1) to step over the program name.
// std::env::args() yields the command-line arguments as an iterator of Strings.
// The FIRST item (index 0) is the program's own name/path, not a real argument;
// your actual arguments start at index 1.
//
// The Rust Playground can't pass real args, so to get reproducible output we
// SIMULATE them by building a Vec<String> exactly like args().collect() would.
fn main() {
// In a real program this line would be:
// let args: Vec<String> = std::env::args().collect();
let args: Vec<String> = vec![
String::from("greeter"), // args[0] = the program name
String::from("hello"), // args[1] = first real argument
String::from("world"), // args[2] = second real argument
];
println!("program name (args[0]): {}", args[0]);
println!("number of args (incl. program): {}", args.len());
// Print just the real arguments (everything after the program name).
println!("real arguments:");
for (i, arg) in args.iter().skip(1).enumerate() {
println!(" arg {} = {}", i + 1, arg);
}
}We simulate the arguments here for reproducibility; in a real program the first line would be let args: Vec<String> = std::env::args().collect(); and everything else stays the same.
2️⃣ Parsing Arguments Into Numbers
Arguments are always text, so to compute with them you parse() the String into a number, annotating the target type. parse returns a Result because the text might not be a valid number — handle it with match for a clear error, or unwrap_or for a default.
// Arguments always arrive as Strings. To do math you must parse() them into a
// number, which returns a Result you handle (here with a match).
fn main() {
// Simulated args: program name, then two numbers as text.
let args: Vec<String> = vec![
String::from("adder"),
String::from("12"),
String::from("30"),
];
// Parse args[1] and args[2] from &str into i64.
let a: i64 = match args[1].parse() {
Ok(n) => n,
Err(_) => {
println!("'{}' is not a number", args[1]);
return;
}
};
let b: i64 = args[2].parse().unwrap_or(0);
println!("a = {}", a);
println!("b = {}", b);
println!("{} + {} = {}", a, b, a + b);
}3️⃣ Environment Variables with env::var
Beyond positional arguments, std::env::var(name) reads a named environment variable from the shell, returning a Result: Ok(value) if set, Err if not. Pair it with unwrap_or_else to supply a sensible default for missing variables.
use std::env;
// std::env::var(name) reads an environment variable, returning a Result:
// Ok(value) if set, Err(_) if not. Use unwrap_or to supply a fallback.
fn main() {
// Set a variable from within the program so the example is reproducible.
// (Normally it's set in your shell, e.g. `APP_MODE=debug cargo run`.)
env::set_var("APP_MODE", "debug");
// Read it back with a fallback for the case where it's missing.
let mode = env::var("APP_MODE").unwrap_or_else(|_| String::from("release"));
println!("APP_MODE = {}", mode);
// A variable we did NOT set falls back to the default.
let level = env::var("LOG_LEVEL").unwrap_or_else(|_| String::from("info"));
println!("LOG_LEVEL = {}", level);
// You can also match on the Result directly.
match env::var("APP_MODE") {
Ok(v) => println!("APP_MODE is set to '{}'", v),
Err(_) => println!("APP_MODE is not set"),
}
}We call set_var here only to make the example self-contained; in real use you'd set APP_MODE in your shell before running. The unset LOG_LEVEL falls back to its default "info".
Your turn. Fill in the blanks marked ____, then run it.
// 🎯 YOUR TURN — parse the two argument strings and multiply them.
fn main() {
// Simulated args: program name + two number strings.
let args: Vec<String> = vec![
String::from("multiplier"),
String::from("6"),
String::from("7"),
];
// Parse both real args to i64 (default to 1 if parsing fails).
let a: i64 = args[1].____().unwrap_or(1); // 👉 the string-to-number method
let b: i64 = args[2].____().unwrap_or(1);
println!("{} * {} = {}", a, b, a * b);
}
// ✅ Expected output:
// 6 * 7 = 42Mini-Challenge: A Tiny CLI Calculator
Build an argument-driven calculator that reads an operator and two numbers, then prints the result. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: a tiny arg-driven calculator.
//
// 1. Simulate a command line as a Vec<String>: [program, op, x, y].
// Use "calc", "add", "8", "5".
// 2. Take op = &args[1], then parse args[2] and args[3] into f64
// (use .parse().unwrap_or(0.0)).
// 3. match op.as_str() over "add"/"sub"/"mul"/"div" to compute a result;
// for anything else, print "unknown op: <op>" and return.
// 4. Print the result as: <x> <op> <y> = <result>.
//
// ✅ Example output:
// 8 add 5 = 13
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ index out of bounds: the len is 1 but the index is 1 — you read args[1] with no real arguments given. ✅ Fix: check args.len() first.
- ❌ type annotations needed on parse() — the compiler can't tell which number type. ✅ Fix: annotate, e.g. let n: i64 = s.parse()....
- ❌ Program crashes on bad input — a bare .unwrap() on a failed parse. ✅ Fix: use unwrap_or or match.
- ❌ Forgetting that args[0] is the program — off-by-one indexing. ✅ Fix: start real args at index 1, or .skip(1).
Pro Tips
- 💡 Pass real args locally with cargo run -- foo bar — everything after -- goes to your program.
- 💡 Validate before indexing: check args.len() and print a usage message if arguments are missing.
- 💡 Reach for clap once you need flags, subcommands, and help text — but the standard library is plenty for simple tools.
📋 Quick Reference — std::env
| Call | Gives |
|---|---|
| env::args() | iterator of arg Strings |
| .collect::<Vec<String>>() | all args in a Vec |
| args[0] | the program name |
| s.parse::<i64>() | String to number (Result) |
| env::var("NAME") | an env variable (Result) |
🎉 Lesson Complete!
- ✅ env::args() yields the arguments; .collect() makes a Vec<String>
- ✅ args[0] is the program name — real args start at index 1
- ✅ parse() turns an argument String into a number (a Result)
- ✅ env::var(name) reads an environment variable, with a fallback for missing ones
- ✅ Pass real arguments locally with cargo run -- a b c
Practice quiz
What type does std::env::args() yield for each item?
- i32
- &str
- String
- char
Answer: String. args() is an iterator of String, usually collected into a Vec<String>.
Where do a program's real (user-typed) arguments begin?
- Index 1
- Index 0
- Index 2
- The last index
Answer: Index 1. Index 0 is the program name, so the real arguments start at index 1.
What does this print? let args = vec![String::from("greeter"), String::from("hello"), String::from("world")]; println!("{}", args.len());
- 1
- 2
- 0
- 3
Answer: 3. len() counts all three entries, including the program name at index 0.
A common idiom to skip the program name when iterating arguments is...
- .take(1)
- .skip(1)
- .rev()
- .filter(0)
Answer: .skip(1). args.iter().skip(1) steps over the program name to reach the real arguments.
Why does parse() return a Result?
- It is always fallible because the text may not be a valid number
- It never fails
- Because it sorts the input
- Because strings are immutable
Answer: It is always fallible because the text may not be a valid number. parse can fail when the string is not a valid number, so it returns a Result you handle.
What does this print? let a: i64 = "12".parse().unwrap(); let b: i64 = "30".parse().unwrap_or(0); println!("{}", a + b);
- 1230
- 0
- 42
- It does not compile
Answer: 42. Both parse to integers, and 12 + 30 is 42.
What determines which numeric type parse() produces?
- The first character
- The type annotation on the binding
- The length of the string
- It always returns i32
Answer: The type annotation on the binding. The annotated target type (e.g. let a: i64) drives which number parse produces.
What does std::env::var(name) return?
- Always a String
- An iterator
- A bool
- A Result: Ok(value) if set, Err if not
Answer: A Result: Ok(value) if set, Err if not. var returns a Result because the environment variable might not be set.
How do you supply a fallback for a missing environment variable?
- env::var(name).panic()
- env::var(name).unwrap_or_else(|_| default)
- env::var(name).skip()
- env::var(name).len()
Answer: env::var(name).unwrap_or_else(|_| default). unwrap_or_else (or unwrap_or) provides a default when the variable is absent.
What is the difference between args() and env::var()?
- No difference
- args() reads files; env::var() reads stdin
- args() reads positional arguments; env::var() reads a named environment variable
- args() is for numbers only
Answer: args() reads positional arguments; env::var() reads a named environment variable. Arguments are positional inputs to one run; env vars are named configuration from the shell.
Continue this course
- Previous: File I/O (std::fs)
- Next: Checkpoint: Traits & Generics — Build a shapes program with traits and trait objects — then a quiz
- Quick reference: Rust cheat sheet
Frequently asked questions
How do I read command-line arguments in Rust?
Call std::env::args(), which returns an iterator over the program's arguments as Strings. The most common pattern is to collect it into a vector with let args: Vec<String> = std::env::args().collect(), which lets you index and count the arguments. You then read args[1], args[2], and so on for the real arguments. Each value is owned, so you can keep it around freely. For programs with many flags and options, third-party crates like clap build on this, but the standard library is all you need for simple tools.
Why is the first argument the program name?
By long-standing operating-system convention, the first entry of a program's argument list (index 0) is the path or name used to invoke the program itself, not something the user typed as an option. So if you run a tool as myapp hello world, args[0] is the program name, args[1] is "hello", and args[2] is "world". This means your real arguments always start at index 1, and args.len() counts the program name too. A common idiom is args.iter().skip(1) to iterate only the real arguments.
How do I turn a string argument into a number?
Every argument arrives as a String, so you convert it with the parse method, annotating the target type: let n: i64 = some_arg.parse().unwrap_or(0), or handle the Result with a match for a clear error message. parse returns Result because the text might not be a valid number, and it can produce many numeric types (i32, u64, f64, and more) depending on the type you ask for. Always decide what should happen on bad input — a default with unwrap_or, or an error message and early return.
What is the difference between args() and env::var()?
env::args() reads the positional arguments the user typed after the program name, like filenames or numbers. env::var(name) reads a named environment variable from the surrounding shell session, such as PATH or a custom APP_MODE, and returns a Result because the variable might not be set. Use arguments for the direct inputs to a single run, and environment variables for configuration that tends to persist across runs or is shared by many programs. Both live in the std::env module.