Commands
So far you have seen how to declare arguments on a function. This page covers everything you can do at the command level: descriptions, aliases, hidden commands, deprecation, and per-parameter configuration.
Doc comments as help text
The easiest way to add help text to a command is with doc comments. The first line becomes the short help shown in listings. A blank line separates it from the long details shown only in --help.
#[command]
/// Deploy the application
///
/// Runs pre-flight checks, builds the release artifact,
/// then pushes it to the target environment.
/// Use --force to skip pre-flight checks.
fn deploy(target: Pos<String>) {
println!("Deploying {target}");
}
The short help "Deploy the application" appears in the command listing. The longer paragraph appears when the user runs myapp deploy --help.
Explicit attributes
You can also pass metadata directly in the #[command] attribute:
#[command(
help = "Deploy the application",
details = "Runs pre-flight checks and pushes to the target.",
)]
fn deploy(target: Pos<String>) {
println!("Deploying {target}");
}
Explicit attributes take priority over doc comments. If you provide both, the attribute wins.
Command name
By default the command name is the function name. You can override it:
#[command(name = "ship")]
fn deploy(target: Pos<String>) {
println!("Shipping {target}");
}
Now the command is invoked as myapp ship, not myapp deploy.
Aliases
Give a command multiple names:
#[command(
help = "Deploy the application",
aliases = ["ship", "release", "push"],
)]
fn deploy(target: Pos<String>) {
println!("Deploying {target}");
}
All of these invoke the same command:
myapp deploy production
myapp ship production
myapp release production
myapp push production
Aliases do not appear in the help listing. Only the primary command name is shown.
Hidden commands
Mark a command as hidden to omit it from the help listing. The command is still fully invocable.
#[command(hidden = true)]
fn debug_dump() {
println!("Dumping debug info...");
}
This is useful for maintenance commands, debug tools, or migration scripts that you do not want regular users to see.
Deprecated commands
Mark a command as deprecated to warn users when they invoke it:
#[command(
deprecated = true,
deprecation_note = "use 'upload' instead",
)]
fn publish(target: Pos<String>) {
println!("Publishing {target}");
}
When invoked, the command prints a warning to stderr before running:
warning: command 'publish' is deprecated: use 'upload' instead
Deprecated commands also appear with dimmed styling in the help listing.
Per-parameter configuration
Use param(ident, key = value, ...) blocks inside the #[command] attribute to configure individual parameters. The parameter keyword works too.
#[command(
param(host, help = "Target host", short = 't', placeholder = "HOST", env = "DEPLOY_HOST"),
param(port, help = "Port number", short = 'p', default = "8080"),
param(level, short = 'l', choices = ["debug", "info", "error"]),
param(verbose, short = 'v', conflicts_with = ["quiet"]),
param(quiet, short = 'q'),
)]
fn deploy(
host: Pos<String>,
port: Named<Option<u16>>,
level: Named<String>,
verbose: bool,
quiet: bool,
) {
println!("Deploying to {host}:{port} at {level} level");
}
Let us break down what each key does.
Short aliases
The short key assigns a single-character alias:
param(port, short = 'p')
This lets the user write -p 8080 instead of --port 8080.
Each short character can only be used by one parameter. If two parameters share the same short, the macro produces a compile-time error.
Custom names
The name key overrides the CLI flag name:
param(env, name = "environment")
This registers --environment instead of the default --env.
Placeholders
The placeholder key changes the token shown in help text:
param(host, placeholder = "HOST")
Instead of showing <host>, help shows <HOST>.
Hiding parameters
Set hide = true to omit a parameter from help listings:
param(secret_key, hide = true)
The parameter still works at runtime. It just does not appear in --help.
Environment variable fallback
The env key specifies an environment variable to check when the argument is not provided on the command line:
param(host, env = "DEPLOY_HOST")
If the user does not pass --host, clish checks $DEPLOY_HOST.
Default values
The default key provides a fallback when neither the CLI argument nor the environment variable is set:
param(port, default = "8080")
Resolution order
For any parameter with env and/or default, the resolution order is:
- Command-line argument
- Environment variable
- Default value
- Error (if the parameter is required)
Choices
The choices key restricts a parameter to a set of allowed values:
param(level, choices = ["debug", "info", "error"])
If the user passes a value not in the list, clish prints an error:
error: invalid choice 'trace': expected one of debug, info, error
Conflicts
The conflicts_with key declares parameters that cannot appear together:
param(verbose, conflicts_with = ["quiet"])
param(quiet, conflicts_with = ["verbose"])
If the user passes both, clish prints a conflict error.
Prerequisites
The requires key declares parameters that must be present alongside this one:
param(output, requires = ["format"])
If the user passes --output without --format, clish prints a requires error.
Putting it all together
Here is a full-featured command that uses everything:
#[command(
help = "Deploy the application to a target",
details = "Performs validation, builds artifacts, and deploys.\n\nUse --force to skip validation in emergencies.",
aliases = ["ship", "release"],
param(target, help = "Target host or cluster", short = 't', placeholder = "HOST"),
param(env, help = "Environment name", name = "environment", short = 'e', env = "DEPLOY_ENV"),
param(force, help = "Skip pre-flight checks", short = 'f'),
param(tags, help = "Tags to apply", short = 'T', choices = ["prod", "staging", "dev"]),
param(dry_run, help = "Print actions without executing", conflicts_with = ["force"]),
)]
fn deploy(
target: Pos<String>,
env: Named<String>,
force: bool,
tags: Named<Vec<String>>,
dry_run: bool,
) {
if dry_run {
println!("Would deploy {target} to {env} with tags {tags:?}");
} else {
println!("Deploying {target} to {env} with tags {tags:?} (force={force})");
}
}
Oneshot mode
For single-command CLIs, pass the command function to app!():
use clish::prelude::*;
#[command]
fn greet(name: Pos<String>) {
println!("Hello, {name}!");
}
fn main() {
app!(greet).run();
}
Oneshot mode runs the command directly without subcommand dispatch. It enforces these rules at startup:
- Exactly one
#[command]must be registered in the binary - The command must not have a custom
nameattribute - The command must not have any
aliases - The command must not be
hidden - The command must not be
deprecated
Violations produce a panic with a clear message telling you to use app!() instead.
Next step
If you are building a single-command tool, check out Oneshot Mode. Otherwise, learn about Value Resolution to understand how parameters get their values from the command line, environment variables, and defaults.