Skip to content
Select theme

Setting up a Rust CLI project

Step 2 of 12. Step 1 mapped where hit, the command-line HTTP client this track builds, ends up. This step creates the project every later step grows from. By the end you will have a binary called hit that compiles and runs, and you will have added and removed a dependency once — so when later steps edit Cargo.toml, nothing there is a mystery.

Everything here was captured on Rust 1.99.0, the version the whole track is written against, where cargo new defaults to edition 2024. Any current Rust install behaves the same way; rustup.rs installs it or brings an old one up to date.

Pick the directory where you keep code and run:

Terminal window
cargo new hit
Creating binary (application) `hit` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

cargo new made a directory called hit. Pick a different name and every path and prompt in the rest of this track will differ from yours, so hit it is. binary (application) means a program you can run, as opposed to a library that other code links against. Cargo also started a git repository and wrote a .gitignore — run it inside an existing repository and it skips both, which is why its output then says just package. Move into the new directory:

Terminal window
cd hit

Cargo.toml is the manifest — the project’s name, version, and dependencies:

Cargo.toml
[package]
name = "hit"
version = "0.1.0"
edition = "2024"
[dependencies]

name becomes the binary’s name. edition is the dialect of Rust the project speaks; 2024 is the current one and what a fresh cargo new produces, so there is nothing to change. The empty [dependencies] section is where crates go — the last section of this article fills it in once.

src/main.rs is the program:

src/main.rs
fn main() {
println!("Hello, world!");
}

Every later step replaces this file wholesale. It is only here so the first run has something to run. The generated .gitignore holds one line, /target: target/ is where build artifacts land, and they do not belong in version control.

Terminal window
cargo run
Compiling hit v0.1.0 (/private/tmp/hit)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/hit`
Hello, world!

Four lines from two authors. Compiling, Finished, and Running are cargo narrating; Hello, world! is the program. The path in the Compiling line is the directory you ran cargo new in — yours will not say /private/tmp. dev profile means unoptimized with debug info, the default while developing; the time it prints varies by machine. Those three lines go to stderr, not stdout — when a later step pipes hit’s output into something else, cargo’s narration stays behind.

The second run is where cargo earns its keep:

Terminal window
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hit`
Hello, world!

No Compiling line: nothing changed since the last build, so cargo ran the binary it already had. Edit a file and the next cargo run recompiles; run again unchanged and it doesn’t. That loop — edit, cargo run, read the output — is the whole development workflow for the next ten steps.

Two entries appeared on disk since cargo new:

hit/
├── .gitignore
├── Cargo.lock ← appeared on the first build
├── Cargo.toml
├── src/
│ └── main.rs
└── target/ ← build artifacts, gitignored

Cargo.lock records the exact versions cargo resolved at build time; with no dependencies yet it lists only hit itself. Applications commit it. The Cargo chapter has the details.

Every remaining step adds crates: clap for arguments in the next one, reqwest for HTTP in step 4. Before one of those edits matters, make one throwaway round trip so cargo add is a known quantity. anyhow is the error-handling crate hit picks up for real in step 4:

Terminal window
cargo add anyhow
Updating crates.io index
Adding anyhow v1.0.104 to dependencies
Features:
+ std
- backtrace

The version number will be newer by the time you run this; that is expected, and the lockfile is why it is safe. Cargo.toml gained a line:

Cargo.toml
[dependencies]
anyhow = "1.0.104"

cargo add resolves the newest version your toolchain supports, writes it into Cargo.toml, and records the exact version in Cargo.lock. The next cargo run shows what a dependency costs:

Downloading crates ...
Downloaded anyhow v1.0.104
Compiling anyhow v1.0.104
Compiling hit v0.1.0 (/private/tmp/hit)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.39s
Running `target/debug/hit`
Hello, world!

Nothing in main.rs uses anyhow, and cargo downloaded and compiled it anyway: every declared dependency is built, used or not.

hit does not need anyhow yet, and a declared-but-unused crate would tax every future build for nothing — so take it back out:

Terminal window
cargo remove anyhow
Removing anyhow from dependencies

cargo remove deletes the line, and the now-empty [dependencies] header goes with it. The first dependency hit keeps arrives in the next step, which teaches the project to read its own command line with clap — and replaces everything in src/main.rs.

The track’s code lives in the examples repository, under cli-tool/. Each step is tagged; this one is cli-ch02 — the project exactly as this article leaves it:

Terminal window
git clone https://github.com/hackingwithrust/examples.git
cd examples/cli-tool
git checkout cli-ch02
cargo run

If a later build misbehaves, checking out that tag and diffing it against your own tree shows which line drifted.

Step 3 is already on the site — the one step you can read ahead on: parsing command-line arguments with clap gives hit its get and post subcommands. The track overview keeps the full list current.