argtree

Swift Platform Build

Swift 中的命令行参数解析器包。

基本思想是定义一个解析器树结构,然后解析所有命令行参数。 这种方法非常灵活,可以快速轻松地解析简单脚本的标志,以及大型命令行程序的复杂解析树。

目录

安装

Swift Package Manager

let package = Package(
        dependencies: [
          .package(url: "https://github.com/dastrobu/argtree.git", from: "1.5.5"),
        ]
)

依赖

至少需要 clang-3.6。 在 linux 上,可能需要显式安装它。 在 macOS 上没有依赖项。

快速入门

以下示例显示了一个带有一个标志(没有选项或命令)和生成的帮助的 hello world 脚本。

// global modal for the application
var verbose = false

try! ArgTree(description:
"""
usage: \(CommandLine.arguments[0])) [flags...]

hello world demo

flags:
""",
    parsers: [
        Flag(longName: "verbose", shortName: "v", description: "print verbose output") { _ in
            verbose = true
        }
    ]).parse()
    
// here comes the real program code after parsing the command line arguments
if verbose {
    print("hello world")
} else {
    print("hi")
}

帮助文本生成

可以自动生成帮助文本(部分),详见自动帮助标志。 这仅适用于全局帮助。 不会生成关于各个命令的帮助,但是,可以通过向命令添加一个 Help 标志来轻松实现。

解析器

已经实现了多种解析器来组合解析器树。

如果这些不够,很容易实现一个自定义解析器。 因此,必须实现 Parser 接口,详细信息请参阅架构

标志 (Flag)

标志是一个布尔属性,例如 -v--verbose。 标志有一个长名称和一个短名称,两者都是可选的(但是,不设置任何一个没有意义)。 处理标志可以通过 parsed 闭包来完成

let verbose = false
try! ArgTree(parsers: [
    Flag(longName: "verbose", shortName: "v") { value, path in verbose = true }
]).parse()

或者通过稍后访问已解析的值。

let verboseFlag = Flag(longName: "verbose", shortName: "v")
try! ArgTree(parsers: [verboseFlag]).parse()
let verbose = verboseFlag.value != nil

标志前缀 (Flag Prefixes)

默认情况下,长名称以 "--" 为前缀,短名称以 "-" 为前缀。 可以指定其他前缀来处理例如 +a

let a = Flag(shortName: "a", shortPrefix: "+")

长前缀也可以这样做。

多次传递标志 (Passing a Flag Multiple Times)

默认情况下,多次传递同一个标志会被报告为错误 (FlagParseError.flagAllowedOnlyOnce)。 但是,有时能够多次传递同一个标志是有用的,例如,如果 -v 应该打印详细输出,而 -v -v 应该打印非常详细的输出。 在这种情况下,标志应该将属性 multiAllowed 设置为 true。

let verboseFlag = Flag(longName: "verbose", shortName: "v", multiAllowed: true)
try! ArgTree(parsers: [verboseFlag]).parse()
let verbosity = verboseFlag.values.count

可以通过 values 属性访问该值被解析的次数。 由实现来决定多次传递同一个标志是被简单地忽略还是意味着一些有用的东西。

处理意外标志 (Handling Unexpected Flags)

默认情况下,如果在命令行设置了一个没有意义的标志,即没有被解析的标志,则什么也不会发生。 要将所有没有意义的类似标志的参数报告为错误,只需将 UnexpectedFlagHandler 添加到解析器中。 必须在标志解析器之后添加处理程序,才能正确报告错误。

try! ArgTree(parsers: [
        Flag(longName: "verbose", shortName: "v")
        UnexpectedFlagHandler()
]).parse()

在这种情况下,所有以长前缀或短前缀开头的参数都将被报告为错误。 UnexpectedFlagHandler 支持停止令牌,以允许使用类似标志的可变参数。 此外,可以自定义 longPrefixshortPrefix。 如果使用带有不同前缀的标志,例如 -a+a,则可以添加两个单独的 UnexpectedFlagHandler,一个用于标准前缀,一个用于 + 前缀。

多重标志 (Multi Flags)

多重标志是组合标志(对于短名称)。 例如,如果存在标志 -a-b,则也可以传递组合标志 -ab-ba,这等效于 -a -b

要实现这种解析,请使用 MultiFlag

