Skip to content
Select theme

How to pass arguments to a Rust program with cargo run

Short answer:

Terminal window
cargo run -- hello world

Everything 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.

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:

Terminal window
cargo run hello world
[
"target/debug/args1",
"hello",
"world",
]

What breaks without -- is an argument that looks like a Cargo flag:

Terminal window
cargo run -- --hello # your program receives "--hello"
cargo run --hello # cargo rejects it
error: 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:

Terminal window
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.

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{args:#?}");
}
Terminal window
cargo run -- hello world

The 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:

Terminal window
./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.

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 -- Ada
hello, Ada
$ echo $?
0
$ cargo run -q -- Ada Grace Alan
hello, 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.

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.

"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 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.

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.