Commands¶
A command is a Python function decorated with command(). The function’s
signature defines the CLI contract.
Defining a command¶
from xclif import command
@command()
def greet(name: str, loud: bool = False) -> None:
"""Greet someone."""
msg = f"Hello, {name}!"
print(msg.upper() if loud else msg)
This produces:
Greet someone.
Usage: greet [OPTIONS] [NAME]
Arguments:
[name] No description
Options:
--help, -h Show this help message and exit (=plain|rich|agent)
--verbose, -v Increase log verbosity (repeatable up to 3 times)
--colors Control color output (always|never|auto)
--loud, -l No description
To suppress “No description” placeholders in help output, pass
show_no_description=False to the command() decorator:
from xclif import command
@command(show_no_description=False)
def greet(name: str) -> None:
...
Or when constructing Command directly:
from xclif.command import Command
cmd = Command("foo", lambda: 0, show_no_description=False)
This omits the description line entirely from help, subcommand listings, and agent output
when no docstring or description was provided. The default is True for backward
compatibility.
Markdown descriptions¶
Docstrings are rendered as Markdown in --help output (via
Rich’s Markdown support).
You can use bold, code spans, lists, and other Markdown formatting:
@command()
def deploy(target: str, dry_run: bool = False) -> None:
"""Deploy the application.
Pushes the current build to the specified **target** environment.
Supported targets:
- `staging` — deploy to the staging cluster
- `production` — deploy to prod (requires `--confirm`)
> Note: runs `preflight_checks()` before deploying.
"""
...
The first line of the docstring is used as the short description in subcommand
listings and is rendered as plain text. The full docstring (including Markdown) is
shown when running command --help.
Command naming¶
There are three ways to name a command, listed in order of precedence:
1. Explicit name — pass a string to @command():
@command("deploy")
def whatever(...): ... # command is named "deploy"
The string argument wins regardless of the function name or module name.
2. Function name — omit the argument and name the function:
@command()
def greet(...): ... # command is named "greet"
This is the most natural form for the decorator/flat API.
3. Module inference — name the function _:
# In routes/greet.py — command is named "greet" (from the module)
@command()
def _(...): ...
When the function is named _, Xclif derives the command name from the last component
of the module’s dotted path. This is the idiomatic style for file-based routing: the
filename already carries the command name, so duplicating it in the function name is
unnecessary.
All three forms are equivalent in what they produce — the only difference is where the
name comes from. In practice, prefer the module-inference style (def _) in route
files and the function-name style (def greet) in the decorator API.
Aliases¶
A command can have one or more aliases — alternative names that resolve to the same command. Pass additional strings after the canonical name:
@command("checkout", "co")
def _(branch: str) -> None:
"""Switch branches."""
...
Now both myapp checkout main and myapp co main work identically. Aliases appear
dimmed in help output next to the canonical name.
Multiple aliases are supported:
@command("checkout", "co", "sw")
def _(branch: str) -> None: ...
Aliases are validated at registration time — if an alias collides with an existing
subcommand name, a ValueError is raised.
Parameter rules¶
Python parameter |
CLI meaning |
|---|---|
|
Positional argument |
|
|
|
|
|
Repeatable |
|
Variadic positional args |
Supported types¶
str,int,float,boollist[str],list[int],list[float]Literal["a", "b", "c"]— constrained string choices (see Constrained choices)
Return value¶
The function should return an int exit code, or None (treated as 0).
Implicit options¶
Every command automatically gets:
--help/-h— print help and exit (supports--help=plain,--help=rich, and--help=agentto override auto-detection)--verbose/-v— increase verbosity; repeatable up to 3 times (-vvv)--colors— control color output (always,never, orauto)
The root command additionally gets --version.
Both --verbose and --colors cascade: if set at any level, the value is
automatically inherited by all subcommands. So myapp --verbose subcommand and
myapp subcommand --verbose are equivalent, and myapp -vv subcommand gives
the subcommand a verbosity level of 2.
Logging and verbosity¶
Xclif configures Python’s standard logging system before command handlers
run. Use ordinary loggers in your command modules:
from xclif import command, get_logger
log = get_logger(__name__)
@command()
def deploy(target: str) -> None:
log.info("Connecting to %s", target)
log.debug("Resolved deployment plan")
...
By default, warnings and errors are shown. -v enables INFO logs,
-vv enables DEBUG logs, and -vvv enables every record that reaches
the configured logger. Log output uses Rich on stderr and follows
--colors=always|never|auto.
If your application has already configured logging handlers, Xclif respects
them and only updates the logger level. Call
configure_logging() directly with force=True if you
want to replace existing handlers with Xclif’s Rich handler.
Accessing context¶
Use get_context() to access the current verbosity level,
color mode, and other cascading state from anywhere in your code — no need to
thread values through function calls:
from xclif import get_context
def deploy(target: str) -> None:
ctx = get_context()
if ctx.verbosity >= 2:
print(f"Debug: deploying to {target}")
run_deploy(target)
def run_deploy(target: str) -> None:
# Works in nested calls too — no need to pass verbosity around
ctx = get_context()
if ctx.verbosity >= 1:
print(f"Connecting to {target}...")
...
The Context object provides typed properties for
built-in options:
ctx.verbosity—int(0–3), from-v/-vv/-vvvctx.log_level— standardlogginglevel implied byctx.verbosityctx.colors—str("always"/"never"/"auto")
Note
get_context() can only be called during command dispatch (inside a
command’s run() function or code called from it). Calling it at module
level or outside dispatch raises RuntimeError.
User-defined cascading options¶
You can declare your own cascading options by annotating a parameter with
Cascade. Cascading options set on ancestor commands are
propagated to all subcommands and accessible via
get_context().
Mark a root-level option with Cascade() to share it across the entire
command tree:
from xclif import Cascade, command, get_context
@command()
def root(base_url: str = "https://api.example.com") -> None:
"""Root command."""
# base_url cascades to all subcommands
@command()
def deploy() -> None:
"""Deploy to the environment."""
ctx = get_context()
print(f"Deploying to {ctx['base_url']}")
Now myapp --base-url https://prod.example.com deploy sets base_url
for the deploy subcommand.
Cascade type-wrapper syntax¶
Cascade also works as a type wrapper, which is especially useful when
combined with WithConfig:
from xclif import Cascade, WithConfig, command, get_context
@command()
def root(
base_url: Cascade[WithConfig[str]] = "https://api.example.com",
) -> None:
...
Here Cascade[WithConfig[str]] is sugar for
Annotated[str, WithConfig(), Cascade()] — the option is both config-backed
and cascading.
You can also combine Cascade with Arg and Option annotations via
the full Annotated form:
from typing import Annotated
from xclif import Cascade, Option, command, get_context
@command()
def root(
base_url: Annotated[
str,
Option(description="API base URL"),
Cascade(),
] = "https://api.example.com",
) -> None:
"""Root command with a cascading option."""
...
Accessing cascaded values¶
Use dict-style access on the Context object:
ctx = get_context()
ctx["base_url"] # raises KeyError if not set
ctx.get("base_url") # returns None if not set
ctx.get("base_url", "https://default.example.com") # with default
get_context() returns the same Context instance
regardless of which command in the tree accesses it, so cascaded values set
by an ancestor are always visible to descendant commands.
Agent-optimized help¶
When --help output is piped or redirected (i.e. stdout is not a TTY), Xclif
automatically switches to a compact, plain-text format designed for LLM agents and
scripts. This format:
Flattens the entire command tree into one output (no need to call
--helpon each subcommand separately)Strips Rich formatting (no ANSI escape codes)
Omits framework-owned options (
--help,--verbose,--colors,--version) and thecompletionssubcommandShows only user-defined options with their types and defaults
For example, a CLI with subcommands greet and config get/config set
produces:
myapp: My application.
greet NAME - Greet someone. Options: --template STR (default: 'Hello, {}!')
config get - Print the current config.
config set KEY VALUE - Set config values.
Overriding the format¶
By default, format detection uses Rich’s Console.is_terminal, which respects
FORCE_COLOR, TTY_COMPATIBLE, and other standard environment variables.
You can also override the format explicitly:
--help=plain— same layout as rich, but with no colors or ANSI codes--help=rich— full Rich-formatted help with colors, even when piped--help=agent— compact token-efficient format, even in a TTY--help(bare) — auto-detect based on whether stdout is a TTY (rich in TTY, agent when piped)
This is useful when you want to pipe rich help through a pager (--help=rich | less -R),
get plain output in an interactive terminal for scripting, or force agent format for
LLM consumption.
Building subcommand trees imperatively¶
If you need to build the command tree in code rather than via file-based routing, see Flat (Imperative) API.