try! ArgTree(parsers: [
    MultiFlag(parsers: [
        Flag(shortName: "a")
        Flag(shortName: "b")
    ])
]).parse()

请注意,多重标志和所有添加的标志必须具有相同的 shortPrefix 才能获得预期的结果。

自动帮助标志 (Automatic Help Flag)

如果使用 descriptionhelpText 初始化 ArgTree,则会自动添加一个帮助标志,该标志将显示帮助文本。 一个最小的例子是

try! ArgTree(helpText: "usage...").parse()

-h--help 作为标志传递来调用脚本将会打印

usage...

并在此之后退出。

如果传递了 helpText,它将被简单地打印出来。 或者,可以传递一个 description,它将从描述和所有传递的标志和选项的描述中生成帮助文本。

try! ArgTree(description: 
"""
usage: \(CommandLine.arguments[0]) [flags...]

flags:
""", 
parsers: [
    Flag(longName: "foo", shortName: "f", description: "a foo flag"),
]).parse()

此示例将打印

usage: my_script [flags...]

flags:
--foo, -f a foo flag
解析顺序 (Parse Order)

生成的帮助标志总是作为第一个解析器添加,以确保它与可变参数很好地配合使用。 在创建 ArgTree 对象后,可以通过 MutableCollection 协议(例如数组)操作其元素来更改解析器的顺序。 例如,要将自动生成的帮助标志解析器移动到末尾,请执行

var argTree = ArgTree(description: "foo")
argTree.append(argTree.removeFirst())
默认操作 (Default Action)

生成的帮助自动设置为默认操作。 如果这不是预期的,则可以取消设置默认操作或将其设置为其他操作。

let argTree = ArgTree(description: "usage...")
argTree.defaultAction = nil
try! argtree.parse()
打印帮助后退出 (Exit After Help Printed)

生成的帮助标志解析器总是在打印帮助文本后以代码 0 退出。 如果这不是预期的行为,则可以传递一个闭包,该闭包在打印帮助文本后被调用。 以下示例显示了如何在打印帮助后继续而不执行任何特定操作。

let argTree = ArgTree(description: "usage...") { /* do nothing after help was printed */ }
try! argtree.parse()
输出流 (Output Stream)

默认情况下,帮助文本被打印到 stdout。 可以通过设置 writeToOutStream 委托来对其进行自定义。 例如,可以将输出重定向到一个字符串。

let argTree = ArgTree(description: "usage...")
var out = ""
argTree.writeToOutStream = { s in
    print(s, to: &out)
}

选项 (Option)

选项是一个键值属性,例如 --foo=bar--foo bar。 选项有一个长名称和一个短名称,两者都是可选的。 支持两种语法,即可以通过 = 分隔传递键值对,或者通过将值作为后续参数传递给键。 如果无法解析键的值,则它将被报告为错误 (OptionParseError.missingValueForOption)。 以下示例显示了选项的基本用法。

var foo: String = "default"
try! ArgTree(parsers: [
    Option(longName: "foo") {value, _ in foo = value}
]).parse()

选项前缀 (Option Prefixes)

可以像标志一样更改前缀,请参阅标志前缀

多次传递选项 (Passing an Option Multiple Times)

可以多次传递选项,指定不同的值。 默认情况下,多次传递一个选项会被报告为错误 (OptionParseError.optionAllowedOnlyOnce),就像标志一样,请参阅多次传递标志。 以下示例显示了如何实现一个可以传递不同值的选项。

let fooOption = Option(longName: "foo", multiAllowed: true)
try! ArgTree(parsers: [fooOption]).parse()
fooOption.values.forEach{ value in /* ... */ }

在这种情况下,通过解析后的 values 处理解析后的选项,而不是通过闭包处理它们,更有意义。 虽然这也是可能的。

var foo: [String] = []
try! ArgTree(parsers: [
    Option(longName: "foo", multiAllowed: true) { value, _ in 
        foo.append(value)
    }
]).parse()

Int 选项和 Double 选项 (Int Option and Double Option)

如果只允许整数或浮点数值作为选项,则可以使用方便的解析器 IntOptionDoubleOption。 它们的工作方式与 Option 完全相同,除了所有无法解析为 IntDouble 的值都将被报告为 OptionParseError.valueNotIntConvertibleOptionParseError.valueNotDoubleConvertible

处理意外选项 (Handling Unexpected Options)

