Features
Commands
Commands are the core building blocks of your CLI. You define them using decorators.
from piou import Cli, Option
cli = Cli(description='A CLI tool')
@cli.command(cmd='foo', help='Run foo command')
def foo_main(
foo1: int = Option(help='Foo arguments'),
foo2: str = Option('-f', '--foo2', help='Foo2 arguments'),
foo3: str | None = Option(None, '-g', '--foo3', help='Foo3 arguments'),
):
pass
In this case:
- foo1 is a required positional argument (no keyword flags, default is ...).
- foo2 is a required keyword argument (has flags, default is ...).
- foo3 is an optional keyword argument (has flags, default is None).
Optional Ellipsis
Option() defaults to required (...), so Option(help='...') is equivalent to Option(..., help='...').
Global Options
You can specify global options that apply to all commands:
cli = Cli(description='A CLI tool')
cli.add_option('-q', '--quiet', help='Do not output any message')
Docstrings as Descriptions
The description can also be extracted from the function docstring:
Async Support
A command can also be asynchronous; it will be run automatically using asyncio.run.
Default Command (Main)
Use the @cli.main() decorator (or is_main=True) to define a default command that runs when no named command is matched.
Run it directly:
@cli.main() can also coexist with named commands. Named commands always take priority, and unmatched input falls back to the default command:
cli = Cli(description='My CLI')
@cli.main()
def default(name: str = Option(...)):
print(f'Hello {name}')
@cli.command('greet', help='Greet someone')
def greet(name: str = Option(..., '--name')):
print(f'Greetings, {name}!')
python run.py world # runs default: "Hello world"
python run.py greet --name a # runs greet: "Greetings, a!"
Command Groups / Sub-commands
You can nest commands arbitrarily deep using command groups.
sub_cmd = cli.add_command_group('sub', help='A sub command')
sub_cmd.add_option('--test', help='Test mode')
@sub_cmd.command(cmd='bar', help='Run bar command')
def sub_bar_main(**kwargs):
pass
Running python run.py sub -h will show the specific help for that group.
Options Processor
Sometimes you need to intercept global options before a command runs (e.g., to set up logging level).
from piou import Cli, Option
cli = Cli(description='A CLI tool')
@cli.processor()
def processor(verbose: bool = Option(False, '--verbose', help='Increase verbosity')):
print(f'Processing {verbose=}')
By default, processed options are consumed. To pass them down to the command functions as well, use propagate_options=True when creating the Cli or sub-parser.
You can also use set_options_processor() instead of the decorator. Options defined via Option() in the function signature are automatically extracted and registered as global options:
cli = Cli(description='A CLI tool')
def processor(
verbose: bool = Option(False, '-v', '--verbose'),
log_level: str | None = Option(None, '--log-level'),
):
...
cli.set_options_processor(processor)
This is equivalent to using the @cli.processor() decorator.
Error Handling
CommandError
Use CommandError to display a user-friendly error message and exit with code 1, without showing a traceback.
from piou import Cli, Option
from piou.exceptions import CommandError
cli = Cli(description='A CLI tool')
@cli.command(cmd='deploy', help='Deploy the application')
def deploy_main(
env: str = Option(help='Target environment'),
):
if env not in ('staging', 'production'):
raise CommandError(f'Unknown environment: {env!r}')
...
When raised inside a command, CommandError is caught by Cli.run() and the message is printed using the configured formatter — no Python traceback is shown to the user.