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.
Create the project
Section titled “Create the project”Pick the directory where you keep code and run:
cargo new hit Creating binary (application) `hit` packagenote: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.htmlcargo 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:
cd hitWhat cargo generated
Section titled “What cargo generated”Cargo.toml is the manifest — the project’s name, version, and dependencies:
[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:
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.
The first run
Section titled “The first run”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:
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, gitignoredCargo.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.
The first dependency
Section titled “The first dependency”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:
cargo add anyhow Updating crates.io index Adding anyhow v1.0.104 to dependencies Features: + std - backtraceThe 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:
[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:
cargo remove anyhow Removing anyhow from dependenciescargo 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.
Where the code lives
Section titled “Where the code lives”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:
git clone https://github.com/hackingwithrust/examples.gitcd examples/cli-toolgit checkout cli-ch02cargo runIf 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.