该机制与标志相同,请参阅处理意外标志。 只需将一个 UnexpectedOptionHandler 添加到解析器中即可。

命令 (Command)

命令是一个特殊的参数,用于更改程序的控制流程。 简单的脚本或程序(例如 rm)通常没有命令,更高级的命令行程序(例如 git)支持各种命令,甚至是子命令。 以下示例显示了一个程序的实现,该程序处理命令 foo

try! ArgTree(parsers: [
    Command(name: "foo") { path in 
        /* handle foo command */ }
]).parse()

当涉及到处理命令时,事情很快就会变得复杂。 例如,会出现以下问题

好消息是,所有事情都可以用 ArgTree 来完成。 但是,它需要对解析的工作方式有一定的了解,因为将解析器添加到解析树的顺序很重要。 为了支持找出正确的顺序,请考虑在解析时打开日志记录,请参阅日志

以下各节中的示例详细介绍了一些上述情况。

全局标志(或选项)(Global Flags (or Options))

一个对每个命令都执行相同操作的全局标志很容易实现。 只需在所有命令之前添加它即可。

var verbose = false;
try! ArgTree(parsers: [
    Flag(longName: "verbose", shortName: "v") { _ in verbose = true }
    Command(name: "foo") 
]).parse()

选项也可以这样做。

半全局标志(或选项)(Semi-Global Flags (or Options))

所谓的半全局标志是可以设置在任何级别但具有不同效果的标志。 以下示例显示了如果不想使用生成的帮助,如何实现自定义帮助标志。

try! ArgTree(parsers: [
        Command(name: "foo", parsers: [
            Flag(longName: "help", shortName: "h") { _ in print("help for foo") }
        ]) 
        Flag(longName: "help", shortName: "h") { _ in print("global help") }
    ]).parse()

相应的解析树是

argTree
  +-- foo
  |   +-- help(1)
  +-- help(2)

这里使用了两个不同的 Flag 实例(help(1)help(2))来解析帮助标志。

除了定义单独的 Flag 实例之外,还可以基于解析路径执行不同的操作。

let help = Flag(longName: "help", shortName: "h")
let foo = Command(name: "foo", parsers: [help])
help.parsed = { path in
    switch path.last {
        case let cmd as Command where cmd === foo:
            print("foo help")
        // case let cmd as Command where ...  (other commands)
        default:
            print("global help")
        }
        exit(0)
    }
try! ArgTree(parsers: [help foo]).parse()

通过使用路径来确定标志的上下文,可以实现非常通用的实现。

相应的解析树是

argTree
  +-- foo
  |   +-- help
  +-- help

哪种策略更好取决于具体的使用场景。如果标志在每个命令中都应该具有相同的描述,那么最好在所有地方都使用相同的实例,并基于路径片段来实现逻辑。另一方面,如果每个命令的描述都不同,那么使用不同的实例可能会更简单。

命令上的可变参数 (Var Args on Commands)

有关可变参数的总体信息,请参见可变参数。如果可变参数应该传递给特定的命令,只需将可变参数添加到该命令即可。

let fooVarArgs = VarArgs()
try! ArgTree(parsers: [
    Command(name: "foo", parsers: [fooVarArgs]) 
]).parse()

要在全局级别添加对可变参数的支持,只需在该级别添加另一个可变参数对象。它必须添加到命令之后,否则该命令将被解析为可变参数。

let globalVarArgs = VarArgs()
let fooVarArgs = VarArgs()
try! ArgTree(parsers: [
    Command(name: "foo", parsers: [fooVarArgs]),
    globalVarArgs,
]).parse()

请看一些示例,了解在这种情况下可变参数将如何解析。

my_script a b         # a and b parsed by globalVarArgs
my_script foo a b     # a and b parsed by fooVarArgs
my_script a foo b     # a parsed by globalVarArgs and b parsed by fooVarArgs

当查看解析树时,这非常简单明了。

argTree
  +-- foo
  |   +-- globalVarArgs
  +-- fooVarArgs

命令默认操作 (Command Default Action)

与解析树的根节点 ArgTree 类似,每个命令都有一个可选的默认操作。如果没有子解析器消耗任何进一步的参数,则会调用默认操作。默认操作可以直接在命令上设置。

let foo = Command(name: "foo" parsers: [
        Flag(longName: "bar") { _ in print("--bar parsed") }
    ]) { _ in 
    print("foo (maybe also --bar parsed)") 
}
foo.defaultAction = { () in print("foo (--bar not parsed)") }
try! ArgTree(parsers: [foo, baz]).parse()

