Skip to content

Arguments, Options and Flags

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(name, help="", default=UNSET, type=None, choices=None, metavar=None, variadic=False)

Positional, and required unless given a default:

Argument("email") # required
Argument("email", default=None) # optional

That 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”.

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.

Terminal window
sillo files:check a.py b.py c.py # ["a.py", "b.py", "c.py"]
Option(name, help="", default=UNSET, type=None, choices=None,
metavar=None, short=None, multiple=False, required=False)

Named, and takes a value:

Terminal window
--limit 50
--limit=50
-l 50
-l50
ParameterEffect
shortA one-character alias, -l. More than one character raises.
multipleRepeatable; values collect into a list. Defaults to [].
requiredFail when absent, even though it is an option.
Option("queue", short="q", multiple=True, help="Queue to consume. Repeatable")
Terminal window
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")
Terminal window
--staff # True
# False

Give a flag default=True and it is turned off by the --no- form:

Flag("git", default=True, help="Initialise a git repository")
Terminal window
--no-git # False
# True

Both 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 value
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 int

choices 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, csv
  • --name value and --name=value
  • -n value and -nvalue
  • bundled short flags: -abc is -a -b -c
  • -- stops option parsing; everything after it is self.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.

InputMessage
--unknownunknown option --unknown
-zunknown option -z
--limit with nothing after--limit needs a value
a missing required argumentmissing argument <EMAIL>
a missing required optionmissing required option --queue
a surplus positionalunexpected argument 'extra'

All of them exit 2, and print the usage line for the command plus how to see its help.

Dashes are permitted and are what appears on the command line; lookups accept either spelling:

Flag("dry-run") # --dry-run
self.flag("dry_run") # reads it
self.flag("dry-run") # also reads it

metavar renames the placeholder in the help without renaming the parameter:

Argument("identifier", metavar="EMAIL_OR_USERNAME")

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.