使用 usage.Options 解析命令行

简介

程序经常需要解析类似 UNIX 的命令行程序:以 --- 开头的选项,有时后面跟着参数,最后跟着参数列表。 twisted.python.usage 提供了一个类 Options 来简化这种解析。

虽然 Python 有 getopt 模块可以实现这一点,但它对选项的抽象级别非常低。Twisted 在 twisted.python.usage.Options 类中提供了更高层次的抽象。它使用 Python 的反射机制为命令行提供了一个易于使用且灵活的接口。虽然大多数命令行处理器要么强制应用程序编写者编写自己的循环,要么对命令行施加任意限制(最常见的是不能拥有同一个选项的多个实例,因此无法使用 program -v -v -v 这样的语法),Twisted 允许程序员决定他们想要多少控制权。

Options 类通过子类化使用。由于它经常在 twisted.tap 包中使用,而本地约定要求特定的选项解析类也称为 Options,因此通常使用以下方式导入它

from twisted.python import usage

布尔选项

对于简单的布尔选项,请像这样定义属性 optFlags

class Options(usage.Options):

    optFlags = [["fast", "f", "Act quickly"], ["safe", "s", "Act safely"]]

optFlags 应该是一个包含 3 个列表的列表。第一个元素是长名称,将在命令行中用作 --fast。第二个元素是短名称,将在命令行中用作 -f。最后一个元素是标志的描述,将用于生成使用信息文本。长名称也决定了将在 Options 实例上设置的键的名称。如果看到该选项,它的值将为 1,否则为 0。以下是一个使用示例

from __future__ import print_function

class Options(usage.Options):

    optFlags = [
        ["fast", "f", "Act quickly"],
        ["good", "g", "Act well"],
        ["cheap", "c", "Act cheaply"]
    ]

command_line = ["-g", "--fast"]

options = Options()
try:
    options.parseOptions(command_line)
except usage.UsageError as errortext:
    print('{}: {}'.format(sys.argv[0], errortext))
    print('{}: Try --help for usage details.'.format(sys.argv[0]))
    sys.exit(1)
if options['fast']:
    print("fast", end='')
if options['good']:
    print("good", end='')
if options['cheap']:
    print("cheap", end='')
print()

以上代码将打印 fast good

请注意,Options 完全支持映射接口。您可以像访问任何其他字典一样访问它。选项作为映射项存储在 Options 实例中:参数为 'paramname':'value',标志为 'flagname': 1 或 0。

继承,或者:我如何学会停止担心并爱上超类

有时需要多个选项处理器,它们具有一个统一的核心。也许您希望所有命令都理解 -q /--quiet 表示静默,或者类似的东西。从表面上看,这似乎不可能:在 Python 中,子类的 optFlags 将覆盖超类的 optFlags。但是,usage.Options 使用特殊的反射代码来获取层次结构中定义的所有 optFlags。因此,以下代码

class BaseOptions(usage.Options):

    optFlags = [["quiet", "q", None]]

class SpecificOptions(BaseOptions):

    optFlags = [
        ["fast", "f", None], ["good", "g", None], ["cheap", "c", None]
    ]

与以下代码相同

class SpecificOptions(usage.Options):

    optFlags = [
        ["quiet", "q", "Silence output"],
        ["fast", "f", "Run quickly"],
        ["good", "g", "Don't validate input"],
        ["cheap", "c", "Use cheap resources"]
    ]

参数

参数使用属性 optParameters 指定。它们 *必须* 给定一个默认值。如果您想确保从命令行获取了参数,请给定一个非字符串默认值。由于命令行只有字符串,因此这完全可靠。

以下是一个示例

from __future__ import print_function

from twisted.python import usage

class Options(usage.Options):

    optFlags = [
        ["fast", "f", "Run quickly"],
        ["good", "g", "Don't validate input"],
        ["cheap", "c", "Use cheap resources"]
    ]
    optParameters = [["user", "u", None, "The user name"]]

config = Options()
try:
    config.parseOptions() # When given no argument, parses sys.argv[1:]
except usage.UsageError as errortext:
    print('{}: {}'.format(sys.argv[0], errortext))
    print('{}: Try --help for usage details.'.format(sys.argv[0]))
    sys.exit(1)

if config['user'] is not None:
    print("Hello", config['user'])
print("So, you want it:")

if config['fast']:
    print("fast", end='')
if config['good']:
    print("good", end='')
if config['cheap']:
    print("cheap", end='')
print()

optFlags 一样,optParameters 与继承一起顺利工作。

选项子命令

有时,根据逻辑“操作”将一组选项分组在一起非常有用。为此,usage.Options 类允许您定义一组“子命令”,每个子命令都可以提供自己的 usage.Options 实例来处理其特定的选项。

以下是一个 Options 类示例,它可能解析类似于 cvs 程序所接受的选项

from twisted.python import usage

class ImportOptions(usage.Options):
    optParameters = [
        ['module', 'm', None, None], ['vendor', 'v', None, None],
        ['release', 'r', None]
    ]

class CheckoutOptions(usage.Options):
    optParameters = [['module', 'm', None, None], ['tag', 'r', None, None]]

class Options(usage.Options):
    subCommands = [['import', None, ImportOptions, "Do an Import"],
                   ['checkout', None, CheckoutOptions, "Do a Checkout"]]

    optParameters = [
        ['compression', 'z', 0, 'Use compression'],
        ['repository', 'r', None, 'Specify an alternate repository']
    ]

config = Options(); config.parseOptions()
if config.subCommand == 'import':
    doImport(config.subOptions)
