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.