How to pass arguments to a Rust program with cargo run
Short answer:
cargo run -- hello worldEverything after -- goes to your program. std::env::args() gives you those values as strings,
starting with the path to the binary itself.
The examples use a project called args1, so that is the name you will see in the first line of the
output. Make yours with cargo new args1, or read the name that belongs to yours.
Why the double dash
Section titled “Why the double dash”cargo run is itself a program with its own flags — --release, --quiet, --bin, --features.
The -- marks the point where Cargo stops reading and your arguments begin.
The rule is narrower than “always use --”. Cargo forwards trailing arguments that it does not
recognise, so this works too:
cargo run hello world[ "target/debug/args1", "hello", "world",]What breaks without -- is an argument that looks like a Cargo flag:
cargo run -- --hello # your program receives "--hello"cargo run --hello # cargo rejects iterror: unexpected argument '--hello' found
tip: a similar argument exists: '--help' tip: to pass '--hello' as a value, use '-- --hello'
Usage: cargo run --help [ARGS]...
For more information, try '--help'.And the dangerous case, where Cargo accepts the flag silently and your program never sees it:
cargo run --release[ "target/release/args1",]Cargo built a release binary and passed nothing. If your program has a --release mode of its own,
you now have a bug that only shows up when someone tries to use it.
Use -- every time. It costs one token and removes the entire class of problem.
Reading the arguments
Section titled “Reading the arguments”use std::env;
fn main() { let args: Vec<String> = env::args().collect();
println!("{args:#?}");}cargo run -- hello worldThe first element is the program path, and it is not what you think:
[ "target/debug/args1", "hello", "world",]That is the path under cargo run, but if you execute the binary directly it is whatever you typed:
./target/debug/args1 alpha beta[ "./target/debug/args1", "alpha", "beta",]Nothing guarantees this is a full path, or even a valid one — a program launched through a symlink or
with exec -a gets whatever the caller chose. So never compare it against a literal, and almost
always drop it.
A pattern you can actually use
Section titled “A pattern you can actually use”use std::env;
fn main() { let args: Vec<String> = env::args().skip(1).collect();
let Some(name) = args.first() else { eprintln!("usage: args1 <name>"); std::process::exit(1); };
println!("hello, {name}");
if args.len() > 1 { println!("(ignoring {} extra argument(s))", args.len() - 1); }}$ cargo run -q --usage: args1 <name>$ echo $?1
$ cargo run -q -- Adahello, Ada$ echo $?0
$ cargo run -q -- Ada Grace Alanhello, Ada(ignoring 2 extra argument(s))Four things worth copying here. skip(1) drops the program name. first() returns an Option, so
the missing-argument case is a type the compiler makes you handle. The usage message goes to
eprintln!, not println!, so it survives output redirection. And a failure exits with a non-zero
code, which is how every shell script and CI job detects that it failed.
Arguments that are not valid UTF-8
Section titled “Arguments that are not valid UTF-8”env::args() yields String, so it panics if an argument is not valid Unicode. On Linux this is
reachable — a filename with a stray byte passed as an argument will crash your program. On Windows
and macOS argv is UTF-16 and UTF-8 respectively, so it does not come up.
use std::ffi::OsString;use std::os::unix::ffi::OsStringExt;
fn main() { // What an argument looks like when a Linux filename contains an invalid UTF-8 byte. let arg = OsString::from_vec(vec![0x66, 0x6F, 0x6F, 0xFF, 0x2E, 0x74, 0x78, 0x74]);
println!("{:?}", arg.to_string_lossy());}"foo�.txt"That is to_string_lossy: the invalid byte becomes the replacement character instead of an error.
Note the trade-off — you get a string back, but it no longer names the file on disk.
If that matters, use env::args_os(), which yields OsString, and pass it straight to std::fs. The
file APIs take OsStr for exactly this reason, so you never have to convert:
use std::env;
fn main() { for arg in env::args_os().skip(1) { println!("{:?}", arg.to_string_lossy()); }}Use args_os when the argument is a path. Use args when it is a value — a number, a flag, a
subcommand — where a non-Unicode argument is a user error worth failing on.
When to stop doing this by hand
Section titled “When to stop doing this by hand”An if chain over args stops scaling around the third flag. Once you need -v, --output <path>
and a subcommand, the hand-written parser is longer than the library and wrong about -abc versus
-a -b -c and --flag=value versus --flag value. That is what clap is for; it also generates
--help. You do not need it for a script that takes one filename.
- Parsing arguments with clap — the same program, with the parsing done for you
- Back to Building a command-line tool