I'm trying to build a small interactive shell in Python using the cmd
module. Is there an easy way to allow for multi-word commands?
For example, it's easy to process the hello
command
class FooShell(Cmd):
def do_hello(self, args):
print("Hi")
But what if I wanted something more complicated. Let's say I'm trying to implement an SQL shell and want to write show tables
. The show
command can take multiple targets such as show track_counts
or show bonjour
. If I wanted to process something like this in the cmd module, it looks like I would have to write the following:
class FooShell(Cmd):
def do_show(self, line):
args = line.strip().split(" ")
if args == []:
print("Error: show command requires arguments")
else:
if args[0] == "tables":
pass # logic here
elif args[0] == "bonjour":
pass # logic here
elif args[0] == "track_counts":
pass # logic here
else:
print("{} is not a valid target for the 'show' command".format(args[0]))
print("Valid targets are tables, bonjour, track_counts")
There are a few problems with this approach:
show
Another way of writing the above would be like this:
class FooShell(Cmd):
def do_show_tables(self, args):
pass
def do_show_bonjour(self, args):
pass
def do_show_track_counts(self, args):
pass
def do_show(self, line):
args = line.strip().split(" ")
if args == []:
print("Error: show command requires arguments")
else:
handlers = {
"tables": self.do_show_tables,
"bonjour": self.do_show_bonjour,
"track_counts": self.do_show_track_counts
}
handler = handlers.get(args[0], None)
if handler:
handler(args[1:])
else:
print("{} is not a valid target for the 'show' command".format(args[0]))
targets = ", ".join([key for key in handlers])
print("Valid targets are: {}".format(targets))
But this still does not give tab completion after the 'show' command. Additionally, it now feels like I'm basically rewriting the core functionality of the cmd
module.
Is there a simpler way to do this? Should I be using another module instead of cmd
?
EDIT: to be clear, I am not actually writing an SQL shell, just using that as an example of how I want multi-word commands to be parsed.
This can be achieved using argparse subparsers in the cmd2
module.
Here's an official example: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py
It boils down to four things:
argparse
functionality of cmd2
with the cmd2.Cmd2ArgumentParser()
functionadd_subparsers
functionset_defaults
function@cmd2.with_argparser(base_parser)
decorator and calling subcommands in themIf all of this is present, you can modify your command to call the subcommand functions (taken from the official example in case the link goes down):
@cmd2.with_argparser(base_parser)
def do_base(self, args):
"""Base command help"""
func = getattr(args, 'func', None)
if func is not None:
# Call whatever subcommand function was selected
func(self, args)
else:
# No subcommand was provided, so call help
self.do_help('base')
It supports:
Here's a sample with an error message from running the official example:
(Cmd) base bar apple
Usage: base bar [-h] {apple, artichoke, cranberries} ... z
Error: the following arguments are required: z
Following this here's the original answer, with how to achieve this in vanilla cmd
, in case you don't want to add a dependency.
Command parsing itself does indeed look ugly, but tab completion can be achieved.
The recon-ng
module uses the same approach using the cmd
module, it can be made less ugly by using arrays for the command lists and separate functions for the sub-command parsing.
Here's a simple example written using the cmd
module for tab completion (without separate functions to keep it simple), to show how it works:
def do_example(self, params):
'Example action command'
subs = params.split(' ')
if subs[0] == 'first-l2' and subs[1] == 'second-l3':
print("Second l3 called in first l2")
if subs[0] == 'second-l2' and subs[1] == 'first-l3':
print("First l3 called in second l2")
def complete_example(self, text, line, *ignored):
'Interactive tab completion for example command'
subs = ['first-l2', 'second-l2']
args = line.split(' ')
if args[1] in subs:
subsubs = ['first-l3', 'second-l3']
if args[2] in subsubs:
return []
return [sub for sub in subsubs if sub.startswith(text)]
return [sub for sub in subs if sub.startswith(text)]
Basically, when you're writing the complete
function, you should only return suggestions about the current level. The previous levels will already be there.
The cliff
package does not have autocompletion in interactive mode.