Python using argparse with cmd
Asked Answered
P

2

10

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.

Persian answered 5/10, 2012 at 16:30 Comment(0)
B
4

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.

Brundisium answered 15/4, 2014 at 14:12 Comment(1)
sys.exit() raises SystemExit (as demonstrated in @Ramon's answer) so no monkeypatching or override needed. Just catch it and handle if you can, or re-raise.Cutout
N
4

The straight forward way would be to create an argparse parser, and parse line.split() within your function, expecting 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

Neilla answered 11/7, 2016 at 19:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.