Declaring what a command accepts (positional arguments, value options and boolean flags) plus type conversion, choices, repetition, variadics and the parsing rules.
A command lists what it accepts explicitly:
from sillo.console import Argument, Command, Flag, Option
class ListUsers(Command): name = "user:list" help = "List users, newest first"
arguments = [ Argument("email", help="Address to look up"), Option("limit", type=int, default=50, short="l", help="How many to show"), Flag("staff", help="Only administrators"), ]The three kinds map onto the three shapes a command line has. An Argument is
positional. An Option takes a value. A Flag is on or off and never consumes
the token after it.
Argument
Section titled “Argument”Argument(name, help="", default=UNSET, type=None, choices=None, metavar=None, variadic=False)Positional, and required unless given a default:
Argument("email") # requiredArgument("email", default=None) # optionalThat distinction is why the absence of a default is its own sentinel rather
than None. None is a perfectly good default for an optional argument, so it
cannot also mean “no default was given”.
Variadic
Section titled “Variadic”Argument("paths", variadic=True, help="Files to process")Collects every remaining positional token into a list. A variadic argument is never required (absent, it is an empty list) and must be declared last. Declaring one before another argument raises at registration, naming both.
sillo files:check a.py b.py c.py # ["a.py", "b.py", "c.py"]Option
Section titled “Option”Option(name, help="", default=UNSET, type=None, choices=None, metavar=None, short=None, multiple=False, required=False)Named, and takes a value:
--limit 50--limit=50-l 50-l50| Parameter | Effect |
|---|---|
short | A one-character alias, -l. More than one character raises. |
multiple | Repeatable; values collect into a list. Defaults to []. |
required | Fail when absent, even though it is an option. |
Option("queue", short="q", multiple=True, help="Queue to consume. Repeatable")sillo queue:work -q mail -q default # ["mail", "default"]Each parse gets a fresh list, so a repeated option’s default never accumulates values across two invocations of the same declaration.
Flag(name, help="", default=False, short=None)On or off, and never consumes the next token:
Flag("staff", help="Only administrators")--staff # True # FalseFlags that default to on
Section titled “Flags that default to on”Give a flag default=True and it is turned off by the --no- form:
Flag("git", default=True, help="Initialise a git repository")--no-git # False # TrueBoth spellings are always registered, so --staff and --no-staff both parse
whichever way the default points. The help prints the one that changes the
default, because that is the only one worth typing.
Passing a value to a flag is an error rather than being ignored:
--staff is a flag and takes no valueConversion and validation
Section titled “Conversion and validation”Option("port", type=int, default=8000)Option("root", type=Path)Option("rate", type=float, default=1.0)type is any callable taking a string. Anything raising ValueError or
TypeError on bad input works, which covers int, float, pathlib.Path and
most enum constructors. Failures become usage errors naming the value and the
type:
port: 'eight' is not a valid intchoices is checked after conversion, so it compares converted values:
Option("format", default="table", choices=["table", "json", "csv"])format: 'yaml' is not one of table, json, csvWhat the parser accepts
Section titled “What the parser accepts”--name valueand--name=value-n valueand-nvalue- bundled short flags:
-abcis-a -b -c --stops option parsing; everything after it isself.extra
Bundling and inline values interact the way you would expect: in a cluster,
everything after the first option that takes a value is that value. -c8 is
--concurrency 8, and -fc8 is --force --concurrency 8.
Errors
Section titled “Errors”| Input | Message |
|---|---|
--unknown | unknown option --unknown |
-z | unknown option -z |
--limit with nothing after | --limit needs a value |
| a missing required argument | missing argument <EMAIL> |
| a missing required option | missing required option --queue |
| a surplus positional | unexpected argument 'extra' |
All of them exit 2, and print the usage line for the command plus how to see
its help.
Naming
Section titled “Naming”Dashes are permitted and are what appears on the command line; lookups accept either spelling:
Flag("dry-run") # --dry-runself.flag("dry_run") # reads itself.flag("dry-run") # also reads itmetavar renames the placeholder in the help without renaming the parameter:
Argument("identifier", metavar="EMAIL_OR_USERNAME")Why not argparse
Section titled “Why not argparse”Two reasons, both about control. The console renders its own help and phrases
its own errors, which argparse would have to be fought for. And argparse calls
sys.exit on a bad argument. A test cannot catch that cleanly, and an
embedding application should not have it happen underneath it. Here a parse
failure is a UsageError, which the console turns into an exit code it
returns.