File-Based Routing¶
Xclif discovers commands by walking a Python package. The package hierarchy maps directly to the command hierarchy — no explicit registration required.
How it works¶
Given this package layout:
myapp/routes/
├── __init__.py → myapp (root command)
├── greet.py → myapp greet
└── server/
├── __init__.py → myapp server (group command)
├── start.py → myapp server start
└── stop.py → myapp server stop
Each module must export exactly one Command object (typically created
with the command() decorator):
# routes/__init__.py — the root command
from xclif import command
@command()
def myapp() -> None:
"""My awesome CLI."""
# routes/greet.py
from xclif import command
@command()
def _(name: str) -> None:
"""Greet someone."""
print(f"Hello, {name}!")
See Commands for a full explanation of the naming rules.
Entry point¶
# __main__.py
from xclif import Cli
from . import routes
cli = Cli.from_routes(routes)
if __name__ == "__main__":
cli()
from_routes() walks the package, collects all Command objects, and wires
them into the tree automatically.
Group commands¶
A directory with an __init__.py becomes a group command — a command that has subcommands.
The __init__.py should define the group’s help text and any group-level options:
# routes/server/__init__.py
from xclif import command
@command()
def _() -> None:
"""Manage the server."""
# Called when user types `myapp server` with no subcommand.
# Default behaviour: print help.
Note
A group command cannot declare positional arguments. Positional arguments and subcommands are mutually exclusive — Xclif enforces this at definition time.
Best practices¶
Keep routes lean. from_routes uses pkgutil.walk_packages to discover commands, which
imports every module it finds in the package. Every file in your routes tree is executed at
startup — including files that define no command. Put business logic, helpers, and shared
utilities in a sibling module outside the routes package and import from there:
myapp/
├── __init__.py
├── __main__.py
├── utils.py ← helpers live here, imported only when needed
├── db.py ← same — not walked by from_routes
└── routes/
├── __init__.py
├── greet.py ← imports from myapp.utils as needed
└── config/
├── __init__.py
├── get.py
└── set.py
# routes/greet.py
from xclif import command
from myapp.utils import format_greeting # imported only when greet.py is loaded
@command()
def _(name: str) -> None:
"""Greet someone."""
print(format_greeting(name))
If you put a utility module inside the routes package, it will be imported on every invocation even when the user runs a completely unrelated command.
Prefix private modules with ``_``. Xclif skips route modules and packages whose names start
with _, so helper code can live inside the routes package without being registered as commands.