Write Your First CLI Tool in Rust with Clap and Publish It to crates.io
Build a `greet` CLI with subcommands using Clap's derive API, then ship it to crates.io in minutes.
What You'll Build
A greet CLI with two subcommands (hello and goodbye), short and long flags, and auto-generated help text — then published to crates.io for the world to install.
Prerequisites
- Rust 1.74+ installed via rustup — run
rustup update stableto ensure you're current - A free crates.io account (sign in with GitHub at crates.io) — verify your email address in account settings before publishing, or
cargo publishwill fail - macOS, Linux, or Windows with a terminal — no prior Rust experience required
1. Create the Project
cargo new greet-cli
cd greet-cli
This generates src/main.rs and Cargo.toml.
2. Add Clap as a Dependency
Open Cargo.toml and replace the [dependencies] section:
[dependencies]
clap = { version = "4", features = ["derive"] }
The derive feature unlocks #[derive(Parser)], which generates argument parsing from your struct automatically.
3. Write the CLI
Replace the entire contents of src/main.rs:
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "greet", version, about = "A friendly greeting tool")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Say hello to someone
Hello {
/// Name to greet
#[arg(short, long)]
name: String,
/// Shout the greeting in uppercase
#[arg(short, long)]
shout: bool,
},
/// Say goodbye to someone
Goodbye {
/// Name to bid farewell
#[arg(short, long)]
name: String,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Hello { name, shout } => {
let msg = format!("Hello, {}!", name);
if shout {
println!("{}", msg.to_uppercase());
} else {
println!("{}", msg);
}
}
Commands::Goodbye { name } => {
println!("Goodbye, {}!", name);
}
}
}
How it works:
#[derive(Parser)]onCligenerates all argument-parsing logic from the struct shape.#[command(subcommand)]wiresCommandsas the subcommand dispatcher.#[arg(short, long)]auto-creates both-n/--nameand-s/--shoutflags from field names.///doc comments become the help text shown by--help.versionin#[command(...)]reads the version string directly fromCargo.toml.
4. Test Locally
cargo run -- hello --name Alice
cargo run -- hello --name Alice --shout
cargo run -- goodbye -n Bob
cargo run -- --help
cargo run -- hello --help
The -- separates cargo run arguments from your binary's arguments.
5. Prepare Cargo.toml for Publishing
crates.io requires description and license (or license-file). Edit Cargo.toml so it matches exactly:
[package]
name = "greet-cli"
version = "0.1.0"
edition = "2021"
description = "A friendly greeting CLI with hello/goodbye subcommands"
license = "MIT"
repository = "https://github.com/yourusername/greet-cli"
[[bin]]
name = "greet"
path = "src/main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
The [[bin]] section explicitly sets the installed binary name to greet regardless of the package name. Without it, Cargo names the binary after the package (greet-cli), so greet hello would fail with "command not found" after install.
Pick a unique crate name.
greet-climay already be taken. Runcargo search your-chosen-nameto check. Thenamefield is exactly what users type incargo install.
6. Publish to crates.io
- Go to https://crates.io/settings/tokens and create a new API token with the "Publish new crates" scope.
- Authenticate Cargo with that token:
cargo login
Cargo will prompt you to paste the token.
- Commit all your changes.
cargo newinitializes a Git repository, andcargo publishrefuses to package a dirty working tree:
git add .
git commit -m "ready to publish"
- Dry-run first to catch packaging errors before anything goes live:
cargo publish --dry-run
- Ship it:
cargo publish
Email verification required. If you signed in to crates.io with GitHub but haven't verified your email,
cargo publishwill fail with an email verification error. Confirm your address at crates.io/settings/profile before this step.
Your crate is now live. Anyone can install it with:
cargo install greet-cli
Verify It Works
After cargo install, the binary lands in ~/.cargo/bin (already on your PATH if you used rustup). Run:
greet hello --name Alice --shout
# Expected: HELLO, ALICE!
greet --version
# Expected: greet 0.1.0
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
error[E0432]: unresolved import clap::Parser |
Missing features = ["derive"] |
Add features = ["derive"] to the clap entry in Cargo.toml |
the name … is already taken on publish |
Crate name collision on crates.io | Choose a unique name in [package] |
missing field 'description' on publish |
Required metadata absent | Add description and license to [package] |
X dirty files found in the working directory |
Uncommitted changes in the Git repo | Run git add . && git commit -m "ready to publish" |
greet: command not found after install |
~/.cargo/bin not on PATH |
Run source ~/.cargo/env or restart your shell |
Next Steps
- Richer argument types: use
Vec<String>for variadic inputs orOption<String>for optional flags — see the Clap derive reference. - Structured output: add
serde+serde_jsonto support a--output jsonflag for scripting. - Releasing updates: bump
versioninCargo.tomland runcargo publishagain — all prior versions remain permanently immutable on crates.io. - Automate releases: use the publish-crates GitHub Action to publish on every tagged commit.
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.