parsedafterChildrenParsed

命令有两个可选的委托:parsedafterChildrenParsed。第一个 parsed 在命令解析后立即调用,就像标志和选项一样。但是,此时,parsers 属性中的解析器尚未解析任何进一步的参数。第二个委托 afterChildrenParsed 在命令解析后调用,并且所有后续参数也由 parsers 属性中的解析器解析。因此,当命令的定义如下例所示时

let bar = Flag(longName: "bar")
let foo = Command(name: "foo", parsers: [bar]) { _ in print("bar?: \(bar.value)") }

尾随闭包指的是 afterChildrenParsed,并且可以访问来自任何子解析器的所有已解析值。

嵌套命令 (Nested Commands)

CommandsFlagsOptions 等可以嵌套到任意深度。这就是该软件包被称为 ArgTree 的原因。这是一个简单的例子。

let bar = Command(name: "bar") { _ in print("foo bar") }
let foo = Command(name: "foo", parsers: [foo])
let baz = Command(name: "baz") { _ in print("baz") }
foo.defaultAction = { () in print("foo (no sub command)") }
try! ArgTree(parsers: [foo, baz]).parse()

这是解析树。

argTree
  +-- foo
  |   +-- bar
  +-- baz

可变参数 (Var Args)

可变参数是指所有未被任何其他解析器专门解析的参数。通常,脚本会接受任意数量的文件作为可变参数。考虑以下示例

my_script -v file1 file2
my_script file1 file2 --verbose

两个脚本都应该将 file1file2 作为可变参数处理,并将 -v--verbose 作为标志处理,而不管其位置如何(要将 -v--verbose 作为文件处理,请参见停止令牌)。这可以通过定义以下解析树来实现。

let varArgs = VarArgs()
let argTree = ArgTree(parsers: [
    Flag(longName: "verbose", shortName: "-v", description: "verbose output") { _ _ in }
    varArgs
])

正如在示例中所看到的,像定义 Flag 一样内联定义 VarArgs 是可能的,但没有意义。请注意,重要的是将 varArgs 放在 Flag 之后,否则每个参数都将被解析为可变参数,而不是解析为标志。因此,通常 VarArgs 添加在 parsers 数组的最后。已解析的可变参数通常在解析完成后通过 RandomAccessCollection 协议(例如数组)处理。

try! argTree.parse()
varArgs.values.forEach{ value in /* ... */ }

处理意外参数 (Handling Unexpected Arguments)

如果未使用任何可变参数,则将所有错误报告为意外参数可能会有所帮助。在这种情况下,可以将 UnexpectedArgHandler 添加为最后一个解析器。这将报告之前未被其他解析器解析的任何参数为 ArgParseError.unexpectedArg

另请参见处理意外标志处理意外选项

停止令牌 (Stop Token)

停止令牌会停止将后续参数作为它们通常被解析的方式进行解析。默认情况下,-- 用作停止令牌。这意味着,在 -- 之后传递的所有参数都将被解析为可变参数。这很有用,例如,如果应该将与命令或标志名称冲突的文件名作为参数传递。一个例子是处理一个名为 -h 的文件

my_script -- -h

这通常会打印帮助文本。在这种情况下,-h 被视为可变参数,例如,一个文件名。

默认操作 (Default Action)

如果无法解析任何参数,则可以执行默认操作。如果存在生成的帮助标志,则默认操作将设置为打印帮助文本并退出。这可以通过设置(或取消设置)默认操作来定制。

let argTree = ArgTree()
argTree.defaultAction = { () in print("this is the default") }

另请参见命令默认操作

错误处理 (Error Handling)

parse 应该在解析参数时抛出错误。可以抛出各种错误,并且在简单的脚本中,强制尝试解析调用可能就足够了(如本文档中的所有示例)。任何解析错误都将报告给 stderr,并且程序将退出。虽然这种默认行为对于简单的脚本和程序来说已经足够,但更复杂的程序可能会打印出更友好的错误消息。这可以通过捕获错误并进行一些友好的错误处理来完成。

do {
    try ArgTree(parsers: [UnexpectedArgHandler()]).parse()
} catch ArgParseError.unexpectedArg(argument:let arg, atIndex:_) {
    print("got an unexpected argument: \(arg)")
    exit(1)
} catch let error {
    print("unknown error \(error)")
    exit(99)
}

