Skip to content
Learn Rust
Theme

Command-line tools with clap

clap builds the whole command line from a struct: flags, arguments, subcommands, --help and error messages. Your doc comments become the help text.

RememberDescribe your command line as a struct, derive Parser, and call Cli::parse().

Project layout
todo-cli/
├── src/
│   └── main.rs                 the program on this page
├── Cargo.lock                  created by Cargo
└── Cargo.toml                  dependencies go here
RustCargo.toml
[package]
name = "todo-cli"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.6.7", features = ["derive"] }
Rustsrc/main.rs
use clap::{Parser, Subcommand};
/// A tiny to-do list
#[derive(Parser)]
#[command(version)]
struct Cli {
/// Print more detail (allowed before or after the subcommand)
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Add a task
Add {
/// What to do
text: String,
/// Priority from 1 (high) to 3 (low)
#[arg(short, long, default_value_t = 2)]
priority: u8,
},
/// List tasks
List,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Command::Add { text, priority } => {
println!("added {text:?} with priority {priority}");
if cli.verbose {
println!("(verbose mode is on)");
}
}
Command::List => println!("nothing to list yet"),
}
}

Here’s what you get without writing any help text by hand. After --, the arguments go to your program instead of to Cargo:

terminal
$ cargo run -- add "learn clap" -p 1 --verbose
added "learn clap" with priority 1
(verbose mode is on)
$ cargo run -- add "learn clap" -p abc
error: invalid value 'abc' for '--priority <PRIORITY>': invalid digit found in string

For more information, try '--help'.
$ cargo run -- --help
A tiny to-do list

Usage: todo-cli [OPTIONS] <COMMAND>

Commands:
  add   Add a task
  list  List tasks
  help  Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose  Print more detail (allowed before or after the subcommand)
  -h, --help     Print help
  -V, --version  Print version
$ cargo run -- add --help
Add a task

Usage: todo-cli add [OPTIONS] <TEXT>

Arguments:
  <TEXT>  What to do

Options:
  -p, --priority <PRIORITY>  Priority from 1 (high) to 3 (low) [default: 2]
  -v, --verbose              Print more detail (allowed before or after the subcommand)
  -h, --help                 Print help
$ cargo run -- add
error: the following required arguments were not provided:
  <TEXT>

Usage: todo-cli add <TEXT>

For more information, try '--help'.

The same tool in Go, with the standard flag package

Section titled “The same tool in Go, with the standard flag package”
Gomain.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
verbose := flag.Bool("verbose", false, "print more detail")
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintln(os.Stderr, "usage: todo [-verbose] add|list ...")
os.Exit(2)
}
switch flag.Arg(0) {
case "add":
addCmd := flag.NewFlagSet("add", flag.ExitOnError)
priority := addCmd.Int("priority", 2, "priority from 1 (high) to 3 (low)")
addCmd.Parse(flag.Args()[1:])
if addCmd.NArg() < 1 {
fmt.Fprintln(os.Stderr, "usage: todo add [-priority N] TEXT")
os.Exit(2)
}
fmt.Printf("added %q with priority %d\n", addCmd.Arg(0), *priority)
if *verbose {
fmt.Println("(verbose mode is on)")
}
case "list":
fmt.Println("nothing to list yet")
default:
fmt.Fprintln(os.Stderr, "unknown command:", flag.Arg(0))
os.Exit(2)
}
}
$ go build -o todo .
$ ./todo -verbose add -priority 1 "learn flags"
added "learn flags" with priority 1
(verbose mode is on)
$ ./todo -h
Usage of ./todo:
  -verbose
    	print more detail
$ ./todo add
usage: todo add [-priority N] TEXT
$ ./todo add -priority 1 "learn flags" -verbose
added "learn flags" with priority 1

