I've created an application that uses a cmd interface. It has multiple levels, and the number of available commands and their complexity is growing. As such, I need to generalise argument parsing - of the
I like argparse - I've previously used it a few times for its 'intended' purpose - so I figured I'd try that. Unfortunately, it doesn't lend itself well to being used in 'unconventional' ways, as many design decisions are forced on you (e.g.
sys.exit() is called on invalid arguments, etc.).
I looked around for solutions and found this. However, that solution relinquishes
cmd's listing of available functions, and its help functionality. As well, it requires that all the available commands and their arguments be listed in one place (i.e.
__init__), which makes it impractical when there are many commands.
I came up with a solution using the decorator below:
class WrapperCmdLineArgParser: 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.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 allows a command/function to be defined independently, taking in a third argument
parsed, which will contain the return value of a successful
parse_args(). There are 2 ways to define a function/command:
This allows one to pass the parser as an argument to the decorator. A simple way would be to define name mangled static variables, as below
__test1_parser = argparse.ArgumentParser(prog="test1") __test1_parser.add_argument('--foo', help="foo help") @WrapperCmdLineArgParser(parser=__test1_parser) def do_test1(self, line, parsed): print("Test1...") print(parsed)
In some cases, specially if the parser is complex, it may be preferred to define the parser within the function itself. This is possible as well. The function would have to take a 4th argument
get_parser=False, and return the parser when
@WrapperCmdLineArgParser(parser=None) def do_test2(self, line, parsed, get_parser=False): if get_parser: parser = argparse.ArgumentParser(prog="test2") parser.add_argument('--bar', help="bar help") return parser print("Test2...") print(parsed)
Regardless of the way in which they are defined, the functions will be listed under
(Cmd) help Documented commands (type help <topic>): ======================================== help test1 test2
as well as have their own help messages - via
help <func_name> or
(Cmd) help test1 usage: test1 [-h] [--foo FOO] optional arguments: -h, --help show this help message and exit --foo FOO foo help (Cmd) test2 -h usage: test2 [-h] [--bar BAR] optional arguments: -h, --help show this help message and exit --bar BAR bar help
Furthermore, when parsing invalid arguments, the error message will be printed, and it will return to the prompt, without calling the given function.
(Cmd) test1 --unk usage: test1 [-h] [--foo FOO] test1: error: unrecognized arguments: --unk (Cmd)
I think this is a decent solution that allows one to simply and independently define commands that use
argparse, while maintaining and not clashing with any of
What are some possible drawbacks of this solution? What are some possible improvements, or obvious bugs? Is there a better solution using
argparse? Do any other modules exist that provide the functionality I am trying to achieve? Which of the 2 ways above would be better?
Thus far I have noticed that on PyCharm's debugger console, when there are invalid arguments and
SystemExit is raised, the error messages and the prompt sometimes overlap or are out of order. It doesn't happen on a regular terminal.