Reputation: 10257
Is there a way to use the argparse
module hooked in as the interpreter for every prompt in an interface inheriting from cmd
?
I'd like for my cmd interface to interpret the typical line
parameter in the same way one would interpret the options and arguments passed in at runtime on the bash shell, using optional arguments with -
as well as positional arguments.
Upvotes: 10
Views: 6189
Reputation: 1299
The straight forward way would be to create an argparse
parser, and parse line.split()
within your function, expect
ing SystemExit
in case invalid arguments are supplied (parse_args()
calls sys.exit()
when it finds invalid arguments).
class TestInterface(cmd.Cmd):
__test1_parser = argparse.ArgumentParser(prog="test1")
__test1_parser.add_argument('--bar', help="bar help")
def help_test1(self): self.__test1_parser.print_help()
def do_test1(self, line):
try:
parsed = self.__test1_parser.parse_args(line.split())
except SystemExit:
return
print("Test1...")
print(parsed)
If invalid arguments are passed, parse_args()
will print errors, and the program will return to the interface without exiting.
(Cmd) test1 --unk
usage: test1 [-h] [--bar BAR]
test1: error: unrecognized arguments: --unk
(Cmd)
Everything else should work the same as a regular argparse
use case, also maintaining all of cmd
's functionality (help messages, function listing, etc.)
Source: https://groups.google.com/forum/#!topic/argparse-users/7QRPlG97cak
Another way, which simplifies the setup above, is using the decorator below:
class ArgparseCmdWrapper:
def __init__(self, parser):
"""Init decorator with an argparse parser to be used in parsing cmd-line options"""
self.parser = parser
self.help_msg = ""
def __call__(self, f):
"""Decorate 'f' to parse 'line' and pass options to decorated function"""
if not self.parser: # If no parser was passed to the decorator, get it from 'f'
self.parser = f(None, None, None, True)
def wrapped_f(*args):
line = args[1].split()
try:
parsed = self.parser.parse_args(line)
except SystemExit:
return
f(*args, parsed=parsed)
wrapped_f.__doc__ = self.__get_help(self.parser)
return wrapped_f
@staticmethod
def __get_help(parser):
"""Get and return help message from 'parser.print_help()'"""
f = tempfile.SpooledTemporaryFile(max_size=2048)
parser.print_help(file=f)
f.seek(0)
return f.read().rstrip()
It makes defining additional commands simpler, where they take an extra parsed
parameter that contains the result of a successful parse_args()
. If there are any invalid arguments the function is never entered, everything being handled by the decorator.
__test2_parser = argparse.ArgumentParser(prog="test2")
__test2_parser.add_argument('--foo', help="foo help")
@WrapperCmdLineArgParser(parser=__test2_parser)
def do_test2(self, line, parsed):
print("Test2...")
print(parsed)
Everything works as the original example, including argparse
generated help messages - without the need to define a help_command()
function.
Source: https://codereview.stackexchange.com/questions/134333/using-argparse-module-within-cmd-interface
Upvotes: 4
Reputation: 2633
Well, one way to do that is to override cmd
's default
method and use it to parse the line with argparse
, because all commands without do_
method in your cmd.Cmd subclass will fall through to use the default
method. Note the additional _
before do_test
to avoid it being used as cmd
's command.
import argparse
import cmd
import shlex
class TestCLI(cmd.Cmd):
def __init__(self, **kwargs):
cmd.Cmd.__init__(self, **kwargs)
self.parser = argparse.ArgumentParser()
subparsers = self.parser.add_subparsers()
test_parser = subparsers.add_parser("test")
test_parser.add_argument("--foo", default="Hello")
test_parser.add_argument("--bar", default="World")
test_parser.set_defaults(func=self._do_test)
def _do_test(self, args):
print args.foo, args.bar
def default(self, line):
args = self.parser.parse_args(shlex.split(line))
if hasattr(args, 'func'):
args.func(args)
else:
cmd.Cmd.default(self, line)
test = TestCLI()
test.cmdloop()
argparse
does a sys.exit
if it encounters unknown commands, so you would need to override or monkey patch your ArgumentParser
's error
method to raise an exception instead of exiting and handle that in the default
method, in order to stay in cmd
's command loop.
I would suggest you look into cliff which allows you to write commands that can automatically be used both as argparse
and cmd
commands, which is pretty neat. It also supports loading commands from setuptools
entry points, which allows you to distribute commands as plugins to your app. Note however, that cliff
uses cmd2
, which is cmd
's more powerful cousin, but you can replace it cmd
as cmd2
was developed as a drop-in replacement for cmd
.
Upvotes: 4