Rust Command Reference
Every Rust project is built, tested, and shipped through a small set of command-line tools: rustup, cargo, and rustc. Once you understand what each one does and how they fit together, you can create a project, add dependencies, compile, test, format, lint, and publish code entirely from the terminal. This lesson is a complete reference to the commands you will use every day as a Rust developer, from your very first cargo new to running an optimized release build.
Overview: How the Rust Toolchain Fits Together
Rust’s command-line tooling has three layers, and it helps to think of them from the outside in.
rustup is the toolchain installer and version manager. It downloads and switches between Rust versions (stable, beta, nightly), installs cross-compilation targets, and installs extra components like clippy (the linter) and rustfmt (the formatter). You interact with rustup rarely — mostly when you first install Rust or want to update it.
rustc is the actual compiler. It reads one crate’s worth of .rs source files, checks that every type, ownership rule, and borrow is valid, and emits a compiled binary or library. Every other Rust tool eventually calls rustc under the hood — but you will rarely invoke it directly, because it has no idea what a dependency is.
cargo is the build tool and package manager, and it is the command you will use constantly. A cargo project is described by a Cargo.toml manifest (package name, version, and dependencies) and, once built, a Cargo.lock file that records the exact version of every dependency that was actually resolved, so builds are reproducible. Cargo reads these files, downloads dependencies from crates.io, figures out the correct order to compile everything in, and invokes rustc once per crate with the right flags. Source code for a binary project lives in src/main.rs; a library lives in src/lib.rs. Every build artifact — compiled binaries, intermediate object files, cached dependency builds — lands in a target/ directory, split into target/debug for normal builds and target/release for optimized ones.
A typical development loop looks like this: edit code, run cargo check for fast feedback (it type-checks and borrow-checks without generating a binary), run cargo build or cargo run once you want to actually execute something, run cargo test before committing, and run cargo fmt and cargo clippy to keep the code clean and idiomatic. Everything below walks through these commands with real, runnable examples.
Syntax
Cargo and rustc both follow simple, predictable invocation patterns:
cargo <command> [options]
cargo <command> [options] -- <args-for-your-program>
rustc [options] <input-file>
- cargo <command> — a cargo subcommand such as
build,run, ortest. - [options] — flags for cargo itself, such as
--release. - — <args> — everything after a bare
--is forwarded to your program’s ownargv, not interpreted by cargo. - rustc <input-file> — compiles a single source file directly, with no dependency resolution.
| Command | What it does |
|---|---|
cargo new NAME |
Creates a new binary project directory called NAME with a Cargo.toml, src/main.rs, and a .gitignore. |
cargo init |
Turns the current directory into a cargo project in place. |
cargo build |
Compiles the project and its dependencies into target/debug. |
cargo build --release |
Compiles with optimizations into target/release. |
cargo run |
Builds if needed, then runs the resulting binary. |
cargo run -- ARGS |
Runs the binary, forwarding ARGS as command-line arguments. |
cargo check |
Type-checks and borrow-checks the project without generating a binary — the fastest feedback loop. |
cargo test |
Compiles and runs every function annotated #[test]. |
cargo fmt |
Reformats source files to the standard Rust style. |
cargo clippy |
Runs the Clippy linter for extra style and correctness warnings beyond what rustc catches. |
cargo doc --open |
Builds HTML documentation from doc comments and opens it in a browser. |
cargo add CRATE |
Adds CRATE as a dependency in Cargo.toml. |
cargo remove CRATE |
Removes a dependency. |
cargo update |
Re-resolves dependency versions and rewrites Cargo.lock. |
cargo tree |
Prints the dependency graph. |
cargo publish |
Uploads a crate to crates.io. |
rustup update |
Updates installed Rust toolchains to the latest stable release. |
rustup default stable |
Sets stable as the default toolchain. |
rustup component add clippy rustfmt |
Installs the Clippy and rustfmt components. |
rustc --version |
Prints the compiler version. |
Examples
Example 1: Creating and running a project
cargo new hello_cli scaffolds a full project. Replace the generated src/main.rs with a program that greets whoever is named on the command line:
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
println!("Hello, {}!", args[1]);
} else {
println!("Hello, world!");
}
}
$ cargo new hello_cli
$ cd hello_cli
$ cargo run
Compiling hello_cli v0.1.0 (/path/to/hello_cli)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello_cli`
Hello, world!
$ cargo run -- Ferris
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/hello_cli Ferris`
Hello, Ferris!
std::env::args() returns every command-line argument, including the program’s own path as args[0]. With no extra argument, args.len() is 1, so the program falls back to "Hello, world!". Everything typed after -- on the cargo run line is passed straight through as additional entries in args, which is why cargo run -- Ferris makes args[1] equal to "Ferris".
Example 2: Testing with cargo test
Add a small function with its own unit test, so you can see cargo check, cargo build, and cargo test in action:
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::add;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}
fn main() {
println!("2 + 3 = {}", add(2, 3));
}
$ cargo test
Compiling hello_cli v0.1.0 (/path/to/hello_cli)
Finished test [unoptimized + debuginfo] target(s) in 0.55s
Running unittests src/main.rs (target/debug/deps/hello_cli-9f3b1c2)
running 1 test
test tests::adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The #[cfg(test)] attribute tells the compiler to include the tests module only when compiling for testing, so it adds no cost to a normal cargo build or cargo run. cargo test compiles the crate with testing enabled, links in a lightweight test harness, and runs every function tagged #[test], reporting pass or fail for each one. Running cargo check instead of cargo build here would catch a broken assert_eq! call’s types just as fast, without paying for full code generation — that speed difference is why cargo check is the tool you reach for while actively editing.
Example 3: Debug vs. release builds
The same project can be compiled two different ways:
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
$ cargo build --release
Finished release [optimized] target(s) in 1.10s
$ ls target/debug/hello_cli target/release/hello_cli
target/debug/hello_cli target/release/hello_cli
cargo build produces an unoptimized binary in target/debug with debug symbols, favoring fast compile times — ideal while developing. cargo build --release passes optimization flags to rustc, producing a binary in target/release that runs significantly faster but takes longer to compile. Use debug builds while iterating and release builds for anything you measure performance on or ship.
How It Works Step by Step
When you run cargo build (or anything that implies a build, like cargo run or cargo test), several things happen in order:
- Cargo reads
Cargo.tomland resolves the dependency graph, consultingCargo.lockfor exact versions (or creating/updating it if this is the first build or a dependency changed). - For each crate in that graph — every dependency, plus your own code — cargo invokes
rustcwith the correct flags: edition, optimization level, output path, and paths to already-compiled dependency artifacts. rustcparses the source into an abstract syntax tree, resolves every name, and runs full type checking and borrow checking. This is the stage where ownership and borrowing violations are caught — before any machine code is ever generated.- Once the program is known to be valid,
rustclowers it through internal representations (MIR, then LLVM IR) and hands it to LLVM, which optimizes and emits native machine code. - The linker combines your crate’s compiled code with its dependencies’ compiled code into a single executable, placed in
target/debugortarget/release. cargo runsimply executes that resulting binary and streams its stdout and stderr back to your terminal;cargo testdoes the same but against a binary built with the test harness linked in, and reports each#[test]function’s pass/fail result.
Common Mistakes
Mistake 1: Invoking rustc directly on a project with dependencies
It’s tempting to skip cargo and just run rustc on a source file, but rustc has no concept of crates.io or Cargo.toml:
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen_range(1..100);
println!("Random number: {}", n);
}
error[E0433]: failed to resolve: use of undeclared crate or module `rand`
--> main.rs:1:5
|
1 | use rand::Rng;
| ^^^^ use of undeclared crate or module `rand`
Running rustc main.rs here fails because rustc only knows about the standard library and whatever .rlib files you point it at manually — it never reads Cargo.toml. The fix is to let cargo manage the dependency: run cargo add rand to record it in Cargo.toml, then build with cargo build or cargo run, which downloads rand, compiles it, and links it in automatically.
Mistake 2: Forgetting mut
Rust variable bindings are immutable by default, so trying to mutate one without mut is a compile error, not a runtime surprise:
fn main() {
let count = 0;
count += 1;
println!("count = {}", count);
}
error[E0384]: cannot assign twice to immutable variable `count`
--> main.rs:3:5
|
2 | let count = 0;
| ----- first assignment to `count`
3 | count += 1;
| ^^^^^^^^^^ cannot assign twice to immutable variable
The fix is to declare the binding as mutable up front:
fn main() {
let mut count = 0;
count += 1;
println!("count = {}", count);
}
Because this is caught at compile time by cargo check or cargo build, you find out immediately rather than debugging an unexpected value later.
Other pitfalls to watch for
- Deleting or hand-editing
Cargo.lockfor a binary project. Commit it for binaries so every teammate and CI run gets identical dependency versions; libraries, by convention, leave it uncommitted so downstream crates can resolve their own compatible versions. - Benchmarking or measuring performance against a plain
cargo buildbinary. Debug builds skip optimizations entirely and can be many times slower than acargo build --releasebinary — always measure with--release.
Best Practices
- Run
cargo checkconstantly while editing; it is far faster thancargo buildbecause it stops before code generation. - Run
cargo fmtandcargo clippybefore every commit to keep formatting consistent and catch idiom issues rustc itself won’t flag. - Commit
Cargo.lockfor binary projects so builds are reproducible across machines and CI; omit it for libraries. - Always use
cargo build --release(orcargo run --release) before measuring performance — debug builds are unoptimized. - Use
cargo run -- argsto pass real input to your program instead of hardcoding test values in source. - Use
cargo doc --openearly and often to check that your own doc comments render the way you expect. - Keep
rustupup to date withrustup update, and pin a project’s toolchain with arust-toolchain.tomlfile when reproducibility across machines matters. - Never invoke
rustcdirectly on a project that has dependencies inCargo.toml— let cargo driverustcfor you.
Practice Exercises
- Create a new binary project called
greeterwithcargo new. Modifysrc/main.rsso it reads a name from the command line (as in Example 1), defaulting to"friend"instead of"world"when no argument is given. Confirm bothcargo runandcargo run -- Ferrisproduce the output you expect. - Add a second function,
multiply(a: i32, b: i32) -> i32, to the project from Example 2, along with its own#[test]function. Runcargo testand confirm both tests pass. - Run
cargo buildand thencargo build --releaseon the same project. Compare how long each takes and the size of the resulting binary intarget/debugversustarget/release(hint: usels -lh).
Summary
rustupmanages Rust toolchains and components;cargomanages projects, dependencies, and builds;rustcis the compiler both ultimately invoke.cargo new/cargo initscaffold a project;cargo build/run/checkcompile it;cargo testruns its tests;cargo fmt/clippykeep it clean.cargo checkis the fastest feedback loop because it stops before code generation — use it while editing.- Debug builds (
target/debug) are unoptimized and fast to compile; release builds (target/release, via--release) are optimized and fast to run. - Everything after
--on acargo runline is passed straight through to your program’s ownargv, not interpreted by cargo. - Never call
rustcdirectly on a project with crates.io dependencies — it has no concept ofCargo.tomlor dependency resolution. - Commit
Cargo.lockfor binary projects so builds are reproducible across machines.
