SubCommand CLI Parser¶
SubCommand is a simple and concise SubCommand parser to assist CLI developers in creating relatively complex sub commands.
The SubCommand project fits into a single file, which is less than 500 lines. The interface consists of 3 Classes and a few decorators and that is it.
Doesn’t argparse already support sub commands?
I does, but in practice it can be quite complex and it makes keeping the args and the subcommands together and easily reasoned about difficult in large CLI code bases.
Doesn’t project X already do this?
There are several projects that attempt to solve the sub command problem.
- Plumbum - http://plumbum.readthedocs.org/en/latest/cli.html
- Click - http://click.pocoo.org
- Cliff - http://docs.openstack.org/developer/cliff
When I originally wrote SubCommand none of these projects existed, and even now none of them are as small and simple to use as SubCommand. IMHO =)
In fact SubCommand is so small you can easily copy single .py module into your own project to avoid carrying around yet another external dependency.
Installation¶
Install via pip:
$ pip install cli-subcommand
Source is available here http://github.com/thrawn01/subcommand
What does it look like?¶
Here is simple example:
from subcommand import opt, noargs
import subcommand
import sys
class TestCommands(subcommand.Commands):
def __init__(self):
subcommand.Commands.__init__(self)
self.opt('-d', '--debug', action='store_const',
const=True, default=False, help="Output debug")
self.count = 0
@opt('--count', default=1, type=int, help="Num of Hello's")
@opt('name', help="Your name")
def hello(self, name, count=1):
""" Docstring for hello """
for x in range(count):
print('Hello, %s' % name)
self.count += 1
return 0
@noargs
def return_non_zero(self):
if self.debug:
print('Exit with non-zero status')
return 1
if __name__ == "__main__":
parser = subcommand.Parser([TestCommands()],
desc='Test Application')
sys.exit(parser.run())
It looks like this when run:
$ python hello.py hello derrick --count 5
Hello, derrick
Hello, derrick
Hello, derrick
Hello, derrick
Hello, derrick
$ echo $?
What it looks like when you pass no arguments:
$ python hello.py
Usage: hello.py <command> [-h]
Test Application
Available Commands:
return-non-zero
hello
What it looks like when you ask for hello -h:
$ python hello.py hello -h
usage: hello [-h] [--count COUNT] [-d] name
Docstring for hello
positional arguments:
name Your name
optional arguments:
-h, --help show this help message and exit
--count COUNT Num of Hello's
-d, --debug Output debug
Can my commands have subcommands?¶
In order to use subcommands you must use the
subcommand.SubParser
Class to parse your
subcommand.Commands
objects. In addition you must
give your Commands object a name by giving it the _name attribute.
Example:
from subcommand import opt, noargs
import subcommand
import sys
class BaseCommands(subcommand.Commands):
def pre_command(self):
self.client = self.client_factory()
class TicketCommands(BaseCommands):
""" Ticket SubCommand Docs """
_name = 'tickets'
@opt('tkt-num', help="tkt number to get")
def get(self, tkt_num):
""" Get Ticket docstring """
print(self.client.get_ticket(tkt_num))
class QueueCommands(BaseCommands):
""" Queue SubCommand Docs """
_name = 'queues'
@opt('queue-num', help="queue to get")
def get(self, queue_num):
print(self.client.get_queue(queue_num))
if __name__ == "__main__":
parser = subcommand.SubParser([TicketCommands(),
QueueCommands()],
desc='Ticket Client')
sys.exit(parser.run())
What it looks like when you run it:
$ python hello.py
Usage: hello.py <command> [-h]
Ticket Client
Available Commands:
tickets
queues
When you run the subcommands:
$ python hello.py tickets
Usage: hello.py tickets <command> [-h]
Ticket SubCommand Docs
Available Commands:
get
Getting help from the sub command:
$ python hello.py tickets get -h
usage: get [-h] tkt-num
Get Ticket docstring
positional arguments:
tkt-num tkt number to get
optional arguments:
-h, --help show this help message and exit
What if I have lots of arguments?¶
If you have a command with a ton of command line arguments, and don’t really want to specify each in the method signature you can specify a single argument called args and SubCommand will detect this and pass in all the arguments as a list.
Here is an example of a CLI interface to a ticketing system:
class TicketCommands(Commands):
def __init__(self):
SubCommand.__init__(self)
# Add debug option to all commands (creates self.debug)
self.opt('-d', '--debug', action='store_const',
const=True, default=False,
help="print server requests and responses")
self.opt('-U', '--url', default=None,
help="URL to our ticketing rest api")
@opt('-c', '--classification', type=int,
help="Specify the class of this ticket")
@opt('text', help="Body of the ticket")
@opt('subject', help="Subject of the ticket")
@opt('severity', type=int, help="Tkt severity level")
@opt('subcategory', help="The subcategory")
@opt('queue-name', help="Name of the queue")
def add_ticket(self, args):
client = self.ticket_factory()
# Remove --url and --debug
args = self.remove(args, ['url', 'debug'])
# The ticket client requires some args to
# be optional so we split them here
args, kwargs = self.split(args, ['queue_name',
'subcategory', 'source', 'severity',
'subject', 'text'])
resp = client.add_ticket(args['queue_name'],
args['subcategory'],
args['source'],
args['severity'],
args['subject'],
args['text'], **kwargs)
print(resp.to_json())
Can I execute common code before each command?¶
You can define a pre_command method which will get executed before a command is run
To further our ticket example:
class TicketCommands(Commands):
def pre_command(self):
self.client = ticket.client_factory()
@opt('tkt-num', help="tkt number to get")
def get_ticket(self, tkt_num):
print(self.client.get_ticket(tkt_num))
@opt('tkt-num', help="tkt number to delete")
def delete_ticket(self):
print(self.client.delete_ticket(tkt_num))
Does SubCommand provide bash completion?¶
Once you have created your CLI with SubCommand you can generate a bash completion script on ubuntu by running the following:
./my-script.py --bash-completion-script > /etc/bash_completion.d/my-script.py
Installation¶
This part of the documentation covers the installation of SubCommand. The first step to using any software package is getting it properly installed.
Distribute & Pip¶
Installing SubCommand is simple with pip, just run this in your terminal:
$ pip install git+https://github.com/thrawn01/subcommand.git
Get the Code¶
SubCommand is developed on GitHub, You can find the code here.
Clone the public repository:
$ git clone git://github.com/thrawn01/subcommand.git
Once you have a copy of the source, you can embed it in your Python package, or install it into your site-packages easily:
$ python setup.py install
Developer Interface¶
This part of the documentation covers all the interfaces of SubCommand.
Decorators¶
Commands
Methods decorated with these functions indicate the
method is a command and should be exposed via the command line.
-
subcommand.
opt
(*args, **kwargs)[source]¶ Use this decorator to add options to a sub command method. This decorator accepts the same arguments as ArgumentParser.add_argument
>>> @opt('--opt-arg', help="This is my optional arg") >>> @opt('pos-arg', help="This is my positional arg") >>> def test_sub_command(self, pos_arg=None, opt_arg=None): >>> print(pos_arg, opt_arg)
Commands Class¶
Use this class to define the sub commands to be exposed via the command line.
-
class
subcommand.
Commands
[source]¶ This is where you define all the commands that are accessed via the command line. Every command object must have a _name defined if used by the
SubParser
>>> class TestCommands(Commands): ... _name = 'test' ... @opt('pos-arg', help="This is my positional arg") ... @opt('--opt-arg', help="This is my optional arg") ... def command1(self, pos_arg=None, opt_arg=None): ... print('cmd with opts %s, %s' % pos_arg, opt_arg) ... @noargs ... def command2(self): ... print('cmd with no args')
-
bash_completion
()[source]¶ By default returns all the commands defined for this
Commands
object. This method is invoked when –bash-completion is called for a specific subcommand. This can be overidden by the implementor to return custom behaivor
-
opt
(*args, **kwargs)[source]¶ Use this method to define
Commands
options that are common to each command>>> class TestCommands(Commands): ... _name = 'test' ... def __init__(self): ... self.opt('-d', '--debug', action='store_const', ... const=True, default=False, ... help="Print debug to stdout") ... @noargs ... def command1(self): ... print(self.debug) ... @noargs ... def command2(self): ... print(self.debug)
-
pre_command
()[source]¶ This method is called prior to calling the command specified via the command line. Use it to initialize common code within your
Commands
object>>> class TestCommands(Commands): ... _name = 'test' ... def pre_command(self): ... print("I get run first") ... @noargs ... def command(self): ... print("Run my command") >>> parser = SubParser([TestCommands()], desc="") >>> parser.run(['prog', 'test', 'command']) I get run first Run my command
-
Parser Class¶
-
class
subcommand.
Parser
(commands, desc=None)[source]¶ >>> parser = CommandParser([TestCommands()], desc="") >>> return parser.run()
-
bash_completion
(args)¶ Used by the bash completion script to output completion candidates. The args passed by the bash completion script to this command are as follows:
args = ['--bash-completion', '%prog', 'sub-cmd', 'command']
-
bash_completion_script
(prog)¶ Output a bash completion script to stdout. To Create a bash completion script on ubuntu and maybe other distros:
./my-script.py --bash-completion-script \ > /etc/bash_completion.d/my-script.py
-
help
()¶ Print help message and exit
-
run
(args=None, prog=None)¶ Parse the command line arguments passed and execute the command requested, if no matching command is found or no argument display help and exit
-
SubParser Class¶
-
class
subcommand.
SubParser
(sub_commands, desc=None)[source]¶ Collects Command() objects and parse the commandline
>>> parser = SubParser([TestCommands()], ... desc="Test Description") >>> parser.run(['prog', 'test', 'command', ... 'arg1', '--opt-arg', 'arg2'])
-
bash_completion
(args)[source]¶ Used by the bash completion script to output completion candidates. The args passed by the bash completion script to this command are as follows:
args = ['--bash-completion', '%prog', 'sub-cmd', 'command']
-