Using the argparseDecorator
The easiest way to install argparseDecorator is with pip:
$ pip import argparseDecorator
Alternativly the sources can be downloaded directly from the Github page.
Once argparseDecorator has been installed it can be used like this.
First, if you are using a python version previous to 3.10 it is a good idea to add
from __future__ import annotations
at the top of your file. This ensures that the annotations used by the argparseDecorator are handled as strings and not as Python Types. This is not only faster, but some uses of annotations by the decorator will cause errors otherwise.
Next the argparseDecorator is imported. The best way is to
from argparsedecorator import *
to import all required types, including the annotation objects. Take a look at the __init__.py file to see which names are pulled into the namespace
Now the decorator can be instantiated and its command decorator can be used to mark a function as a command.
In this short example the command
reverse is created, which takes a single argument named
cli = ArgParseDecorator() @cli.command def reverse(word): print(word[::-1])
With this a command can be executed like this:
cli.execute("reverse foobar") raboof
The ArgParseDecorator class
To use the argparseDecorator an instance of the
class has to be created.
cli = ArgParseDecorator()
command() is a Decorator that can mark any function
or method as a command. There can be any number of decorated functions.
@cli.command def foobar(word): ...
Any such decorated function is called by
execute(cmdstring) when the
cmdstring contains the command.
command decorator can be used with or without parenthesis.
Take a look at the
ArgParseDecorator API to see what optional
arguments can be given when instantiating the class.
Note that any keyword argument that ArgParseDecorator does not handle itself
will be passed onto the the underlying
argparse.ArgumentParser class. Some options like
allow_abbrev might be useful in some cases.
Instead the ArgParseDecorator by default adds a
help command to the CLI which will provide a list of all
supported commands when called by itself or a detailed command description when supplied with a command name argument.
To override this behaviour and instead use the
-h/--help system of ArgumentParser set
when instantiating the ArgParseDecorator
cli = ArgParseDecorator(helpoption="-h")
If no help is wanted set
cli = ArgParseDecorator(helpoption=None)
Sometimes it makes sense to split commands into multiple subcommands. This is supported by the argparseDecorator. To define a subcommand just add an underscore between the main command and the subcommand in the function name.
For example the commands to switch an LED on or off could be implemented like this
@cli.command def led_on(): ... @cli.command def led_off(): ...
With this the argparseDecorator now understands the two commands
led on and
led off and the respective
functions are called.
Commands with Hyphens
To create a command containing a hypen
get-info ... a double underscore is used
in the command name, e.g.
@cli.command def get__info(): ... cli.execute("get-info")
Using ArgParseDecorator to Decorate Class Methods
When using this library to decorate methods within a class there is one caveat:
class MyCLI: cli = ArgParseDecorator() @cli.command def cmd(self, arg1, arg2, ...): ...
To mark methods as commands the ArgParseDecorator must be instantiated as a class variable.
But as a class variable it does not have access to any data from a MyCLI instance, especially not to the
To correctly call the
cmd function from
a reference to
self must be given, e.g. like this:
class MyCLI: cli = ArgParseDecorator() @cli.command def cmd(self, arg1, arg2, ...): ... def execute(self, cmdline): cli.execute(cmdline, self)
cli.execute() is wrapped in a method and how it passes a reference
to self to the ArgParseDecorator.
An alternative method would be the use of inner functions like this:
class MyCLI: def __init__(self): self.setup_cli() def setup_cli(self): cli = ArgParseDecorator() self.cli = cli # store as instance variable @cli.command def cmd(arg1, arg2, ...) self.do_something_with(arg1) def execute(self, cmdline) self.cli.execute(cmdline)
argparseDecorator makes heavy use of type annotaions to pass additional information to the ArgumentParser. This includes a number of custom types which are used to provide additional information about the arguments.
For example the following
command will add up a list of numbers or, if
--squared is added to the command,
will calculate the sum of the squares.
@cli.command def add(values: OneOrMore[float], squared: Option = False) -> None: if squared: values = [x*x for x in values] print sum(values)
OneOrMore[float] tells the decorator, that
values must have at least one value and
that it is accepting only valid numbers (int or float).
Option = False marks
as an option (starting with
--) and that it has the the value
True if set on the
command line (overriding the default) or
False (the default) otherwise.
add command can now be used like this
cli.execute("add 1 2 3 4") 10 cli.execute("add --squared 1 2 3 4") 30
Take a look at the
annotations API for all supported annotations and more examples.
Flags and Options
The argparse library only destinguishes between position arguments and flags. Flags are
all arguments starting with either a single or a double hyphen
As python identifiers must not start with a hyphen there must be a way to tell the argparseDecorator that the argument of a command is a flag.
This is done with the
Option annotations. The
Flag tells the the decorator
to internally add a single
- to the argument.
Option does the same, but with a double hyphen
If an Flag or Option should have multiple names, e.g. a long Option name like
--foobar and a short
Flag name like
:alias --foobar: -f must be added to the docstring of the command function.
See Aliases below for details.
This can be done by just annotating the argument with the required type in the normal Python fashion:
@cli.command def add(value1: float, value2: float): print(value1 + value2) @cli.execute("add 1 2.5") # output "3.5" @cli.execute("add apple banana) # causes ValueError
Some of the special annotations of argparseDecorator can also specify the type in brackets to make the code more readable:
@cli.command def sum(values: OneOrMore[float]): print(sum(values)
is almost equivalent to
@cli.command def sum(values: float | OneOrMore): print(sum(values)
but it is nicer to read and it also tells any type-checker that
values is a
of floats and not a union of a float and a generic List.
Take a look at the argparse documentation for more info what types are possible and how to implement custom types.
Number of Values
annotations has a number of Annotation Types to tell the ArgParseDecorator (and the
arparse.ArgumentParser) how many values a command argument expects.
If nothing is specified a single value is expected for the argument.
These annotations are supported:
The argparseDecorator also uses the docstring of a decorated function to get a description of the command that is used for help and some additional meta information about arguments that can not be easily written as annotations.
argparseDecorator uses the docstring of a decorated function for description of the command and its arguments, as well as some additional data that can not be set via the signature and its annotations.
If a decorated function has a docstring its content is used as the help text for the command:
@cli.command def foo(bar): """The foo command will foo a bar.""" ... cli.execute("help foo")
will create the output:
usage: foo bar The foo command will foo a bar. positional arguments: bar
The docstring can be used add small help strings to arguments. For this a line in the format
:param argname: short description
is added to the docstring. Example:
@cli.command def foo(bar): """ The foo command will foo a bar. :param bar: Which bar to foo """ ... cli.execute("help foo")
... positional arguments: bar Which bar to foo
If the help for an argument starts with
SUPPRESS, then this argument is hidden in the help. This might
be usefull to hide some unofficial options used for example for debugging.
ArgumentParser allows for flags (arguments starting with
--) to have multiple names, e.g.
-f. To support multiple names for the same argument the
:alias directive can be used
in the docstring. It has the format
:alias argname: -name1, --name2
Here is an example on how this can be used:
@cli.command def foobar(flag: Option = False): """ :alias flag: -f """ print(flag) cli.execute("foobar --flag") cli.execute("foobar -f")
the last two lines are identical and will print
While the argname given to
:alias will work with or without leading hyphens, the actual alias(es) must have
either one or two leading hyphens.
ArgParseDecorator supports the
Choices annotation in the signature to restrict the value of an argument
to a list of predefined values. As the syntax somewhat ugly for a list of strings (they have to be encapsuled
Literal annotation to keep type checkers happy) there is an alternative using a docstring with
:choices argname: opt1, opt2, ...
@cli.command def foobar(value): """ Only allow values foo, bar, 1 or 2 :choices value: 'foo', 'bar', 1, 2 """ print(flag) cli.execute("foobar foo") cli.execute("foobar 2") cli.execute("foobar baz") # this will raise an Exception
The list of choices is parsed using the python
It can be anything that returns a sequence of items, e.g.
range(1,4) would be a valid value for choices.
When ArgumentParser generates help messages, it needs some way to refer to each expected argument.
By default, ArgumentParser objects use name of the argument as the
name of each object.
By default, for positional argument actions, the dest value is used directly, and for
optional argument actions, the dest value is uppercased. For example
def foobar(datetime: Option | Exactly2[str]):
will have a help output of
usage: foobar [--datetime DATETIME DATETIME] optional arguments: --datetime DATETIME DATETIME
which does look ugly and is not as descriptive. Here the
:metavar directive can be used to assign more
descriptive names to the arguments of
def foobar(datetime: Option | Exactly2[str]): """ :metavar datetime: DATE, TIME
will have a help output of
usage: foobar [--datetime DATE TIME] optional arguments: --datetime DATE TIME
The number of metavar names must match the number of parameters an argument takes.
Executing a Command Line
ArgParseDecorator has been set up with all decorated
functions or methods it can be used to execute arbitrary command lines.
This is done by calling the
with a command line string. The command line can come directly from the prompt like in the example below, or it
could come for example from a ssh connection.
cli = ArgParseDecorator() ... cmdline = input() cli.execute(cmdline)
Internally the command line is first split into separate tokens using the
library (in POSIX mode). These tokens are then passed to the internal
argparse.ArgumentParser instance and, if there are no errors, the command function
(the first word of the command line) is called with all arguments.
Execute Async Code
A typical use case for a command line interface is via a remote ssh connection. These are usually implemented
asyncio code. ArgParseDecorator supports this with the
execute_async() method which is functionally equivalent to
execute(), but is implemented as a coroutine which can be awaited.
Here is a simple example for a sleep command that will pause the cli while other stuff could continue to run:
import asyncio from argparsedecorator import * cli = ArgParseDecorator() @cli.command async def sleep(n: float): await asyncio.sleep(n) async def runner(): await cli.execute_async("sleep 1.5") if __name__ == "__main__": asyncio.run(runner())
Take a look at the ssh_cli.py demo for a more complex module using argparseDecorator in an asyncio application.
Using sys.argv as Input
Instead of a single string the execute and execute_async methods can also take a list of strings (or any
Iterator), where the first item is the name of the command and all following items
are the arguments.
This is useful if you - instead of implementing a full CLI - just want to parse the command line arguments of a Python
script. A Python script has all its arguments in the system parameter
sys.argv containing the script name. This can be passed directly to execute/execute_async as the
commandline argument. For example, the following script will implement a
--verbose argument for the script:
# testverbose.py import sys from argparsedecorator import * # use helpoption='-h' as the default "help" option does not # work when parsing script arguments. argparser = ArgParseDecorator(helpoption="-h") @argparser.command def testverbose(v: Flag = False): # must be the same name as the script. """ Sample to show script argument parsing. :param v: switch on verbose mode. :alias v: --verbose """ if v: print("chatty mode activated") if __name__ == "__main__": argparser.execute(sys.argv)
# python testverbose.py --verbose chatty mode activated # python testverbose.py --help usage: testverbose [-h] [-v] Sample to show script argument parsing. options: -h, --help show this help message and exit -v, --verbose switch on verbose mode. Process finished with exit code 0
Using the name of the script as the name of the command function allows for the same script to behave differently depending on the name of the script, e.g. by using differently named links to the same Python script.
Using Quotes on the Command Line
ArgParseDecorator uses the
shlex lexer library (in POSIX mode) to split a given
commandline into seperate tokens for the command and the arguments. Arguments containing spaces can be encapsulated
in single or double quotemarks to prevent splitting them into seperate arguments.
However these quotemarks will be removed by shlex. If an argument requires quotes to be preserved they need to
be escaped by a backslash character
\. If a backslash character is part of an argument it has to be escaped
as well like
cli.execute('cmd foo bar') # -> Split into ['cmd', 'foo', 'bar'] cli.execute('cmd "foo bar"') # -> Split into ['cmd', 'foo bar'] cli.execute('cmd "a \'quote\' "') # -> Split into ['cmd', "a 'quote' "] cli.execute('cmd path\\to\\file') # -> Split into ['cmd', 'path\to\file']
If this behaviour is not desired, e.g. when working with lots of Windows paths, then the caller can implement its own lexer (e.g. shlex in the default non-POSIX mode) and pass its result to the execute method (note: shlex implements the Iterator methods and can be passed to execute directly).
See shlex parsing rules for more details on how shlex works in the different modes.
If there is an error parsing the command line (e.g. invalid commands, illegal arguments etc.) an error message is written to sys.stderr.
If a more involved error handling is required, e.g. to translate the error messages or to
do some formatting on them, a special error handler function can be given to
execute() that is called
whenever an error occurs.
The error handler function is called with one argument , an
argparse.ArgumentError exception object.
The string representation of the exception contains the full error message.
def my_error_handler(err: argparse.ArgumentError): print(str(err)) # output the error message to stdout instead of stderr cli = ArgParseDecorator() cli.execute("command", error_handler=my_error_handler) # "command" does not exist causing an error message
The error_handler can be explicitly set to
None. In this case no error message is output but instead an
argparse.ArgumentError is raised which can be caught and acted upon.
while True: try: cmdline = input() cli.execute(cmdline, error_handler=None) except ArgumentError as err: print(str(err))
When executing a command line all output (e.g. help messages) is written by default to the sys.stdout stream and any error message (e.g. invalid syntax) is written to the sys.stderr stream. These are usually the stdout and stderr streams of the shell from where python was started.
As the typical use case for a CLI implemented with ArgParseDecorator is some kind of remote connection, for example a ssh server implementation, there must be a way to redirect the output of the ArgumentParser to the remote connection.
This can be done by passing TextIO Streams for stdout and
stderr to the
This method will then redirect
sys.sterr to the given stream(s) before calling
argparse.ArgumentParser and the command function. After the command has been called and before
returning to the caller
sys.stderr are restored to their original values.
cli = ArgParseDecorator() my_stdout = BufferedWriter() @cli.command def echo(text: str): print(text) cli.execute("echo foobar", stdout=my_stdout) print(stdout.getvalue()) # prints 'foobar'
If any commands require further user input, e.g. for confirmation checks, the sys.stdin can also be redirected to a different stream:
cli = ArgParseDecorator() my_stdin = io.StringIO("yes") @cli.command def delete(): print("type 'yes' to confirm that you want to delete everything") result = input() if result == "yes": print("you have chosen 'yes'") cli.execute("delete", stdin=my_stdin) # will output "you have chosen 'yes'" immediatly