我创建了一个使用cmd界面的应用程序。它具有多个级别,并且可用命令的数量及其复杂性正在增长。因此,我需要归纳参数解析-line参数。

我喜欢argparse-之前我曾几次将其用于“预期”目的-因此我想尝试一下。不幸的是,由于许多设计决策都被强加给您(例如,在无效参数上调用sys.exit()等),因此它不太适合以“非常规”方式使用。

我环顾四周寻找解决方案,并找到了这个。但是,该解决方案放弃了cmd的可用功能列表及其帮助功能。同样,它要求所有可用命令及其参数都放在一个位置(即__init__),这在有许多命令时不切实际。

我想出了使用装饰器的解决方案如下:

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[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()


它允许独立定义命令/功能,并引入第三个参数parsed,其中将包含成功的parse_args()的返回值。定义函数/命令的方法有2种:

这允许将解析器作为参数传递给装饰器。一种简单的方法是定义名称混乱的静态变量,如下__test1_parser所示。

__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)


在某些情况下,特别是如果解析器很复杂,则最好定义函数本身中的解析器。这也是可能的。该函数必须采用第4个参数get_parser=False,并在True时返回解析器。

@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)


无论定义它们的方式如何,函数都将列出在cmd的帮助下

 (Cmd) help

Documented commands (type help <topic>):
========================================
help  test1  test2
 


以及具有自己的帮助消息-通过help <func_name><func_name> -h

 (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
 


此外,当解析无效参数时,错误消息将被打印,并且将返回到提示,而无需调用给定的函数。

(Cmd) test1 --unk
usage: test1 [-h] [--foo FOO]
test1: error: unrecognized arguments: --unk
(Cmd)
的命令,同时保持并且不与argparse的任何功能冲突。 

此解决方案可能有哪些缺点?有哪些可能的改进或明显的错误?使用cmdcmd是否有更好的解决方案?是否存在提供我要实现的功能的其他模块?上面两种方法中的哪一种更好?

到目前为止,我已经注意到,在PyCharm的调试器控制台上,当存在无效参数并且引发argparse时,错误消息和提示有时会重叠或不存在的顺序。它不会在常规终端上发生。

评论

您看过点击吗?很好,我认为可以将用于测试的cliRunner转换为cmd样式的提示。

@pjz我没有;确实看起来不错。我现在已经离开了那个项目,但是感谢您的指导!

如何将部分解析与子命令一起使用?

#1 楼

与其说是代码审查,不如说是架构建议。您的代码很好,但是我对当事情变得越来越复杂时倾向于增加的聪明之处感到厌倦。

除非您真的想从单个.py模块运行所有内容,否则我会建议将程序分成多个部分。将共享的片段放在一个公共库中,并通过调用外部实用程序将不同的组分成不同的可执行文件,就像git和apt do一样。这也使其他人可以轻松地为您的实用程序编写附件。

想想utility verb --be-awesome就是在后台调用utility-verb --be-awesome并传递它得到的任何命令行参数。作为一个有用的约定,您可能希望具有一些影响基本实用程序的参数,这些参数在调用时可能会从命令行中删除,或者只是强制将它们放在utilityverb部件之间。所有utility-verb命令都应该能够依赖库上的共享功能。