日志 (Logging)

要更深入地了解为什么某个参数被解析或未被解析,打开日志记录可能非常有用。

日志记录是通过swift-log API 完成的。因此,默认情况下不会记录任何内容。要激活日志记录,必须配置一个日志记录器,例如(HeliumLogger)或 StreamLogHandler,可以按以下方式使用它。

import Logging
LoggingSystem.bootstrap({ label in
    var logHandler = StreamLogHandler.standardError(label: label)
    logHandler.logLevel = .trace/
    logHandler = logHandler
    return logHandler
})

let argTree = ArgTree()
try! argtree.parse()

请注意,大多数日志记录都是在调试级别完成的,因此应该激活此级别才能看到任何日志输出。

架构 (Architecture)

基本的想法是定义一个解析器树,然后逐个消耗参数。此软件包有助于定义解析器树并调用它。解析器树中的每个节点通常解析特定类型的参数,例如标志或选项。要理解必须如何设置解析器树,了解如何遍历该树非常重要。考虑以下示例

arguments = ['arg_0', 'arg_1', 'arg_2']

第一个参数总是被忽略,因为它指的是脚本名称。解析从 arg_1 开始。现在,如果有以下树

argTree
  +-- parser_0
  |   +-- parser_0_1
  +-- parser_1

首先,将解析 arg_1。因此,argTree 使用参数数组和索引 i(应该完成解析的位置)调用每个子解析器。每个子解析器(在本例中为 parser_0parser_1)可以决定从 i 开始消耗任意数量的参数,并返回它消耗了多少个参数。因此,如果 parser_0 决定消耗所有参数,则永远不会调用 parser_1,因为没有剩余的参数。如果 parser_0 不消耗任何参数,则在同一索引 i 上调用 parser_1,并且也可以消耗任意数量的参数。对于任何索引 i,一旦一个子解析器消耗了非零数量的参数,就会停止调用后续的子解析器。之后,索引 i 增加消耗的参数数量,并且子解析器列表从头开始再次迭代。因此,如果 parser_0 消耗了 arg_1,则不会调用 parser_1。相反,i 增加 1,并且再次为 arg_2 调用 parser_0。因此,重要的是要理解,索引较低的解析器总是比后面的解析器具有更高的优先级。一个特殊情况是,如果某个参数未被任何解析器消耗。在这种情况下,i 增加 1,并解析下一个参数。这意味着未解析的参数将被简单地忽略。如果忽略参数不是预期的行为,则可以添加 UnexpectedArgHandler 以抛出错误。现在应该清楚的是,必须将 UnexpectedArgHandler 添加为最后一个解析器,因为它只是消耗任何参数并将其转换为错误。

在理解了一个节点的解析过程之后,理解整个树就很简单了。由于每个解析器节点都可以消耗任意数量的参数,因此节点如何解析参数并不重要。因此,每个节点本身都可以以描述的方式委托给子解析器。这使得重用用于标志和选项的简单解析器变得非常容易。这是一个命令行程序的简短示例,该程序接受一个全局标志 -v 和两个命令 foobar,它们本身分别接受标志 -f-b

argTree
  +-- -v
  +-- foo
  |   +-- -f
  +-- bar
      +-- -b

解析路径 (Parse Path)

为了定义已解析参数的上下文,始终指定解析路径。这只是调用链中的解析器数组。请注意,根解析器未添加到路径中。因此,对于以下示例

argTree
  +-- -v
  +-- foo
  |   +-- -f
  +-- bar
      +-- -b

解析时将存在以下路径

[] : -v
[foo] : -f
[bar] : -b

如果应该多次使用解析器,但应该感知上下文,则可以使用此路径。例如,如果 -v 对于 foo 和对于 bar 应该做不同的事情,则应该定义以下树

argTree
  +-- foo
  |   +-- -v
  |   +-- -f
  +-- bar
      +-- -v
      +-- -b

或者,另一种选择是

argTree
  +-- -v
  +-- foo
  |   +-- -v
  |   +-- -f
  +-- bar
      +-- -v
      +-- -b

如果还应该在全球级别支持 -v。有关特定于路径的操作的示例,请参见半全局标志(或选项)

文档 (Docs)

阅读生成的文档