Argument Parsing
The toolbelt framework has built-in argument parsing, with the help of Python’s built-in argparse module. The parser in the toolbelt framework builds on top of this to enable powerfull command spesification using only Python syntax.
Defining an argument
In commandline programs, there are two types of arguments: arguments and options. Arguments are like python args, while options are like python kwargs. This fact is reflected in the way commands are defined in the toolbelt.
In the toolbelt, the arguments to your program is defined with the normal python syntax. So if you already know python, there is not much else to know. Take the following example:
#!/usr/bin/env python3
from argus_cli import register_command, run
@register_command()
def funky_command(name, say_something_extra=False):
print(f"Hello there, {name}!")
if say_something_extra:
print("You look like a fish!")
if __name__ == "__main__":
run(funky_command)
This command has one argument and one option, and this is reflected in the generated command:
$ ./my_command.py Mark --say-somehting-extra
Hello there, Mark
You look like a fish
As demonstrated here, both the argument and keyword argument from the python function is automatically translated and used in the commandline application.
Using type-checking
Sometimes users use the wrong type for their input, and that can be a pain in the ass to handle. Sometimes a lot of time is spent on securing the userinput and giving good messages, rather than writing business logic. The toolbelt framework has you covered on this as well, with it’s automatic and extendable type handling using python’s type-hint syntax.
Any callable or object type that is added to the type-hints is passed to argparse, and used to type-check user input. Let’s take a look at this functionality by modifying our previous example.
#!/usr/bin/env python3
from argus_cli import register_command, run
@register_command()
def funky_command(name: str, age: int = None):
print(f"Hello there, {name}!")
if age is not None:
print(f"You are {age} years old. Wow!")
if __name__ == "__main__":
run(funky_command)
When running the command now, the arguments and options will be validated automatically.
$ ./my_command.py Mark --age No
error: argument --age: invalid int value: 'No'
If the correct type is used instead, the command will work as expected.
$ ./my_command.py Mark --age 4
Hello there, Mark!
You are 4 years old. Wow!
Special cases
There are a few special cases of argument parsing that is worth being aware of. These cases are quite useful to help you create your commands.
Date and time
datetime
objects are automatically handled and allows users to write natural
language or normal dates in any locale.
Some working examples:
now
- Yields the current date and time
"5 weeks ago"
- Yields the date and time for 5 weeks ago
2020-01-01
- Yeilds the 1st of January 2020.
Lists
Lists of data are currently implemented in a non-stanard way, using the docstring. To mark an argument as a list, the following format is used:
def my_command(ages: int = None):
"""A great command.
:param ages list:
"""
pass
In this example, the user can input a list of integers. Users can input the list the same as most other command line programs, like so:
--ages 22 14 42
--ages 22 --ages 14 --ages 42
Be aware that if the option is specified as the last option before the program
arguments, a --
has to be appended before the last argument. This indicates
the end all arguments, and the following is just options.
Dicts
Somtimes, it might be usefull to just straight up give a map as an input. If
the argument type is specified as a dict
, the argument parser will try to
parse the input as JSON or YAML.
Booleans
A lot of programs use flags to enable or dissable features. To create a flag,
use the boolean
type in the function arguments with a default value of
either True
or False
.
Depending on the default value, one of two flags will be created. In this
example, we have a argument called dry_run
.
If the default value is
False
, the option in the commandline will be called--dry-run
.If the default value is
True
, the option in the commandline will be called--no-dry-run
.
Files
File input is usefull in noumerous cases. Templates, data, etc, can all be passed by files, and piping data from one commandline command to another also use files.
Files have automatic handling in the toolbelt framework, and can be accessed with some imports. These are:
argparse.FileType
- Specify with the argumentr
orw
for a writable or readable file. If the input is-
,stdin
will be the file that is opened. This allows for piping.
sys.stdin
- Same asFileType
, but the default input isstdin
, which allows for piping by default.
Choices
Sometimes a curated list of pre-defined inputs is desirable. An example of this
can be a set of input types. This is done by using a list
or tuple
in
the type hint.
def my_funky_func(sex: ["male", "female", "other"]):
pass
This will make sure the user can only input one of these three choices. Any other input, and they’ll get an error displaying the legal inputs.
Adding documentation to commands
Just like the type-checking, the framework also automatically uses the docstring of your function to use as help-text for users. This format follows the Sphinx format, like so:
def func(some, stuff=None):
"""This is a one-two sentence intro to the command. It shows up as a
short help text.
This is the body of the command. This can be as long as you'd like.
:param some: This will show up as help text for the argument some
:param stuff: Same with this, but just for the option --stuff
"""
This will be automatically used whenever a user writes --help
when calling
the command.