Commander 是一个 Swift 框架,通过与 Swift 标准库协议 Decodable 和 Decoder 集成来解码命令行参数。 Commander 可以帮助您通过声明 command
的结构和该命令的 options
,而无需编写任何代码来解析 CLI 参数,从而编写结构化的 CLI 程序。 使用 Commander,您只需专注于编写命令的 options
结构,其余工作将由 Commander 自动处理。
struct
或 class
来进行结构化。Decodable
协议,选项类型是类型安全的。commander
或 command
生成帮助消息。<Tab>
补全。 支持 Bash/Zsh <Tab>
自动补全脚本。swift build
的 Linux。使用 Commander,可以如下定义命令及其关联的选项
import Commander
public struct SampleCommand: CommandRepresentable {
public struct Options: OptionsRepresentable {
public typealias ArgumentsResolver = AnyArgumentsResolver<String>
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose = "verbose"
case stringValue = "string-value"
}
public static let keys: [Options.CodingKeys : Character] = [
.verbose: "v",
.stringValue: "s"
]
public static let descriptions: [Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
.stringValue: .usage("Pass a value of String to the command")
]
public var verbose: Bool = false
public var stringValue: String = ""
}
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
public static func main(_ options: Options) throws {
print(options)
print("arguments: \(options.arguments)")
print("\n\n\(Options.CodingKeys.stringValue.stringValue)")
}
}
然后,配置可用命令将如下所示
import Commander
Commander.commands = [
SampleCommand.self,
NoArgsCommand.self
]
Commander.usage = "The sample usage command of 'Commander'"
Commander().dispatch() // Call this to dispatch and run the command
之后,可以通过声明 ArgumentsResolver
来解析参数
public typealias ArgumentsResolver = AnyArgumentsResolver<T> // T must be Decodable
您可以通过以下方式获取参数
public static func main(_ options: Options) throws {
print("arguments: \(options.arguments)") // 'arguments' is being declared in OptionsRepresentable
}
最后,从 shell 运行
commander-sample sample --verbose --string-value String arg1 arg2
#
# Options(verbose: true, stringValue: "String")
# arguments: ["arg1", "arg2"]
#
#
# string-value
它简单又有趣!!!
// swift-tools-version:4.2
dependencies: [
.package(url: "https://github.com/devedbox/Commander.git", "0.5.6..<100.0.0")
]
commander command --key value --key1=value1
commander command --bool
commander command -k value -K=value1
commander command -z=value # {"z": "value"}
commander command -z # {"z": true}
commander command -zop # {"z": true, "o": true, "p": true}
commander command --array val1,val2,val3
commander command -a val1,val2,val3
commander command --dict key1=val1,key2=val2,key3=val3
commander command -d key1=val1,key2=val2,key3=val3
commander command --array val1 --array val2 --array val3
commander command -a val1 -a val2 -a val3
commander command --dict key1=val1 --dict key2=val2 --dict key3=val3
commander command -d key1=val1 -d key2=val2 -d key3=val3
在 Commander 中,参数的位置是不固定的,它们可以在任何地方,但参数必须是连续的
commander command args... --options # before options
commander command --options args... # after options
commander command --options args... --options # between options
commander command arg0... --options arg1... --options # Error
使用 --
标记选项的结束和参数的开始,但是,这在 Commander 中通常是可选的
commander 命令 --选项 -- args...
众所周知,来自 CommandLine.arguments
的所有参数都是 String
类型,在 Commander 中,可用的值类型是
commander command --verbose
commander command --int 100
commander command --string "这是一个字符串值"
commander command --array val1,val2,val3
commander command --dict key1=val1,key2=val2,key3=val3
数组对象由字符 ,
分隔,字典对象由字符 =
和 ,
分隔。
Commander 支持主 commander 以及该 commander 的命令,每个命令都有其自己的子命令和选项。
使用 Commander 很简单,您只需要声明 commander 的 commands
、usage
,然后调用 Commander().dispatch()
,Commander 将自动解码命令行参数并将解码后的选项分发到命令行给定的特定命令。
就像以下一样简单
import Commander
Commander.commands = [
SampleCommand.self,
NoArgsCommand.self
]
Commander.usage = "The sample usage command of 'Commander'"
Commander().dispatch()
在 Commander 中,命令是一种类型(class
或 struct
),它符合协议 CommandRepresentable
。 CommandRepresentable 协议声明了符合命令的信息
Options
:命令选项的关联类型。symbol
:命令行 shell 使用的命令符号。usage
:该命令的用法帮助消息。children
:该命令的子命令。public struct Hello: CommandRepresentable {
public struct Options: OptionsRepresentable {
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose
}
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
]
public var verbose: Bool = false
}
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
public static func main(_ options: Options) throws {
if options.verbose {
print(options.argiments.first ?? "")
}
}
}
一旦创建了命令,就可以针对参数列表进行分发,通常是从 CommandLine.arguments 中获取,并删除命令本身的符号。
let arguments = ["sample", "--verbose", "Hello world"]
Command.dispatch(with: arguments.dropFirst())
// Hello world
作为命令的实际分发,您不需要手动分发命令,分发将由 Commander 自动处理。
在 Commander 中添加子命令是通过声明类型为 [CommandDescribable.Type]
的 children
。
public struct Hello: CommandRepresentable {
...
public static let children: [CommandDescribable.Type] = [
Subcommand1.self,
Subcommand2.self
]
...
}
Options
与 command 相同,是一种类型(class
或 struct
),它符合从 Decodable
继承的协议 OptionsRepresentable
,可以被视为一个简单的数据模型,将由 Commander 中的内置代码类型 OptionsDecoder
进行解码。
正如前面在创建命令中提到的,声明一个选项类型非常容易,只是另一个数据模型表示命令行参数中的原始字符串
public struct Options: OptionsRepresentable {
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose
}
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
]
public var verbose: Bool = false
}
正如声明为 public var verbose: Bool
,我们可以在命令行中使用带有 --verbose
的符号,但是如何在命令行中使用另一个不同的符号来包装 verbose
,例如 --is-verbose
? 在 Commander 中,我们可以这样做
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose = "is-verbose"
}
有时在开发命令行工具时,使用像 -v
这样的模式是必要的和有用的。 在 Commander 中,为选项提供短键很容易,我们只需要在 Options.keys
中声明一个类型为 [CodingKeys: Character]
的键值对
public struct Options: OptionsRepresentable {
...
public static let keys: [CodingKeys: Character] = [
.verbose: "v"
]
...
}
当我们在命令中定义一个标志选项时,需要为该标志提供一个默认值,因为如果我们忘记在命令行中输入该标志,则该标志的值意味着 false
。 在 Commander 中提供默认值是通过在 Options.descritions
中添加声明来实现的,如下所示
public struct Options: OptionsRepresentable {
...
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .default(value: false, usage:"Prints the logs of the command")
]
...
}
在 Commander 中,帮助菜单由描述符合 CommandDescribable
类型的 CommandDescriber
自动生成,包括 commander 本身和所有声明的命令。
为了提供帮助菜单的用法,在命令中
public struct Hello: CommandRepresentable {
...
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
...
}
在选项中
public struct Options: OptionsRepresentable {
...
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .default(value: false, usage:"Prints the logs of the command")
]
...
}
通常,帮助用法消息和默认值都可以由类型 OptionsDescriotion
提供。
在声明用法后,在终端中运行 help
commander-sample --help # or, commander-sample help
# Usage:
#
# $ commander-sample [COMMAND] [OPTIONS]
#
# The sample usage command of 'Commander'
#
# Commands:
#
# help Prints the help message of the command. Usage: [help [COMMANDS]]
# sample Show sample usage of commander
# set-args Set arguments of the command with given arguments
#
# Options:
#
# -h, --help Prints the help message of the command. Usage: [[--help|-h][COMMAND --help][COMMAND -h]]
对于特定命令,运行如下
commander-sample help sample # or, commander-sample sample --help
# Usage of 'sample':
#
# $ commander-sample sample [SUBCOMMAND] [OPTIONS] [ARGUMENTS]
#
# Show sample usage of commander
#
# Subcommands:
#
# set-args Set arguments of the command with given arguments
#
# Options:
#
# -s, --string-value Pass a value of String to the command
# -h, --help Prints the help message of the command. Usage: [[--help|-h][COMMAND --help][COMMAND -h]]
# -v, --verbose Prints the logs of the command
#
# Arguments:
#
# [String] commander-sample sample [options] arg1 arg2 ...
在 Commander 中,一个选项可以从命令行参数中获取多个参数作为该选项的参数,并且可以通过调用 options.arguments
来访问。 默认情况下,参数解码是无法解析的,如果您想解析参数的解码,则必须声明选项的 ArgumentsResolver
public struct Options: OptionsRepresentable {
...
public typealias ArgumentsResolver = AnyArgumentsResolver<String>
...
}
类型 AnyArgumentsResolver<T>
是泛型类型,其中类型 T
表示参数元素的类型。 通过上面的声明,我们可以在命令行中执行此操作
commander hello --verbose -- "Hello world" "Will be dropped"
# "Hello world" "Will be dropped" are both the arguments of Hello.Options
Commander 提供了在 bash/zsh 中编写自动完成的 API,该要求在协议 ShellCompletable
中声明。 默认情况下,CommandDescribable
和 OptionsDescribable
继承自 ShellCompletable
。
要实现自动完成,您只需要编写
import Commander.Utility
// Options:
public static func completions(for commandLine: Utility.CommandLine) -> [String] {
switch key {
case "--string-value":
return [
"a", "b", "c"
]
default:
return [ ]
}
}
在终端中,输入这个
commander sample --string-value <Tab>
# a b c
Commander 可以为您生成自动完成脚本,您可以运行内置命令 complete generate
来根据 shell 类型生成脚本。 当前可用的 shell 有
commander complete generate --shell=bash > ./bash_completion
source ./bash_completion
~/.profile
中更好地使用commander complete generate --shell=zsh > ~/zsh_completions/_commander
~/.zshrc
fpath=(~/zsh_completions $fpath)
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
CommandDescribable
已经提供了完成的默认实现,默认情况下,CommandDescribable 提供子命令以及选项作为 shell 的完成,您可以覆盖默认实现,以便为 Commander 提供您的自定义完成。
默认情况下,OptionsDescribable
返回一个空完成,在调用 CommandDescribable 期间会自动调用 OptionsDescribable,您必须覆盖 OptionsDescribable 的实现以提供您的完成,否则将使用空完成。
这是一个向 shell 提供 git branchs
完成的示例
import Commander.Utility
public static func completions(for commandLine: Utility.CommandLine) -> [String] {
let current = commandLine.arguments.last
let previous = commandLine.arguments.dropLast().last
switch current {
default:
let outputs = ShellIn("git branch -r").execute().output.flatMap {
String(data: $0, encoding: .utf8)
} ?? ""
return outputs.split(whereSeparator: {
" *->\n".contains($0)
}).map {
if $0.hasPrefix("origin/") {
return String(String($0)["origin/".endIndex...])
} else {
return String($0)
}
}
}
}
Commander 在 MIT 许可下发布。