The last command puts -verbose after the text. Go’s flag package stops reading flags at the first argument that is not a flag (docs), so it did not complain and did not turn verbose mode on.

  • Doc comments (///) become the help text. The two --help runs above are built from them, so the description sits next to the code it describes.
  • Types do the checking. priority: u8 is why -p abc is rejected with a clear message, and why -p 300 is too (try it below). A u8 allows 0 to 255, so “between 1 and 3” is still a rule you write yourself. default_value_t = 2 shows up as [default: 2] in the help.
  • Subcommands are an enum, so a match without a _ arm must cover every one. Add a Done { id: u32 } variant and leave main alone, and the build fails (Enums explains why):
$ cargo build
error[E0004]: non-exhaustive patterns: `Command::Done { .. }` not covered
  --> src/main.rs:36:11
   |
36 |     match cli.command {
   |           ^^^^^^^^^^^ pattern `Command::Done { .. }` not covered
   |
note: `Command` defined here
  --> src/main.rs:16:6
   |
16 | enum Command {
   |      ^^^^^^^
...
28 |     Done {
   |     ---- not covered
   = note: the matched value is of type `Command`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
43 ~         Command::List => println!("nothing to list yet"),
44 ~         Command::Done { .. } => todo!(),
  • Global flags. A flag on the top-level struct has to come before the subcommand unless it is marked global = true, which lets --verbose go anywhere. The first run on this page uses it after add. The session below takes , global = true out of a copy and runs it again: after add it is refused, and before add it works. Go’s flag (above) and Python’s argparse (in the table below) also need it first.
terminal
$ cargo run -- add "learn clap" --verbose    # with global = true
added "learn clap" with priority 2
(verbose mode is on)
$ cargo run -- add "learn clap" --verbose    # after deleting ", global = true"
error: unexpected argument '--verbose' found

  tip: to pass '--verbose' as a value, use '-- --verbose'

Usage: todo-cli add <TEXT>

For more information, try '--help'.
$ cargo run -- --verbose add "learn clap"    # still without global = true
added "learn clap" with priority 2
(verbose mode is on)
  • Exit codes. In these runs a usage error exited with code 2 and --help with 0. The clap docs for Error::exit say the same: status 2 when it prints to stderr, 0 when it prints to stdout.

Where the image has a parser that comes with the language or its runtime, I wrote the same to-do tool with it. The other cells only name the usual libraries; I did not run those.

Language Built in, and run for this page Libraries people use (named, not run)
Rust std::env::args is only the raw list of strings (Arguments) clap
Go flag (the program above) cobra
Python argparse Typer, Click
TypeScript util.parseArgs, built into Node (the version is the first line of the runs below) commander, yargs
Java nothing run picocli
Kotlin nothing run Clikt
C# nothing run System.CommandLine (a NuGet package, per Microsoft’s docs)
C getopt (POSIX, from the C library; run with glibc, the GNU C library) none named
C++ nothing run CLI11, cxxopts

What the runs showed:

terminal
$ node --version && python3 --version
v24.22.1
Python 3.13.5
$ python3 todo.py --verbose add "learn argparse" -p 1   # Python, argparse
added "learn argparse" with priority 1
(verbose mode is on)
$ python3 todo.py add "learn argparse" --verbose   # Python
usage: todo.py [-h] [-v] {add,list} ...
todo.py: error: unrecognized arguments: --verbose
$ python3 todo.py add "learn argparse" -p abc   # Python
usage: todo.py add [-h] [-p PRIORITY] text
todo.py add: error: argument -p/--priority: invalid int value: 'abc'
$ node todo.ts add "learn parseArgs" --verbose -p 1   # TypeScript, node:util parseArgs
added "learn parseArgs" with priority 1
(verbose mode is on)
$ node todo.ts add "learn parseArgs" -p abc   # TypeScript
invalid priority: abc
$ gcc -Wall -o todo todo.c   # C, getopt
$ ./todo -v add "learn getopt" -p 1   # C
added "learn getopt" with priority 1
(verbose mode is on)
$ ./todo add "learn getopt" -v   # C (glibc)
added "learn getopt" with priority 2
(verbose mode is on)
$ ./todo add "learn getopt" -p abc   # C
invalid priority: abc
  • argparse checks the number for you (type=int), like clap, and its subparsers also need --verbose before add.
  • parseArgs and getopt hand back text. The number check and the “invalid priority” message are code I wrote. Neither knows about subcommands, so --verbose and -v were accepted after the text. For getopt that is glibc’s default (glibc is the GNU C library): it moves non-option arguments to the end as it scans, and POSIXLY_CORRECT or a leading + in the option string stops at the first one (man page).

Run each command in the project. The session below was run in the image.

  1. cargo run -- --version. It prints todo-cli 0.1.0, from #[command(version)] and the version in Cargo.toml.
  2. cargo run -- add "learn clap" -p 300. Look for 300 is not in 0..=255: the u8 type is the rule.
  3. cargo run -- ad "learn clap". clap rejects the unknown subcommand and adds a tip that add is similar.
  4. cargo run -- add "-draft". The text starts with a dash, so clap reads -d as a flag and says to use -- -d; with -- before it, the same text is accepted as the task.
terminal
$ cargo run -- --version
todo-cli 0.1.0
$ cargo run -- add "learn clap" -p 300
error: invalid value '300' for '--priority <PRIORITY>': 300 is not in 0..=255

For more information, try '--help'.
$ cargo run -- ad "learn clap"
error: unrecognized subcommand 'ad'

  tip: a similar subcommand exists: 'add'

Usage: todo-cli [OPTIONS] <COMMAND>

For more information, try '--help'.
$ cargo run -- add "-draft"
error: unexpected argument '-d' found

  tip: to pass '-d' as a value, use '-- -d'

Usage: todo-cli add [OPTIONS] <TEXT>

For more information, try '--help'.
$ cargo run -- add -- "-draft"
added "-draft" with priority 2