默认子命令,或不使用 argparse 处理子命令

Posted

技术标签:

【中文标题】默认子命令,或不使用 argparse 处理子命令【英文标题】:Default sub-command, or handling no sub-command with argparse 【发布时间】:2011-09-15 23:22:53 【问题描述】:

我怎样才能有一个默认的sub-command,或者处理没有使用argparse给出子命令的情况?

import argparse

a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()

在这里,我希望选择一个命令,或者仅基于下一个***别的解析器(在本例中为***解析器)处理的参数。

joiner@X:~/src> python3 default_subcommand.py
用法:default_subcommand.py [-h] hi ...
default_subcommand.py:错误:参数太少

【问题讨论】:

【参考方案1】:

您可以添加一个带有默认值的参数,我相信该参数将在没有设置任何内容时使用。

看到这个:http://docs.python.org/dev/library/argparse.html#default

编辑:

对不起,我读你的问题有点快。

我认为你不会有直接的方式通过 argparse 做你想做的事。但是您可以检查 sys.argv 的长度,如果它的长度为 1(仅脚本名称),那么您可以手动传递默认参数进行解析,执行如下操作:

import argparse

a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')

if len(sys.argv) == 1:
   a.parse_args(['hi'])
else:
   a.parse_args()

我认为这应该可以满足您的需求,但我同意开箱即用会很好。

【讨论】:

这是一个糟糕的解决方案;如果有额外的标志怎么办?【参考方案2】:

看来我自己最终还是偶然发现了解决方案。

如果该命令是可选的,那么这会使该命令成为一个选项。在我最初的解析器配置中,我有一个package 命令可以采取一系列可能的步骤,或者如果没有给出任何步骤,它将执行所有步骤。这使得该步骤成为一种选择:

parser = argparse.ArgumentParser()

command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])

...other command parsers

parsed_args = parser.parse_args()

if parsed_args.step is None:
    do all the steps...

【讨论】:

但是这意味着如果不同的子命令有不同的标志,你必须自己处理这个逻辑,而不是让 argparse 处理它。 不明白你的意思,代码看起来没有任何不同。您是否在某处删除了“必需”? 我的意思是,因为上面的“step”命令是可选的,所以最好把它当作一个标志。 更优雅的方式是使用dest 值。无需依赖论据。 docs "如果该命令是可选的,那么这将使该命令成为一个选项。"这并非总是如此。例如,git remote 默认充当git remote show,但show 不是一个选项,而是一个子命令(如addrm 等)。但是,为了模拟我手动检查了参数,正如你所建议的那样。【参考方案3】:

在 Python 3.2(和 2.7)上,您会收到该错误,但在 3.3 和 3.4 上不会(无响应)。因此,在 3.3/3.4 上,您可以测试 parsed_args 是否为空 Namespace

更通用的解决方案是添加一个方法set_default_subparser()(取自ruamel.std.argparse 包)并在parse_args() 之前调用该方法:

import argparse
import sys

