Getting Started
Your first CLI¶
An extremely simple clypi CLI can be built in a couple of lines:
from clypi import Command
class Cli(Command):
async def run(self):
print(f"Hello, world!")
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
Adding positional arguments¶
Positional arguments are unnamed arguments provided by the user in a specific order. We only care about the position they are in (hence the name positional).
In this example, whatever the first argument to our program is will be passed in as name
,
and the second argument will be passed as age
since they're defined in that order.
from clypi import Command, Positional
class Cli(Command):
name: Positional[str]
age: Positional[int]
async def run(self):
print(f"Hello, {self.name}. You are {self.age}!")
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] ┃
┃ [AGE] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
If we run it:
Adding options¶
Options are named arguments, and they are usually optional. A user of your CLI must provide the name of the argument and the value(s) to assign to it.
from clypi import Command, Positional
class Cli(Command):
name: Positional[str]
age: int | None = None
async def run(self):
print(f"Hello, {self.name}.")
if self.age is not None:
print(f"You are {self.age}!")
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --age <AGE> ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
If we run it with and without the --age
flag, we get:
python cli.py Daniel --age 22Hello, Daniel.
You are 22!
Adding documentation¶
As it stands right now, our CLI is a bit difficult to use. We can add documentation to help our users understand what our CLI is and how to use it.
To document the overall command we can use Python docstrings. To document each argument
and option we can make user of clypi's arg
helper.
from clypi import Command, Positional, arg
class Cli(Command):
"""A very simple CLI"""
name: Positional[str] = arg(help="Your name")
age: int | None = arg(None, help="Your age in years")
async def run(self):
print(f"Hello, {self.name}.")
if self.age is not None:
print(f"You are {self.age}!")
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
Usage: cli [NAME] [OPTIONS]
┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] Your name ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --age <AGE> Your age in years ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Adding subcommands¶
Our applications sometimes have multiple usecases. To better group each usecase and avoid having too many options and arguments, we can create subcommands. Subcommands allow the user to select which tool to use inside your CLI.
Creating subcommands is very easy. Just create more commands and then define a class-level attribute
in your main CLI with the name subcommand
:
from clypi import Command, Positional, arg
class Greet(Command):
"""Say hi to someone"""
name: Positional[str] = arg(help="Your name")
async def run(self):
print(f"Hello, {self.name}")
class Wave(Command):
"""Wave at someone"""
name: Positional[str] = arg(help="Your name")
async def run(self):
print(f"👋 {self.name}")
class Cli(Command):
"""A very simple CLI"""
subcommand: Greet | Wave
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
Usage: cli COMMAND
┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ greet Say hi to someone ┃
┃ wave Wave at someone ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
python cli.py greet --helpSay hi to someone
Usage: cli greet [NAME]
┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] Your name ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
python cli.py wave Daniel👋 Daniel
Prompting for values¶
Sometimes we want to make arguments required (by not providing a default) but we don't want to force our users to pass in an argument directly. We might want to ask them in a more user-friendly way. For that, we can set up a prompt in case the user does not manually pass in a value:
from clypi import Command, Positional, arg
class Cli(Command):
"""A very simple CLI"""
name: Positional[str] = arg(prompt="What's your name?")
async def run(self):
print(f"Hello, {self.name}!")
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
Hello, Daniel!
python cli.py DanielHello, Daniel!
Built-in parsers¶
Clypi comes with built-in parsers for all common Python types. See the built-in types section in the API docs to find all supported types and validations. Most often, using a normal Python type will automatically load the right parser, but if you want more control or extra features you can use these directly:
from clypi import Command, arg
import clypi.parsers as cp
class MyCommand(Command):
file: Path = arg(
parser=cp.Path(exists=True),
)
You can also create your own parser if there's a complex data type we do not support. Refer to the Custom parsers docs.
Argument groups¶
Sometimes you want to separate your options based on behaviors. For example, you might want to split up environment options from output options. For that, just define a group
parameter in the arg
s you want to group together:
from typing import Literal
from clypi import Command, Positional, arg
class Cli(Command):
"""A very simple CLI"""
# Output configs here
format: Literal["json", "raw"] = arg("raw", help="The output format to use")
verbose: bool = arg(False, help="Whether to show verbose output")
# Cluster configs here...
env: Literal["qa", "prod"] = arg(
"qa",
help="The environment to run in",
group="environment",
)
cluster: Literal["default", "secondary"] = arg(
"default",
help="The cluster to run in",
group="environment",
)
if __name__ == '__main__':
cmd = Cli.parse()
cmd.start()
You can see they now get displayed in different groups:
Usage: cli [OPTIONS]
┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --format <FORMAT> The output format to use {JSON|RAW} ┃
┃ --verbose Whether to show verbose output ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Environment options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --env <ENV> The environment to run in {QA|PROD} ┃
┃ --cluster <CLUSTER> The cluster to run in {DEFAULT|SECONDARY} ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