Quick Start¶
ilcli
is a Python library based on ArgumentParser that provides
the following features:
Subcommand creation support so you can build a complex, well-structured, and standard CLI.
Support for including documentation (man pages, web links, etc.) so users can quickly open different doc directly from the your tool.
REST API automatic conversion: start serving your CLI as a REST API so it can be executed remotely.
Multi-project CLI: build a general CLI whose subcommands will be pulled from different projects so you can unify tools.
The most important bit of ilcli
is the Command
class. All commands you define should be children of this class.
The minimal example¶
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class mycmd(ilcli.Command):
def _run(self, args):
self.out('Hello World!')
return 0
exit(mycmd().run())
This creates a command without options (just -h
):
$ python examples/minimal.py
Hello World!
$ python examples/minimal.py -h
usage: mycmd [-h]
optional arguments:
-h, --help show this help message and exit
Note that the name of the class is taken as program name. You can
tweak this by setting the class attribute name
to whatever value
you need.
Arguments and options¶
Arguments and options should be added by overriding
_init_arguments()
method. In this method, you can use
self.add_argument()
method for adding arguments in exactly the
same way as you would do with ArgumentParser.add_argument()
method sinc it accepts the same parameters.
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class mycmd(ilcli.Command):
def _init_arguments(self):
self.add_argument('message', help='your message')
self.add_argument(
'-u', '--upper', action='store_true', help='message in upper case'
)
def _run(self, args):
msg = args.message
if args.upper:
msg = msg.upper()
self.out(msg)
return 0
cmd = mycmd()
cmd.run() # sys.argv by default
cmd.run(['me too'])
cmd.run(['me too', '-u'])
cmd.run(['--upper', 'me too!!'])
Now our command has a positional argument and one option. If not provided, it will fail:
$ python examples/args_opts.py
usage: mycmd [-h] [--upper] message
mycmd: error: the following arguments are required: message
Note that the arguments and options can be provided as part of the
run()
method:
$ python examples/args_opts.py "I like command line interfaces!"
I like command line interfaces!
me too
ME TOO
ME TOO!!
Validate arguments¶
Although ArgumentParser
provides some mechanisms for checking
the parameters (such as type, choices, etc.), sometimes is useful
to perform extra validation on the arguments which are application
specific. For that, you can use _validate_arguments()
method:
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class mycmd(ilcli.Command):
def _init_arguments(self):
self.add_argument('message', help='your message')
self.add_argument(
'-u', '--upper', action='store_true', help='message in upper case'
)
def _validate_arguments(self, args):
msg = args.message.lower()
if 'ilcli' in msg:
self.err("Please do not take ilcli's name in vain")
return 1
if 'help' in msg:
self.out('You say help? Sure!')
self.parser.print_help()
return 0
def _run(self, args):
msg = args.message
if args.upper:
msg = msg.upper()
self.out(msg)
return 0
exit(mycmd().run())
$ python examples/args_opts_validation.py -u 'Hello!'
HELLO!
Note that any extra argument will be treated as an error (the default
ArgumentParser
behaviour) which will make the program fail:
$ python examples/args_opts_validation.py -u 'Hello!' --something more
usage: mycmd [-h] [-u] message
mycmd: error: unrecognized arguments: --something more
It is worth to mention that self._run()
will not be executed if
self._validate_arguments()
returns something, so we can make sure
that once self._run()
is executed, the arguments are validated
at our semantics level:
$ python examples/args_opts_validation.py 'help me'
You say help? Sure!
usage: mycmd [-h] [-u] message
positional arguments:
message your message
optional arguments:
-h, --help show this help message and exit
-u, --upper message in upper case
help me
$ python examples/args_opts_validation.py 'ilcli is cool!'
Please do not take ilcli's name in vain
In certain cases, you might want to allow the user to provide multiple arguments at the command line interface that actually cannot be specified for multiple reasons. For example, too many possible argument combinations or your application needs to get values to be replaced in a template:
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class mycmd(ilcli.Command):
extra_args = True
def _init_arguments(self):
self.add_argument('message', help='your message')
def _validate_extra_arguments(self, args):
self.kv = dict([args[0].split('=')])
def _run(self, args):
msg = args.message.format(**self.kv)
self.out(msg)
return 0
exit(mycmd().run())
$ python examples/args_extra_opts_validation.py '{name} is cool!' name=ilcli
ilcli is cool!
Subcommands¶
One interesting feature that ilcli.Command
class provides is
that it can be composed by other commands forming sub-commands in a
similar way as tools like apt
or Chef knife
are
structured. The result is a tree-like structure with two types of
commands:
Node commands: commands that define subcommands. By default, their arguments will be passed to their children as common arguments.
Leaf commands: commands that do not define subcommands. They can define their own arguments and, by default, they will inherit the parent’s ones.
Subcommands are defined as a class attribute:
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class ssh(ilcli.Command):
"""
ssh into a hostname
"""
def _run(self, args):
self.out('SSH into: %s', args.hostname)
class ping(ilcli.Command):
"""
ping a hostname
"""
def _run(self, args):
self.out('Ping to: %s', args.hostname)
class net(ilcli.Command):
"""
networking tools
"""
subcommands = [ssh, ping]
def _init_arguments(self):
self.add_argument('hostname', help='target IP or hostname')
class ls(ilcli.Command):
"""
list a file/directory
"""
def _run(self, args):
self.out('ls: %s', args.path)
class rm(ilcli.Command):
"""
remove a file/directory
"""
def _run(self, args):
self.out('rm: %s', args.path)
class fs(ilcli.Command):
"""
file-system tools
"""
subcommands = [ls, rm]
def _init_arguments(self):
self.add_argument('path', help='path to file/dir')
class mytools(ilcli.Command):
subcommands = [net, fs]
def _init_arguments(self):
self.add_argument(
'-v', '--verbose', action='store_true', help='increase verbosity'
)
exit(mytools().run())
ilcli
will automatically build a tree hierarchy with the command
that is firstly created as root and recursively creates the command
tree. Note that each command can define its own set of arguments.
$ python examples/args_subcommands.py -h
usage: mytools [-h] {net,fs} ...
positional arguments:
{net,fs}
net networking tools
fs file-system tools
optional arguments:
-h, --help show this help message and exit
$ python examples/args_subcommands.py net -h
usage: mytools net [-h] {ssh,ping} ...
positional arguments:
{ssh,ping}
ssh ssh into a hostname
ping ping a hostname
optional arguments:
-h, --help show this help message and exit
$ python examples/args_subcommands.py net ssh -h
usage: mytools net ssh [-h] [--verbose] hostname
positional arguments:
hostname target IP or hostname
optional arguments:
-h, --help show this help message and exit
--verbose, -v increase verbosity
Argument inheritance¶
As we have seen in the previous example, it can also define arguments
that will be passed to the children. In general, when a command
defines arguments, then they will propagated to all leaf commands
under that command. This is ilcli
default behaviour although it
might be tweaked using the following mechanisms:
inherit_arguments = False
: if it is defined as a class attribute in the command, no arguments will be passed to this command.#! /usr/bin/env python # -*- coding:utf-8; mode:python -*- import ilcli class ls(ilcli.Command): """ list a file/directory """ inherit_arguments = False def _init_arguments(self): self.add_argument( '-v', '--version', action='store_true', help='show version' ) def _validate_arguments(self, args): print('validate arguments at "ls"') def _run(self, args): print('Running ls') class rm(ilcli.Command): """ rm a file/directory """ def _validate_arguments(self, args): print('validate arguments at "rm"') def _run(self, args): print('Running rm') class fs(ilcli.Command): subcommands = [ls, rm] def _init_arguments(self): self.add_argument( '-v', '--verbose', action='store_true', help='increase verbosity' ) def _validate_arguments(self, args): print('validate arguments at "fs"') exit(fs().run())
Thus, subcommands could define their own arguments without clashing with parent’s:
$ python examples/not_inherit_args.py ls -h usage: fs ls [-h] [-v] optional arguments: -h, --help show this help message and exit -v, --version show version
$ python examples/not_inherit_args.py rm -h usage: fs rm [-h] [-v] optional arguments: -h, --help show this help message and exit -v, --verbose increase verbosity
This is very useful in situations when you want to have a subcommand within a specific command (because makes sense there or it is related to it) but all the previous arguments do not apply to it. For exmplate, you my have:
$ mytool members list --org myorg $ mytool members create --org myorg new-member $ mytool members request --url https://api.server.com
In this case,
list
andcreate
share the argument--org
that comes frommembers
. However,request
a new member requires just the URL (from where you probably get theorg
parameter) and still makes sense havingrequest
undermembers
because it is related to it.An important aspect when you use this feature is that the parents’ validation code is not executed at all. Using this flag you will need to implement a new validation argument functions, if needed. See the following execution examples:
$ python examples/not_inherit_args.py rm -v validate arguments at "fs" validate arguments at "rm" Running rm
$ python examples/not_inherit_args.py ls -v validate arguments at "ls" Running ls
_init_arguments()
: the parent could decide what arguments pass to its children by implementing it in the_init_arguments()
method. The parent keeps a list of subcommands atself._subcommands
The argument inheritance mechanisms implies a few limitations:
By default, since the arguments actually reside on leaf command parsers, there can not be conflicted names. That is, 2 argument names can not be defined at different levels of the same path of the hierarchy. However, you can change this default behaviour and allow argument overriding. See Argument overriding.
Commands that define sub-commands should not define
_run()
. By default, it will never be executed as only leaf commands are taking into account. However, since children has a reference to the parent command they might run parent commands as part as their run.
Removing arguments¶
A Command
might inherit an argument you don’t want,
either from a parent class or being a sub-command of another
Command
. These can be ignored via ignore_arguments
,
though if you are ignoring lots of things you may want to reevaluate
your class/command heirarchy. Arguments are ignored by their switch
(-f/--foo
) or the name given for positional arguments.
ignore_arguments = []
: list of switches/positional arguments to ignore, identified by their switch (both-f
and--foo
will work) or the name given to positional arguments.
$ python examples/removing_arguments.py -h
usage: parent [-h] {firstdemocommand,seconddemocommand,thirddemocommand} ...
positional arguments:
{firstdemocommand,seconddemocommand,thirddemocommand}
firstdemocommand
seconddemocommand
thirddemocommand
optional arguments:
-h, --help show this help message and exit
firstdemocommand
ignores the -b/--bar
switch, and adds the --foo
switch
$ python examples/removing_arguments.py firstdemocommand -h
usage: parent firstdemocommand [-h] [--foo FOO] bat
positional arguments:
bat
optional arguments:
-h, --help show this help message and exit
--foo FOO
seconddemocommand
inherits from firstdemocommand
and both ignores
-b/--bar
switch and the --foo
switch.
$ python examples/removing_arguments.py seconddemocommand -h
usage: parent seconddemocommand [-h] bat
positional arguments:
bat
optional arguments:
-h, --help show this help message and exit
thirddemocommand
inherits from firstdemocommand
and ignores the bat
positional argument.
$ python examples/removing_arguments.py thirddemocommand -h
usage: parent thirddemocommand [-h] [--foo FOO] [-b BAR]
optional arguments:
-h, --help show this help message and exit
--foo FOO
-b BAR, --bar BAR
Documentation support¶
Although using -h
in your commands there is plenty of information
to know how to use your tool, sometimes is also good to provide
extended information.
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class ls(ilcli.Command):
"""
list a file/directory
"""
man_page = 'ls'
man_section = 1
exit(ls().run())
This automatically add an option --doc
in the command where it
class attributes has been defined:
$ python examples/doc.py net -h
usage: ls [-h] [--doc]
optional arguments:
-h, --help show this help message and exit
--doc open documentation
In this case, if --doc
is used it will open the man page
specified. Note that this is an action that, once is finished, nothing
else is executed. Also note that the class attributes might be defined
at any point of the command hierarchy, so you could have more general
documentation in top-level commands, and more concrete one in the leaf
commands:
$ mycmd [--doc] subcommand1 [--doc] ...
Argument overriding¶
ilcli
will fail if you define the same argument on a single
inheritance path. However, ArgumentParser
allows to resolve this
conflicts by setting conflict_handler=resolve
. You can set custom
arguments to be used in all parsers created by ilcli
internally by
using the class property parser_args
.
Here it is an example of how to allow argument overriding:
#! /usr/bin/env python
# -*- coding:utf-8; mode:python -*-
import ilcli
class subcommand1(ilcli.Command):
"""
--foo does different things
"""
def _init_arguments(self):
self.add_argument('-o', '--foo', help='new foo help')
def _run(self, args):
self.out('called with: {}'.format(args))
class subcommand2(ilcli.Command):
"""
--foo does different things
"""
def _init_arguments(self):
self.add_argument('-f', '--far', help='new far help')
def _run(self, args):
self.out('called with: {}'.format(args))
class subcommand3(ilcli.Command):
"""
This command replaces the parent definition of -f/--foo entirely
"""
def _init_arguments(self):
self.add_argument('-f', '--foo', help='new foo help')
def _run(self, args):
self.out('called with: {}'.format(args))
class toplevel(ilcli.Command):
subcommands = [subcommand1, subcommand2, subcommand3]
parser_args = {'conflict_handler': 'resolve'}
def _init_arguments(self):
self.add_argument(
'-f', '--foo', help='old foo help'
)
self.add_argument(
'-b', '--bar', help='old bar help'
)
exit(toplevel().run())
Note that toplevel
subcommands defines parser_args
. Now
ilcli
will not fail and, as ArgumentParser
does, subcommands
will override conflicts:
$ python examples/args_conflict_subcommands.py subcommand1 -h
usage: toplevel subcommand1 [-h] [--foo FOO] [-f F] [--bar BAR]
optional arguments:
-h, --help show this help message and exit
--foo FOO, -o FOO new foo help
-f F old foo help
--bar BAR, -b BAR old bar help
$ python examples/args_conflict_subcommands.py subcommand2 -h
usage: toplevel subcommand2 [-h] [-f FAR] [--foo FOO] [-b BAR]
optional arguments:
-h, --help show this help message and exit
-f FAR, --far FAR new far help
--foo FOO old foo help
-b BAR, --bar BAR old bar help
$ python examples/args_conflict_subcommands.py subcommand3 -h
usage: toplevel subcommand3 [-h] [-f FOO] [-b BAR]
optional arguments:
-h, --help show this help message and exit
-f FOO, --foo FOO new foo help
-b BAR, --bar BAR old bar help
REST API support¶
TBD
Multi-project CLI¶
TBD