Argparse - 如何指定默认子命令

21 投票
2 回答
15736 浏览
提问于 2025-04-16 12:53

我正在使用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

我原以为如果我不提供任何参数,主解析器会设置func=print_state,而如果我提供一个参数,主解析器会使用相应子命令的默认值并调用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 个回答

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)

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

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)

撰写回答