Skip to content

Basic usage

Let's take a command from the quick start example and put it into the group.

from paramspecli import Group, argument, option, flag


def ping(addr: str, *, until_stopped: bool, count: int | None):
    print(f"Pinging {addr} ...")


def server(name: str, *, port: int, debug: bool):
    print(f"Serving {name} on port {port} ...")


cli = Group(prog="myprog")

with cli.add_group("net") as group:
    with group.add_command("ping", ping) as cmd:
        cmd.bind(
            -argument("IP"),
            until_stopped=-flag("-t"),
            count=-option("-n", type=int),
        )

    with group.add_command("server", server) as cmd:
        cmd.bind(
            -argument("TEXT"),
            port=-option("--port", type=int, default=80),
            debug=-flag("--debug", "-g"),
        )

route = cli.parse()
route()

Running it:

$ python myprog.py net ping -t -n 10 127.0.0.1
Pinging 127.0.0.1 ...

$ python myprog.py net server --port 8080 -g breakfast
Serving breakfast on port 8080 ...

$ python myprog.py net --help
usage: myprog net [--help] {ping,server} ...

options:
  --help, -h     Show help and exit

commands:
  {ping,server}
    ping
    server

The example is self-explaining enough, except the crucial bit. Did you notice the unnoticeable minus sign prefixing the option? You must add it for the type check to pass:

cli.bind(
    count=-option("-n", type=int),
)

Why? In short, paramspecli lies to the type checker. In the name of good, of course.

If you don't like minus, use the t property. Or t object, which overloads [], @, () operations. Choose one style and stick to it.

from paramspecli import t

option("-n").t
t(option("-n"))
t[option("-n")]
t @ option("-n")
option("-n") @ t

Why with and context managers?

Just for an aesthetic - they paint a very nice visual hierarchy. They are not required and in fact do nothing.


Now let's take a closer look at the building blocks.

Arguments

Arguments are instances of the Argument class. They are created by the argument factory function. Arguments are positional-only, and it's an error to bind it by keyword.


argument(metavar, type=None, *, help=None, nargs=None, default=None, choices=None)

  • metavar - metavar name. Note that it's required
  • type - callable to convert string into the parameter type
  • help - help string
  • nargs

    • int: Consume fixed number of words
    • *: Consume zero or more words
    • +: Consume one or more words
    • ?: Consume zero or one word (optional mode)
  • default - Allowed if nargs="?". May be anything. If default is a string, it will be converted by type(), otherwise left as is.

  • choices - Iterable of allowed values
Code Command line Result
argument("N", type=int) 1 1
argument("N", type=int, nargs="*") 1 [1]
1 2 [1, 2]
[]
argument("N", type=int, nargs="?", default=-1) 1 1
-1

Options

Options are instances of the Option class. They are created by one of factory functions. Options are keyword-only, and it's an error to bind it positionally.

option

Tries to consume a word from the command line. Result is a (type converted) word. If it is not present, result is default.


option(*names, type=None, help=None, default=None, nargs=None, choices=None, metavar=None, show_default=None)

  • names - one or more names on a command line. Should start from the -, for example, --foo, -g.
  • type - callable to convert string into the parameter type
  • help - help string. Also may be set to False hide option from --help.
  • default - result if option is missing. May be anything. If default is a string, it will be converted by type(), otherwise left as is.
  • nargs:
    • int: Consume fixed number of words
    • +: Consume one or more words
    • *: Consume zero or more words
    • ?: Consume zero or one word. If zero, ... (ellipsis) is returned instead.
  • choices - Iterable of allowed values.
  • metavar - meta-variable string. If nargs is a fixed number, may be a tuple of strings.
  • show_default - show default value in help:
    • True: show default value
    • False: show nothing
    • None: show default if it makes sense
    • str: show this very string
Code Command line Result
option("--addr") --addr www.example.com "www.example.com"
None
option("--width", type=int, default=80) --width 120 120
80
option("--user", nargs="+")
--user bob ["bob"]
--user bob alice ["bob", "alice"]
None
option("--xyz", nargs=3, type=int) --xyz 0 5 -1 [0, 5, -1]
option("--threads", nargs="?", type=int) --threads 2 2
--threads ...
None
option("--day", choices=("sunday", "monday")) --day sunday "sunday"
--day monday "monday"
None

repeated_option

Like option, but allowed to present multiple times on a command line. Result is a list of (type converted) words. If missing, result is an empty list.


repeated_option(names, *, type=None, help=None, nargs=None, choices=None, flatten=False, metavar=None)

  • names, type, help, choices, metavar, show_default - see option
  • nargs - int, + or *. Consume fixed or unlimited number of words for each option occurence.
  • flatten - Meaningful only if nargs set. Flattens the individual lists into the single list. Under the hood, it switches the argparse action from append to the extend.
Code Command line Result
repeated_option("--port", type=int) --port 80 --port 8080 [80, 8080]
[]
repeated_option("--port", type=int, nargs="+") --port 80 [[80]]
--port 80 --port 8080 8081 [[80], [8080, 8081]]
[]
repeated_option("--port", type=int, nargs="+", flatten=True) --port 80 [[80]]
--port 80 --port 8080 8081 [80, 8080, 8081]
[]

flag

Simple flag. Result is value. If missing, result is default.


flag(names, *, help=None, value=True, default=..., show_default=None)

  • names, help, show_default - see option
  • value - result if flag is present. May be anything.
  • default - result if flag is missing. If not set, choosen automatically between True, False and None depending on a value
Code Command line Result
flag("--debug") --debug True
False
flag("--noerr", value=False) --noerr False
True
flag("--cheat", value="iddqd") --cheat "iddqd"
None
flag("--cheat", value="iddqd", default="nocheat") --cheat "iddqd"
"nocheat"

