Skip to content

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:

  1. Command-line argument
  2. Environment variable
  3. Default value
  4. 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 name attribute
  • 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.