def set_default_subparser(self, name, args=None, positional_args=0):
    """default subparser selection. Call after setup, just before parse_args()
    name: is the name of the subparser to call by default
    args: if set is the argument list handed to parse_args()

    , tested with 2.7, 3.2, 3.3, 3.4
    it works with 2.6 assuming argparse is installed
    """
    subparser_found = False
    for arg in sys.argv[1:]:
        if arg in ['-h', '--help']:  # global help if no subparser
            break
    else:
        for x in self._subparsers._actions:
            if not isinstance(x, argparse._SubParsersAction):
                continue
            for sp_name in x._name_parser_map.keys():
                if sp_name in sys.argv[1:]:
                    subparser_found = True
        if not subparser_found:
            # insert default in last position before global positional
            # arguments, this implies no global options are specified after
            # first positional argument
            if args is None:
                sys.argv.insert(len(sys.argv) - positional_args, name)
            else:
                args.insert(len(args) - positional_args, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

def do_hi():
    print('inside hi')

a = argparse.ArgumentParser()
b = a.add_subparsers()
sp = b.add_parser('hi')
sp.set_defaults(func=do_hi)

a.set_default_subparser('hi')
parsed_args = a.parse_args()

if hasattr(parsed_args, 'func'):
    parsed_args.func()

这适用于 2.6(如果从 PyPI 安装 argparse)、2.7、3.2、3.3、3.4。并允许您两者兼而有之

python3 default_subcommand.py

python3 default_subcommand.py hi

效果一样。

允许为默认选择一个新的子解析器,而不是现有的一个。

代码的第一个版本允许将先前定义的子解析器之一设置为默认子解析器。以下修改允许添加一个新的默认子解析器,然后可以使用它来专门处理用户没有选择子解析器的情况(代码中标记了不同的行)

def set_default_subparser(self, name, args=None, positional_args=0):
    """default subparser selection. Call after setup, just before parse_args()
    name: is the name of the subparser to call by default
    args: if set is the argument list handed to parse_args()

    , tested with 2.7, 3.2, 3.3, 3.4
    it works with 2.6 assuming argparse is installed
    """
    subparser_found = False
    existing_default = False # check if default parser previously defined
    for arg in sys.argv[1:]:
        if arg in ['-h', '--help']:  # global help if no subparser
            break
    else:
        for x in self._subparsers._actions:
            if not isinstance(x, argparse._SubParsersAction):
                continue
            for sp_name in x._name_parser_map.keys():
                if sp_name in sys.argv[1:]:
                    subparser_found = True
                if sp_name == name: # check existance of default parser
                    existing_default = True
        if not subparser_found:
            # If the default subparser is not among the existing ones,
            # create a new parser.
            # As this is called just before 'parse_args', the default
            # parser created here will not pollute the help output.

            if not existing_default:
                for x in self._subparsers._actions:
                    if not isinstance(x, argparse._SubParsersAction):
                        continue
                    x.add_parser(name)
                    break # this works OK, but should I check further?

            # insert default in last position before global positional
            # arguments, this implies no global options are specified after
            # first positional argument
            if args is None:
                sys.argv.insert(len(sys.argv) - positional_args, name)
            else:
                args.insert(len(args) - positional_args, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

a = argparse.ArgumentParser()
b = a.add_subparsers(dest ='cmd')
sp = b.add_parser('hi')
sp2 = b.add_parser('hai')

a.set_default_subparser('hey')
parsed_args = a.parse_args()

print(parsed_args)

“默认”选项仍然不会显示在帮助中:

python test_parser.py -h
usage: test_parser.py [-h] hi,hai ...

positional arguments:
  hi,hai

optional arguments:
  -h, --help  show this help message and exit

但是,现在可以区分并分别处理调用提供的子解析器之一和在未提供参数时调用默认子解析器:

$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py 
Namespace(cmd='hey')

【讨论】:

这是一个很好的解决方案(尽管已经很老了)——我通过广泛的谷歌搜索找到了最好的解决方案。但是,它似乎只支持default_subparser 是先前定义的子解析器之一的情况。我做了一个小的修改,允许像a.set_default_subparser('no_sp') 这样的语句可以单独处理,并且不会污染帮助输出。但是,它只添加了 9 行代码,所以我认为它不值得单独回答 - 你介意我用我的这些修改更新你的答案吗? @penelope Argparse 没有太大变化,这使得旧答案仍然有价值。是的,您可以更新答案,我什至可能在ruamel.std.argparse 中包含更改;-) 请在答案底部添加您的“版本”作为单独的块,例如:---如果您想选择默认子解析器由其名称的字符串表示... 在那里,我在代码和示例中添加了带有 cmets 的版本以演示功能。代码中有一个地方——我的循环以for x in self._subparsers._actions: 开头——我不确定是否需要做额外的检查,你可能更熟悉。这确实使答案有点庞大 - 所以也许看看代码差异并决定你是否喜欢像现在这样将它保留为两个版本或毕竟合并它。希望这对某人有帮助:) 实际上,我发现了一个稍微不受欢迎的行为:当指定了一个不存在的选项时(例如python test_parser.py ho),使用说明也会列出默认的子解析器值:usage: test_parser.py [-h] hi,hai,hey ...(默认的子解析器是添加,因为没有找到匹配的)。这不是一个大问题,但如果你知道如何纠正它会很酷:) 我可能在您发布的原始代码中发现了一个错误。如果解析器还包含必需的可选参数(例如usage: test_parser.py [-h] -i IMAGE hi,hai ...),则在使用“默认子解析器”时忽略提供的-i 参数,例如python test_parser.py -i original1.png 产生以下错误:test_parser.py: error: argument -i/--image is required。解决方案:将sys.argv.insert(0,name) 更改为sys.argv.insert(len(sys.argv), name)args 情况相同。插入到位置 0 时,由于立即进入子解析器,所有参数都将被忽略。【参考方案4】:

也许您正在寻找的是destadd_subparsers 参数:

警告:适用于 Python 3.4,但不适用于 2.7

import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
parser_hi = subparsers.add_parser('hi')
parser.parse_args([]) # Namespace(cmd=None)

现在你可以使用cmd的值了:

