如何处理不能一起使用的选项(使用 OptionParser)?
我的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 个回答
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
的行为一致。)
可能可以通过扩展 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)