cargo run is the primary tool for building and executing Rust projects, but its ability to pass arguments to your binary is often overlooked, leading to confusion when expected input doesn’t reach the program.

Let’s see it in action. Imagine you have a simple Rust program, src/main.rs, that expects two integer arguments:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 3 {
        println!("Usage: {} <num1> <num2>", args[0]);
        return;
    }

    let num1: i32 = args[1].parse().expect("Invalid number for num1");
    let num2: i32 = args[2].parse().expect("Invalid number for num2");

    println!("The sum of {} and {} is: {}", num1, num2, num1 + num2);
}

If you try to run this with cargo run, you’ll see the usage message:

$ cargo run
   Compiling hello_world v0.1.0 (/path/to/your/project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.5s
     Running `target/debug/hello_world`
Usage: target/debug/hello_world <num1> <num2>

This happens because cargo run itself consumes any arguments provided immediately after cargo run. It doesn’t know they are intended for your binary.

To pass arguments to your Rust binary when using cargo run, you need to use the -- separator. Everything after -- is passed directly to your compiled executable.

Let’s try again, this time with the separator:

$ cargo run -- 10 20
   Compiling hello_world v0.1.0 (/path/to/your/project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.5s
     Running `target/debug/hello_world 10 20`
The sum of 10 and 20 is: 30

This works because cargo run sees -- and understands that all subsequent arguments (10 and 20 in this case) should be forwarded to the target/debug/hello_world executable. Your program then receives them as env::args().

The mental model for cargo run with arguments is:

cargo run [CARGO OPTIONS] -- [YOUR PROGRAM ARGUMENTS]

  • cargo run: The command to build and execute your Rust project.
  • [CARGO OPTIONS]: Standard Cargo flags like --release for optimized builds, --target for cross-compilation, etc. These are processed by cargo itself.
  • --: The crucial separator. It tells cargo to stop parsing options for itself and to pass the rest of the line as arguments to the program being run.
  • [YOUR PROGRAM ARGUMENTS]: Any values or flags that your actual Rust code expects to receive via std::env::args().

Consider a scenario where you want to run in release mode and pass arguments. You must put -- after --release if you want --release to be interpreted by cargo and not your program:

$ cargo run --release -- --config production.toml --verbose
   Compiling hello_world v0.1.0 (/path/to/your/project)
    Finished release [optimized] target(s) in 5.2s
     Running `target/release/hello_world --config production.toml --verbose`

If you mistakenly put -- before --release, cargo would pass --release as an argument to your program, which would likely cause a parsing error or unexpected behavior, and the build would remain unoptimized.

The most surprising thing is that cargo doesn’t have a more explicit way to distinguish between its own arguments and the program’s arguments without the -- separator. If you try to pass an argument that looks like a Cargo option (e.g., --help) directly after cargo run without --, cargo will attempt to interpret it, leading to errors like error: unknown argument: –help``. This reinforces the necessity of the -- delimiter for any arguments intended for your application.

The next concept to explore is how to handle more complex argument parsing within your Rust binary using crates like clap or structopt.

Want structured learning?

Take the full Cargo course →