switch

A --foo/--no-foo style complimentary flags. Result is True or False depending on a flag. If missing, result is default.


switch(names, *, help=None, default=False, show_default=None)

  • names, help, show_default - see option
  • default - result if flag is missing. May be anything.
Code Command line Result
switch("--magic") --magic True
--no-magic False
False
switch("--magic", default=True) --magic True
--no-magic False
True
switch("--magic", default=None) --magic True
--no-magic False
None

count

A -vvv style flag. Result is a number of the flag occurences. If missing, result is default


count(names, *, help=None, default=0, show_default=None)

  • names, help, show_default - see option
  • default - int or None. If int, it's also the initial count value.
Code Command line Result
count("-v") -vv 2
0
count("-v", default=40) -vv 42
40
count("-v", default=None) -vv 2
None

repeated_flag

Like a flag, but may present multiple times on a command line. Result is a list of value. If missing, result is an empty list. Repeated flags are most useful if mixed.


repeated_flag(names, *, help=None, value=True)

  • names, help - see option
  • value - value to append to the result list for each flag occurence. May be anything.
Code Command line Result
repeated_flag("--hard") --hard [True]
--hard --hard [True, True]
[]
repeated_flag("--hard", value="core") --hard ["core"]
--hard --hard ["core", "core"]
[]

Tip

Option and Argument objects are immutable and may be reused in different commands. It's a great way to reduce copypaste.

Commands

Commands are instances of the Command class. They could be created directly and attached to the groups later. For hierarchical CLIs it's simpler to use the Group.add_command() method, which combines the creation and attaching.


class Command(func, *, help=None, info=None, usage=None, epilog=None, prog=None, add_help=True)

  • func - command handler
  • help - short help string. Shown in the commands list of a parent group help.
  • info - long description
  • usage - usage string. By default, it's generated automatically.
  • epilog - epilog message
  • prog - program name. Part of the default usage string.
  • add_help - add --help action

Groups

Similar the to commands, groups are created by instantiating the Group class or by calling the Group.add_group() method. Groups may have own options and handlers.


class Group(*, help=None, info=None, usage=None, epilog=None, prog=None, title=None, headline=None, metavar=None, default_func=..., add_help=True)

  • help - short help string. Shown in the commands list of a parent group help.
  • info - long description
  • usage - usage string. By default, it's generated automatically.
  • epilog - epilog message
  • prog - program name. Part of the default usage string.
  • title - title for the list of own commands. Default is set in Configuration.
  • headline - description for the list of own commands
  • metavar - meta-variable name for own commands
  • default_func - default handler if no command was choosen. By default, prints own help. If set to None, choosing command is required.
  • add_help - add --help action

Most of the Group and Command settings are presentational.

See what they mean

Group

usage: PROG GROUP_METAVAR ...

GROUP_INFO

GROUP_TITLE:
  GROUP_HEADLINE

GROUP_METAVAR
  command      COMMAND_HELP

EPILOG

Command

usage: COMMAND_USAGE

COMMAND_INFO

SECTION_TITLE:
  SECTION_HEADLINE

  --option OPTION_METAVAR  OPTION_HELP

EPILOG

Group.add_group(name, *args, **kwargs)

Group.add_command(name, *args, **kwargs)

  • name - group or command name as seen on the command line. May be a tuple to provide aliases
  • args, kwargs: Forwarded to the Group / Command class

Assembling CLI from separate groups and commands is possible too:

ping.py
from paramspecli import Command

def ping():
    pass

cmd = Command(ping)
main.py
from paramspecli import Group
from . import ping

cli = Group()
netgroup = Group()

netgroup["ping"] = ping.cmd
# aliases are supported too
cli["net", "network"] = netgroup

It might be more readable to define all group's nodes at once:

cli.nodes = {
    "ping": ping.cmd,
    ("net", "network"): netgroup,
}

Tip

Same Command objects may be reused in different groups. It's a simple way to make a top-level shortcut for some five-groups-deep command.

Parse result

Parsing the command line and executing handlers are separate steps. Inside the parse() method, paramspecli 'compiles' itself to the argparse.ArgumentParser, runs it to process the command line, then composes the final Route object.

Route contains a sequence of handlers. It's a sequence because groups may have own handlers too. For convenience, Route is callable and calling it invokes handlers one by one.

Tip

Parse and execute separation allows to garbage collect CLI objects and free resources in long running programs. For this to happen, CLI code should reside in a separate function:

def process_commandline():
    cli = Group()
    cli.add_group(...)
    # etc
    return cli.parse()

def main():
    route = process_commandline()
    # CLI object are no longer exist at this point
    route()

Group.parse(input=None, *, config=None, context=None)

Command.parse(input=None, *, config=None, context=None)

  • input - Sequence of strings to parse. Defaults to sys.argv if None.
  • config - argparse 'compilation' settings
  • context - Optional context object to communicate between groups and commands

Handlers may also be examined and called manually:

route = cli.parse("net ping -t -n 10 127.0.0.1".split())

# Invoke the whole handlers sequence
route() # # Pinging 127.0.0.1 ...

# Invoke the final handler only
print(route[-1]) # Handler(func=<function ping at 0xDEADBEEF>, arguments=['127.0.0.1'], options={'until_stopped': True, 'count': 10})
route[-1]() # Pinging 127.0.0.1 ...

# Invoke handlers manually
for handler in route:
    print(handler)
    handler()

# Or even super manually
for handler in route:
    if handler.func:
        handler.func(*handler.arguments, **handler.options)