if cmd in [None, 'hi']:
    print('command "hi"')

【讨论】:

这种方法(如所写)是否绕过sys.argv 中的任何值以支持提供的空列表([])? @beporter 当然在实际使用场景中不要将[] 传递给parse_args,这里我这样做是为了展示我们传递的参数(无)作为示例。【参考方案5】:

这是添加set_default_subparser 方法的更好方法:

class DefaultSubcommandArgParse(argparse.ArgumentParser):
    __default_subparser = None

    def set_default_subparser(self, name):
        self.__default_subparser = name

    def _parse_known_args(self, arg_strings, *args, **kwargs):
        in_args = set(arg_strings)
        d_sp = self.__default_subparser
        if d_sp is not None and not '-h', '--help'.intersection(in_args):
            for x in self._subparsers._actions:
                subparser_found = (
                    isinstance(x, argparse._SubParsersAction) and
                    in_args.intersection(x._name_parser_map.keys())
                )
                if subparser_found:
                    break
            else:
                # insert default in first position, this implies no
                # global options without a sub_parsers specified
                arg_strings = [d_sp] + arg_strings
        return super(DefaultSubcommandArgParse, self)._parse_known_args(
            arg_strings, *args, **kwargs
        )

【讨论】:

感谢您的代码,但通常最好用文字描述您的代码的作用。 这个想法是不要猴子补丁 argparse.ArgumentParser 而是子类化它【参考方案6】:

供以后参考:

...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey')  # <-- this makes hey as default

b.add_parser('hi')

所以,这两个将是相同的:

python main.py 嘿 python main.py

【讨论】:

【参考方案7】:

在 python 2.7 中,您可以覆盖子类中的错误行为(可惜没有更好的方法来区分错误):

import argparse

class ExceptionArgParser(argparse.ArgumentParser):

    def error(self, message):
        if "invalid choice" in message:
            # throw exception (of your choice) to catch
            raise RuntimeError(message)
        else:
            # restore normal behaviour
            super(ExceptionArgParser, self).error(message)


parser = ExceptionArgParser()
subparsers = parser.add_subparsers(title='Modes', dest='mode')

default_parser = subparsers.add_parser('default')
default_parser.add_argument('a', nargs="+")

other_parser = subparsers.add_parser('other')
other_parser.add_argument('b', nargs="+")

try:
    args = parser.parse_args()
except RuntimeError:
    args = default_parser.parse_args()
    # force the mode into namespace
    setattr(args, 'mode', 'default') 

print args

【讨论】:

【参考方案8】:

您可以在主解析器上复制特定子解析器的默认操作,从而有效地将其设为默认操作。

import argparse
p = argparse.ArgumentParser()
sp = p.add_subparsers()

a = sp.add_parser('a')
a.set_defaults(func=do_a)

b = sp.add_parser('b')
b.set_defaults(func=do_b)

p.set_defaults(func=do_b)
args = p.parse_args()

if args.func:
    args.func()
else:
    parser.print_help()

不适用于add_subparsers(required=True),这就是if args.func 在下面的原因。

【讨论】:

这应该适用于原始问题,但不幸的是,当您为子解析器设置默认操作时,这不起作用(它给出“AttributeError:'_SubParsersAction'对象没有属性'set_defaults'”) .我正在尝试模拟“git remote”——默认情况下它只打印遥控器,但它可以有“add”、“rm”等子命令。【参考方案9】:

在我的例子中,当argv 为空时,我发现向parse_args() 显式提供子命令名称是最容易的。

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')

runParser = subparsers.add_parser('run', help='[DEFAULT ACTION]')

altParser = subparsers.add_parser('alt', help='Alternate command')
altParser.add_argument('alt_val', type=str, help='Value required for alt command.')

# Here's my shortcut: If `argv` only contains the script name,
# manually inject our "default" command.
args = parser.parse_args(['run'] if len(sys.argv) == 1 else None)
print args

示例运行:

$ ./test.py 
Namespace()

$ ./test.py alt blah
Namespace(alt_val='blah')

$ ./test.py blah
usage: test.py [-h] run,alt ...
test.py: error: invalid choice: 'blah' (choose from 'run', 'alt')

【讨论】:

以上是关于默认子命令,或不使用 argparse 处理子命令的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 argparse 处理 CLI 子命令

如何有一个特定的子命令需要带有 argparse 的标志?

Python:正确处理子命令的全局选项的参数解析器

具有依赖和冲突的python argparse子命令

带有子命令的命令和子命令请求的 argparse 解决方案

Python argparse 位置参数和子命令