如何处理不能一起使用的选项(使用 OptionParser)?

7 投票
2 回答
2607 浏览
提问于 2025-04-15 22:07

我的Python脚本(用于待办事项列表)是通过命令行这样启动的:

todo [options] <command> [command-options]

有些选项不能一起使用,比如说

todo add --pos=3 --end "Ask Stackoverflow"

这会同时指定列表的第三个位置和列表的末尾。同样,

todo list --brief --informative

这会让我的程序搞不清楚是要简洁还是要详细。因为我希望有一个强大的选项控制,像这样的情况会很多,而且将来肯定还会出现新的情况。如果用户传入了一些不合理的选项组合,我想给出一个有用的提示,最好能附带optparse提供的使用帮助。目前我用一个if-else语句来处理这个问题,但我觉得这样写很丑陋,也不够好。我的理想是能在代码中有这样的东西:

parser.set_not_allowed(combination=["--pos", "--end"], 
                       message="--pos and --end can not be used together")

然后OptionParser在解析选项时会使用这个。

因为我知道的范围内并没有这样的功能,所以我想请教一下Stack Overflow的社区:你们是怎么处理这个问题的?

2 个回答

3

Tamás的回答是个不错的开始,但我没能让它正常工作,因为它有一些问题,比如调用super的地方出错"parser"Conflict.__slots__中缺失、在指定冲突时总是会报错,因为在Conflicts.accepts()中使用了parser.has_option()等等。

由于我真的需要这个功能,所以我自己做了一个解决方案,并在Python包索引上发布了它,叫做ConflictsOptionParser。它几乎可以直接替代optparse.OptionParser使用。(我知道argparse是新的命令行解析工具,但它在Python 2.6及以下版本中不可用,目前的使用率也低于optparse。如果你想一起研究或者已经做了基于argparse的解决方案,可以给我发邮件。)关键在于两个新方法,register_conflict()和相对不那么重要的unregister_conflict()

#/usr/bin/env python

import conflictsparse
parser = conflictsparse.ConflictsOptionParser("python %prog [OPTIONS] ARG")
# You can retain the Option instances for flexibility, in case you change
# option strings later
verbose_opt = parser.add_option('-v', '--verbose', action='store_true')
quiet_opt = parser.add_option('-q', '--quiet', action='store_true')
# Alternatively, you don't need to keep references to the instances;
# we can re-use the option strings later
parser.add_option('--no-output', action='store_true')
# Register the conflict. Specifying an error message is optional; the
# generic one that is generated will usually do.
parser.register_conflict((verbose_opt, quiet_opt, '--no-output'))
# Now we parse the arguments as we would with
# optparse.OptionParser.parse_args()
opts, args = parser.parse_args()

这个解决方案相比Tamás的方案有几个优点:

  • 它开箱即用,可以通过pip(或者如果你愿意的话用easy_install)安装。
  • 冲突中的选项可以通过它们的选项字符串或者optparse.Option实例来指定,这样有助于遵循DRY原则;如果你使用实例,就可以在不担心破坏冲突代码的情况下更改实际字符串。
  • 它遵循正常的optparse.OptionParser.parse_args()行为,并在检测到命令行参数中的冲突选项时自动调用optparse.OptionParser.error(),而不是直接抛出错误。(这既是一个优点也是一个缺点;在optparse的整体设计中算是个缺陷,但对于这个包来说是个优点,因为它至少与optparse的行为一致。)
6

可能可以通过扩展 optparse.OptionParser 来实现:

class Conflict(object):
    __slots__ = ("combination", "message", "parser")

    def __init__(self, combination, message, parser):
        self.combination = combination
        self.message = str(message)
        self.parser = parser

    def accepts(self, options):
        count = sum(1 for option in self.combination if hasattr(options, option))
        return count <= 1

class ConflictError(Exception):
    def __init__(self, conflict):
        self.conflict = conflict

    def __str__(self):
        return self.conflict.message

class MyOptionParser(optparse.OptionParser):
    def __init__(self, *args, **kwds):
        optparse.OptionParser.__init__(self, *args, **kwds)
        self.conflicts = []

    def set_not_allowed(self, combination, message):
        self.conflicts.append(Conflict(combination, message, self))

    def parse_args(self, *args, **kwds):
        # Force-ignore the default values and parse the arguments first
        kwds2 = dict(kwds)
        kwds2["values"] = optparse.Values()
        options, _ = optparse.OptionParser.parse_args(self, *args, **kwds2)

        # Check for conflicts
        for conflict in self.conflicts:
            if not conflict.accepts(options):
                raise ConflictError(conflict)

        # Parse the arguments once again, now with defaults
        return optparse.OptionParser.parse_args(self, *args, **kwds)

然后你可以在调用 parse_args 的地方处理 ConflictError 错误:

try:
    options, args = parser.parse_args()
except ConflictError as err:
    parser.error(err.message)

撰写回答