elif config.subCommand == 'checkout':
    doCheckout(config.subOptions)

OptionssubCommands 属性指示解析器在命令行中出现字符串 "import""checkout" 时,使用另外两个 Options 子类。给定命令字符串后的所有选项都将传递给指定的 Options 子类以进行进一步解析。一次只能指定一个子命令。解析完成后,Options 实例将有两个新属性 - subCommandsubOptions - 它们分别保存命令字符串和用于解析剩余选项的 Options 实例。

选项的通用代码

有时,仅仅根据选项设置属性还不够灵活。在这些情况下,Twisted 甚至不会尝试提供诸如“计数”或“列表”之类的抽象,而是让您调用自己的方法,该方法将在遇到选项时被调用。

以下是一个计算详细程度的示例

from twisted.python import usage

class Options(usage.Options):

    def __init__(self):
        usage.Options.__init__(self)
        self['verbosity'] = 0 # default

    def opt_verbose(self):
        self['verbosity'] = self['verbosity']+1

    def opt_quiet(self):
        self['verbosity'] = self['verbosity']-1

    opt_v = opt_verbose
    opt_q = opt_quiet

类似于 command -v -v -v -v 的命令行将详细程度增加到 4,而 command -q -q -q 将详细程度降低到 -3。

usage.Options 类知道这些是无参数选项,因为方法没有接收参数。以下是一个带参数方法的示例

from twisted.python import usage

class Options(usage.Options):

    def __init__(self):
        usage.Options.__init__(self)
        self['symbols'] = []

    def opt_define(self, symbol):
        self['symbols'].append(symbol)

    opt_D = opt_define

此示例对于使用 command -DFOO -DBAR 定义符号的常见语法很有用。

解析参数

usage.Options 在最后一个参数消失后不会停止帮助。所有其他参数都将发送到一个函数中,该函数应该处理它们。以下是一个类似于 cmp 命令的示例。

from twisted.python import usage

class Options(usage.Options):

    optParameters = [["max_differences", "d", 1, None]]

    def parseArgs(self, origin, changed):
        self['origin'] = origin
        self['changed'] = changed

该命令应该类似于 command origin changed

如果您想拥有可变数量的剩余参数,只需使用 def parseArgs(self, *args):。这对于类似于 UNIX cat(1) 的命令很有用。

后处理

有时,您可能希望对选项进行后处理以修复不一致之处等等。以下是一个示例

from twisted.python import usage

class Options(usage.Options):

    optFlags = [
        ["fast", "f", "Run quickly"],
        ["good", "g", "Don't validate input"],
        ["cheap", "c", "Use cheap resources"]
    ]

    def postOptions(self):
        if self['fast'] and self['good'] and self['cheap']:
            raise usage.UsageError("can't have it all, brother")

类型强制

默认情况下,所有选项都作为字符串处理。您可能希望在某些特定情况下强制执行选项的类型,最典型的例子是端口号。任何可调用对象都可以指定在 optParameters 的第五行中,并且将使用传递的参数字符串值调用它。

from twisted.python import usage

class Options(usage.Options):
    optParameters = [
            ["shiny_integer", "s", 1, None, int],
            ["dummy_float", "d", 3.14159, None, float],
        ]

请注意,默认值不会被强制转换,因此您应该要么使用正确的类型声明它(如上所示),要么在使用选项时处理它。

coerce 函数可能包含一个 coerceDoc 属性,其内容将在选项文档之后打印。这在多个地方重复使用该函数时特别有用。

def oneTwoThree(val):
    val = int(val)
    if val not in range(1, 4):
        raise ValueError("Not in range")
    return val
oneTwoThree.coerceDoc = "Must be 1, 2 or 3."

from twisted.python import usage

class Options(usage.Options):
    optParameters = [["one_choice", "o", 1, None, oneTwoThree]]

将以下示例代码添加到您的程序中,将打印以下帮助信息

$ python myprogram.py --help
Usage: myprogram [options]
Options:
  -o, --one_choice=           [default: 0]. Must be 1, 2 or 3.

Shell 选项卡自动完成

Options 类可以为交互式命令 shell 提供选项卡自动完成功能。目前仅支持 zsh,但未来可能会支持 bash

Twisted 附带的所有命令都自动支持选项卡自动完成。多年来,Zsh 一直提供一个完成函数,该函数与 Options 类提供的支持相结合。

如果您正在编写 twistd 插件,那么您的 twistd 子命令的选项卡自动完成也是自动的。

对于其他命令,您可以轻松地提供 zsh 选项卡自动完成支持。复制文件“twisted/python/twisted-completion.zsh”并将其命名为类似“_mycommand”的文件。zsh 使用以下划线开头的无扩展名作为完成函数文件的约定。

编辑新文件并将第一行更改为仅引用您的新命令,如下所示

#compdef mycommand

然后,通过将该文件放在 zsh 的 $fpath 中出现的目录之一中,确保该文件对 shell 可用。重新启动 zsh 并确保启用高级完成 (autoload -U compinit; compinit))。然后,您应该能够键入命令名称并按 Tab 键以完成命令行选项。

完成元数据

可选地,可以在您的 Options 子类上定义一个特殊属性 compData,以向 shell 自动完成系统提供更多信息。该属性应该是一个 I DON’T KNOW WHAT TO DO WITH THIS LINK! 的实例。

此外,可以在继承层次结构中的父类上定义 compData。来自每个 I DON’T KNOW WHAT TO DO WITH THIS LINK! 的信息。