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 argument r or w 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 as FileType, but the default input is stdin, 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.