Argparse - 如何指定默认子命令

21

我正在使用Python 2.7的argparse包编写一个命令行工具的选项解析逻辑。该工具应接受以下参数之一:

"ON": 打开一个功能。
"OFF": 关闭一个功能。
[未提供参数]: 回显该功能的当前状态。

查看argparse文档后,我认为需要定义两个(可能是三个)子命令,因为这三种状态是互斥的,并表示不同的概念活动。以下是我的当前代码尝试:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser.set_defaults(func=print_state) # I think this line is wrong.

parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=set_state, newstate='ON')

parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=set_state, newstate='OFF')

args = parser.parse_args()

if(args.func == set_state):
    set_state(args.newstate)
elif(args.func == print_state):
    print_state()
else:
    args.func() # Catchall in case I add more functions later

我原本以为,如果我提供0个参数,主解析器会设置func=print_state,如果我提供1个参数,主解析器会使用适当的子命令默认值并调用func=set_state。 但实际上,当我没有提供参数时,我得到以下错误:

usage: cvsSecure.py [-h] {ON,OFF} ...
cvsSecure.py: error: too few arguments
如果我提供“OFF”或“ON”,则会调用print_state而不是set_state。如果我注释掉parser.set_defaults这行,set_state将被正确调用。我是一个初级程序员,但对Python还是个新手。您有关如何使其工作的任何建议吗?
编辑:我查看子命令的另一个原因是考虑未来可能需要的第四个功能:“FORCE txtval”:将函数的状态设置为txtval
2个回答

13
顶级解析器的默认设置会覆盖子解析器上的默认设置,因此在子解析器上设置 func 的默认值会被忽略,但是来自子解析器默认设置的 newstate 的值是正确的。
我认为您不需要使用子命令。当可用选项和位置参数根据选择的子命令而变化时,才使用子命令。但是,在您的情况下,没有其他选项或位置参数。
以下代码似乎可以实现您所需的功能:
import argparse

def print_state():
    print "Print state"

def set_state(s):
    print "Setting state to " + s

parser = argparse.ArgumentParser()
parser.add_argument('state', choices = ['ON', 'OFF'], nargs='?')

args = parser.parse_args()

if args.state is None:
    print_state()
elif args.state in ('ON', 'OFF'):
    set_state(args.state)

请注意parser.add_argument的可选参数。"choices"参数指定允许的选项,而将"nargs"设置为"?"表示如果有可用的话应消耗1个参数,否则不应该消耗任何参数。

编辑:如果您想添加带有参数的FORCE命令,并且希望ON和OFF命令具有单独的帮助文本,则确实需要使用子命令。不幸的是,似乎没有一种指定默认子命令的方法。但是,您可以通过检查空参数列表并提供自己的参数列表来解决问题。以下是一些说明我意思的示例代码:

import argparse
import sys

def print_state(ignored):
    print "Print state"

def set_state(s):
    print "Setting state to " + s

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
on = subparsers.add_parser('ON', help = 'On help here.')
on.set_defaults(func = set_state, newstate = 'ON')
off = subparsers.add_parser('OFF', help = 'Off help here.')
off.set_defaults(func = set_state, newstate = 'OFF')
prt = subparsers.add_parser('PRINT')
prt.set_defaults(func = print_state, newstate = 'N/A')
force = subparsers.add_parser('FORCE' , help = 'Force help here.')
force.add_argument('newstate', choices = [ 'ON', 'OFF' ])
force.set_defaults(func = set_state)

if (len(sys.argv) < 2):
    args = parser.parse_args(['PRINT'])
else:
    args = parser.parse_args(sys.argv[1:])

args.func(args.newstate)

几个问题。1)我是否能够在使用说明中为“ON”和“OFF”提供单独的帮助文本?2)我正在考虑添加第四个选项,它需要一个额外的参数:“FORCE txtval”。当前的解决方案能否实现这一点? - Will Shipley
1
不,上述解决方案不允许为“ON”和“OFF”提供单独的帮助文本,并且带有参数的“FORCE”命令无法与上述解决方案一起使用。我将在稍后添加另一个解决方法的答案。 - srgerg
谢谢,使用带有if(len(sys.argv) < 2):的if/else语句解决了问题!然而,现在它将忽略其余的参数。假设你想要在没有指令的情况下运行脚本,结果为“PRINT”。但是你还想提供--newstate参数。 - Melroy van den Berg

1
你的方法存在两个问题。
首先,你可能已经注意到newstate不是子解析器的某个子值,并且需要在args的顶层中作为args.newstate进行处理。这应该可以解释为什么将默认值分配给newstate两次会导致第一个值被覆盖。无论你使用'ON'还是'OFF'作为参数调用程序,每次调用set_state()都会使用OFF。如果你只想能够执行python cvsSecure ONpython cvsSecure OFF,则以下内容可行:
from __future__ import print_function

import sys
import argparse

def set_state(state):
    print("set_state", state)

def do_on(args):
    set_state('ON')

def do_off(args):
    set_state('OFF')

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=do_on)
parser_on.add_argument('--fast', action='store_true')
parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=do_off)

args = parser.parse_args()
args.func(args)

第二个问题是argparse无法将子解析器处理为单个值参数,因此在调用parser.parse_args()之前必须指定一个。您可以通过添加额外的子解析器'PRINT'并自动插入该解析器来自动插入缺少的参数,使用set_default_subparser添加到argparse.ArgumentParser()中(该代码是ruamel.std.argparse包的一部分)。
from __future__ import print_function

import sys
import argparse

def set_default_subparser(self, name, args=None):
    """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 first position, this implies no
            # global options without a sub_parsers specified
            if args is None:
                sys.argv.insert(1, name)
            else:
                args.insert(0, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser


def print_state(args):
    print("print_state")

def set_state(state):
    print("set_state", state)

def do_on(args):
    set_state('ON')

def do_off(args):
    set_state('OFF')

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser_print = subparsers.add_parser('PRINT', help='default action')
parser_print.set_defaults(func=print_state)
parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=do_on)
parser_on.add_argument('--fast', action='store_true')
parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=do_off)

parser.set_default_subparser('PRINT')

args = parser.parse_args()
args.func(args)

你不需要在do_on()中处理args等内容,但如果您开始为不同的子解析器指定选项,则会很方便。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接