Command Groups

Nested command groups for hierarchical CLI structures.

2 min read 367 words

Groups organize commands into namespaces, creating a hierarchical CLI structure. Each group becomes a subcommand that has its own subcommands.

Creating groups

from milo import CLI

cli = CLI(name="myapp", description="My tool")

site = cli.group("site", description="Site operations")

@site.command("build", description="Build the site")
def build(output: str = "_site") -> str:
    return f"Building to {output}"

@site.command("serve", description="Start dev server")
def serve(port: int = 8080) -> str:
    return f"Serving on port {port}"
myapp site build --output dist
myapp site serve --port 3000

Nesting groups

Groups can contain sub-groups to any depth:

config = site.group("config", description="Config management")

@config.command("show", description="Show merged config")
def config_show() -> dict:
    return {"theme": "dark", "lang": "en"}

@config.command("set", description="Update a config value")
def config_set(key: str, value: str) -> dict:
    return {"updated": key, "value": value}
myapp site config show
myapp site config set --key theme --value light

Group aliases

Groups support aliases just like commands:

site = cli.group("site", description="Site operations", aliases=("s",))
config = site.group("config", description="Config management", aliases=("cfg",))
myapp s build              # same as: myapp site build
myapp s cfg show           # same as: myapp site config show

Dotted paths

Commands in groups are addressable via dotted paths for programmatic access:

cmd = cli.get_command("site.build")
cmd = cli.get_command("site.config.show")

result = cli.call("site.build", output="dist")

Walking all commands

walk_commands()traverses the entire command tree, yielding dotted paths:

for path, cmd in cli.walk_commands():
    print(f"{path}: {cmd.description}")

# Output:
# greet: Say hello
# site.build: Build the site
# site.serve: Start dev server
# site.config.show: Show merged config

This is used by --llms-txt and --mcpto discover all commands including those inside groups.

Adding external groups

Groups can be defined separately and added later:

from milo import Group

db = Group("db", description="Database operations")

@db.command("migrate", description="Run migrations")
def migrate() -> str: ...

@db.command("seed", description="Seed test data")
def seed() -> str: ...

cli.add_group(db)

Freezing groups

Convert a mutableGroup to an immutable GroupDefsnapshot:

frozen = site.to_def()
print(frozen.name)      # "site"
print(frozen.commands)   # {"build": CommandDef(...), ...}
print(frozen.groups)     # {"config": GroupDef(...)}

Tip

Groups integrate fully with--llms-txt (nested headings), --mcp(dot-notation tool names), and help output. All commands in groups are discoverable by AI agents.