一个强大的框架,用于在 Swift 中开发命令行界面(CLI),从最简单到最复杂。
import SwiftCLI
class GreetCommand: Command {
let name = "greet"
@Param var person: String
func execute() throws {
stdout <<< "Hello \(person)!"
}
}
let greeter = CLI(name: "greeter")
greeter.commands = [GreetCommand()]
greeter.go()
~ > greeter greet world
Hello world!
使用 SwiftCLI,您将自动获得
> ice add jakeheis/SwiftCLI
将 SwiftCLI 添加为项目的依赖项
dependencies: [
.package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0")
]
github "jakeheis/SwiftCLI" ~> 5.2.2
pod 'SwiftCLI', '~> 6.0.0'
创建 CLI
时,需要一个 name
,而 version
和 description
都是可选的。
let myCli = CLI(name: "greeter", version: "1.0.0", description: "Greeter - a friendly greeter")
您可以通过 .commands
属性设置命令
myCli.commands = [myCommand, myOtherCommand]
最后,要运行 CLI,您可以调用 go
方法之一。
// Use go if you want program execution to continue afterwards
myCli.go()
// Use goAndExit if you want your program to terminate after the CLI has finished
myCli.goAndExit()
// Use go(with:) if you want to control the arguments which the CLI runs with
myCli.go(with: ["arg1", "arg2"])
为了创建一个命令,您必须实现 Command
协议。 所有需要做的是实现 name
属性和 execute
函数; Command
的其他属性是可选的(但强烈建议使用 shortDescription
)。 可以创建如下一个简单的 hello world 命令
class GreetCommand: Command {
let name = "greet"
let shortDescription = "Says hello to the world"
func execute() throws {
stdout <<< "Hello world!"
}
}
命令可以通过某些实例变量指定它接受哪些参数。 使用反射,SwiftCLI 将识别类型为 @Param
和 @CollectedParam
的属性包装器。 这些属性应按照命令期望用户传递参数的顺序出现。 所有必需参数必须首先出现,然后是任何可选参数,最后最多只能有一个收集参数。
class GreetCommand: Command {
let name = "greet"
@Param var first: String
@Param var second: String?
@CollectedParam var remaining: [String]
}
在此示例中,如果用户运行 greeter greet Jack Jill up the hill
,则 first
将包含值 Jack
,second
将包含值 Jill
,而 remaining
将包含值 ["up", "the", "hill"]
。
单个参数采用属性包装器 @Param
的形式。 由 @Param
包装的属性可以是必需的或可选的。 如果没有给命令传递足够的参数来满足所有必需的参数,则命令将失败。
class GreetCommand: Command {
let name = "greet"
@Param var person: String
@Param var followUp: String
func execute() throws {
stdout <<< "Hey there, \(person)!"
stdout <<< followUp
}
}
~ > greeter greet Jack
Usage: greeter greet <person> <followUp> [options]
Options:
-h, --help Show help information
Error: command requires exactly 2 arguments
~ > greeter greet Jack "What's up?"
Hey there, Jack!
What's up?
如果用户没有传递足够的参数来满足所有可选参数,则这些未满足参数的值将为 nil
。
class GreetCommand: Command {
let name = "greet"
@Param var person: String
@Param var followUp: String? // Note: String? in this example, not String
func execute() throws {
stdout <<< "Hey there, \(person)!"
if let followUpText = followUp {
stdout <<< followUpText
}
}
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack "What's up?"
Hello, Jack!
What's up?
命令在所有其他参数之后可以有一个单独的收集参数,称为 @CollectedParam
。 此参数允许用户传递任意数量的参数,这些参数将被收集到由收集参数包装的数组中。 由 @CollectedParam
包装的属性**必须**是一个数组。 默认情况下,@CollectedParam
不要求用户传递任何参数。 可以使用 @CollectedParam(minCount:)
初始化程序来要求参数具有一定数量的值。
class GreetCommand: Command {
let name = "greet"
@CollectedParam(minCount: 1) var people: [String]
func execute() throws {
for person in people {
stdout <<< "Hey there, \(person)!"
}
}
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack Jill Water
Hey there, Jack!
Hey there, Jill!
Hey there, Water!
使用所有这些参数属性包装器,只要类型符合 ConvertibleFromString
,就可以使用任何类型。 大多数原始类型(例如 Int
)已经符合 ConvertibleFromString
,具有原始值为原始类型的枚举也是如此。
class GreetCommand: Command {
let name = "greet"
@Param var number: Int
func execute() throws {
stdout <<< "Hey there, number \(number)!"
}
}
~ > greeter greet Jack
Usage: greeter greet <number> [options]
Options:
-h, --help Show help information
Error: invalid value passed to 'number'; expected Int
~ > greeter greet 4
Hey there, number 4!
具有符合 CaseIterable
的枚举类型的参数具有其他专门的行为。 在错误消息中,将详细说明该参数允许的值。
class GreetCommand: Command {
let name = "greet"
enum Volume: String, ConvertibleFromString, CaseIterable {
case loud
case quiet
}
@Param var volume: Volume
func execute() throws {
let greeting = "Hello world!"
switch volume {
case .loud: stdout <<< greeting.uppercased()
case .quiet: stdout <<< greeting.lowercased()
}
}
}
~ > greeter greet Jack
Usage: greeter greet <volume> [options]
Options:
-h, --help Show help information
Error: invalid value passed to 'volume'; expected one of: loud, quiet
~ > greet greet loud
HELLO WORLD!
要使自定义类型符合 ConvertibleFromString
,只需实现一个函数
extension MyType: ConvertibleFromString {
init?(input: String) {
// Construct an instance of MyType from the String, or return nil if not possible
...
}
}
命令支持两种类型的选项:flag 选项和 keyed 选项。 两种类型的选项都可以用短划线后跟一个字母(例如 git commit -a
)或两个短划线后跟选项名称(例如 git commit --all
)来表示。 单个字母选项可以级联成一个短划线后跟所有想要的选项:git commit -am "message"
== git commit -a -m "message"
。
选项用命令类上的属性包装器指定,就像参数一样
class ExampleCommand: Command {
...
@Flag("-a", "--all")
var flag: Bool
@Key("-t", "--times")
var key: Int?
...
}
Flags 是充当布尔开关的简单选项。 例如,如果要实现 git commit
,则 -a
将是一个 flag 选项。 它们采用由 @Flag
包装的布尔值的形式。
GreetCommand
可以采用一个 “loudly” flag
class GreetCommand: Command {
...
@Flag("-l", "--loudly", description: "Say the greeting loudly")
var loudly: Bool
func execute() throws {
if loudly {
...
} else {
...
}
}
}
相关的选项类型是 @CounterFlag
,它计算用户传递相同 flag 的次数。 @CounterFlag
只能包装类型为 Int
的属性。 例如,使用像这样的 flag 声明
class GreetCommand: Command {
...
@CounterFlag("-s", "--softly", description: "Say the greeting softly")
var softly: Int
...
}
用户可以写 greeter greet -s -s
,并且 softly.value
将是 2
。
Keys 是具有关联值的选项。 以 "git commit" 为例,"-m" 将是一个 keyed 选项,因为它具有关联的值 - 提交消息。 它们采用由 '@Key` 包装的变量的形式。
GreetCommand
可以采用一个 “number of times” 选项
class GreetCommand: Command {
...
@Key("-n", "--number-of-times", description: "Say the greeting a certain number of times")
var numberOfTimes: Int?
func execute() throws {
for i in 0..<(numberOfTimes ?? 1) {
...
}
}
}
由 @Key
包装的变量可以是符合上述 ConvertibleFromString
的任何类型。 它**必须**是可选的,否则 Swift 编译器将崩溃。
相关的选项类型是 VariadicKey
,它允许用户使用不同的值多次传递相同的 key。 例如,使用像这样的 key 声明
class GreetCommand: Command {
...
@VariadicKey("-l", "--location", description: "Say the greeting in a certain location")
var locations: [String]
...
}
用户可以写 greeter greet -l Chicago -l NYC
,并且 locations.value
将是 ["Chicago", "NYC"]
。 由 @VariadicKey
包装的变量必须是符合 ConvertibleFromString
的类型的数组。
多个选项之间的关系可以通过选项组来指定。 选项组允许命令指定用户必须传递一个组的最多一个选项(传递多个是一个错误),必须传递一个组的恰好一个选项(传递零个或多个于一个是错误),或者必须传递一个组的一个或多个选项(传递零个是一个错误)。
要添加选项组,Command
应该实现属性 optionGroups
。 选项组通过 $
语法引用选项。 例如,如果 GreetCommand
具有 loudly
flag 和 whisper
flag,但不希望用户能够同时传递两者,则可以使用 OptionGroup
class GreetCommand: Command {
...
@Flag("-l", "--loudly", description: "Say the greeting loudly")
var loudly: Bool
@Flag("-w", "--whisper", description: "Whisper the greeting")
var whisper: Bool
var optionGroups: [OptionGroup] {
return [.atMostOne($loudly, $whipser)] // Note: $loudly and $whisper, not loudly and whisper
}
func execute() throws {
if loudly {
...
} else if whisper {
...
} else {
...
}
}
}
全局选项可用于指定每个命令应具有的特定选项。 这是为所有命令实现 -h
flag 的方式。 只需将选项添加到 CLI 的 .globalOptions
数组中(并可选择扩展 Command
以使选项易于在您的命令中访问)
private let verboseFlag = Flag("-v")
extension Command {
var verbose: Bool {
return verboseFlag.value
}
}
myCli.globalOptions.append(verboseFlag)
默认情况下,每个命令都有一个 -h
flag,用于打印帮助信息。 您可以通过将 CLI helpFlag
设置为 nil 来关闭此功能
myCli.helpFlag = nil
如以上示例所示,@Flag
和 @Key
都采用可选的 description
参数。 此处应包含选项作用的简明描述。 这允许 HelpMessageGenerator
为命令生成完全信息性的用法说明。
在三种情况下显示命令的用法说明
greeter greet -z
greeter greet -h
~ > greeter greet -h
Usage: greeter greet <person> [options]
Options:
-l, --loudly Say the greeting loudly
-n, --number-of-times <value> Say the greeting a certain number of times
-h, --help Show help information for this command
命令组提供了一种将相关命令嵌套在特定命名空间下的方法。 组本身可以包含其他组。
class ConfigGroup: CommandGroup {
let name = "config"
let children = [GetCommand(), SetCommand()]
}
class GetCommand: Command {
let name = "get"
func execute() throws {}
}
class SetCommand: Command {
let name = "set"
func execute() throws {}
}
您可以将命令组添加到 CLI 的 .commands
数组中,就像添加普通命令一样
greeter.commands = [ConfigGroup()]
> greeter config
Usage: greeter config <command> [options]
Commands:
get
set
> greeter config set
> greeter config get
可以为您的 CLI 自动生成 Zsh 补全。
let myCli = CLI(...)
let generator = ZshCompletionGenerator(cli: myCli)
generator.writeCompletions()
将自动为命令名称和选项生成补全。 可以指定参数补全模式
@Param(completion: .none)
var noCompletions: String
@Param(completion: .filename)
var aFile: String
@Param(completion: .values([
("optionA", "the first available option"),
("optionB", "the second available option")
]))
var aValue: String
@Param(completion: .function("_my_custom_func"))
var aFunction: String
默认参数补全模式是 .filename
。 如果您使用 .function
指定自定义函数,则在创建补全生成器时必须提供该函数
class MyCommand {
...
@Param(completion: .function("_list_processes"))
var pid: String
...
}
let myCLI = CLI(...)
myCLI.commands [MyCommand()]
let generator = ZshCompletionGenerator(cli: myCli, functions: [
"_list_processes": """
local pids
pids=( $(ps -o pid=) )
_describe '' pids
"""
])
CLI
有两个内置命令:HelpCommand
和 VersionCommand
。
可以使用 myapp help
调用 HelpCommand
。 HelpCommand
首先打印应用程序描述(如果在 CLI.init
期间给出了任何描述)。 然后,它遍历所有可用的命令,打印它们的名称和简短描述。
~ > greeter help
Usage: greeter <command> [options]
Greeter - your own personal greeter
Commands:
greet Greets the given person
help Prints this help information
如果您不希望自动包含此命令,请将 helpCommand
属性设置为 nil
myCLI.helpCommand = nil
可以使用 myapp version
或 myapp --version
调用 VersionCommand
。 VersionCommand 打印在 init 期间给出的应用程序版本 CLI(name:version:)
。 如果未给出版本,则该命令不可用。
~ > greeter --version
Version: 1.0
如果您不希望自动包含此命令,请将 versionCommand
属性设置为 nil
myCLI.versionCommand = nil
Input
类使从 stdin 读取输入变得容易。 有几种方法可用
let str = Input.readLine()
let int = Input.readInt()
let double = Input.readDouble()
let bool = Input.readBool()
所有 read
方法都有四个可选参数
prompt
:接受输入之前要打印的消息(例如“Input: ”)secure
:如果为 true,则在用户键入时隐藏输入validation
:一个闭包,用于定义输入是否有效,或者是否应该重新提示用户errorResponse
:当用户输入无效输入时执行的闭包例如,您可以编写
let percentage = Input.readDouble(
prompt: "Percentage:",
validation: [.within(0...100)],
errorResponse: { (input, reason) in
Term.stderr <<< "'\(input)' is invalid; must be a number between 0 and 100"
}
)
这将导致如下交互
Percentage: asdf
'asdf' is invalid; must be a number between 0 and 100
Percentage: 104
'104' is invalid; must be a number between 0 and 100
Percentage: 43.6
SwiftCLI 使执行外部任务变得容易
// Execute a command and print output:
try Task.run("echo", "hello")
try Task.run(bash: "while true; do echo hi && sleep 1; done")
// Execute a command and capture the output:
let currentDirectory = try Task.capture("pwd").stdout
let sorted = try Task.capture(bash: "cat Package.swift | sort").stdout
您也可以使用 Task
类来获得更自定义的行为
let input = PipeStream()
let output = PipeStream()
let task = Task(executable: "sort", currentDirectory: "~/Ice", stdout: output, stdin: input)
task.runAsync()
input <<< "beta"
input <<< "alpha"
input.closeWrite()
output.readAll() // will be alpha\nbeta\n
有关 Task
的完整文档,请参见 Sources/SwiftCLI/Task.swift
。
如果您的 CLI 仅包含一个命令,您可能希望仅通过调用 cli
而不是 cli command
来执行该命令。 在这种情况下,您可以按如下方式创建您的 CLI
class Ln: Command {
let name = "ln"
func execute() throws { ... }
}
let ln = CLI(singleCommand: Ln())
ln.go()
在这种情况下,如果用户编写 ln myFile newLocation
,而不是搜索名称为 “myFile” 的命令,SwiftCLI
将执行 Ln
命令并将 “myFile” 作为该命令的第一个参数传递。
请记住,在创建单个命令的 CLI 时,您将失去默认的 VersionCommand
。这意味着 cli -v
将不会自动工作。如果您想打印 CLI 版本,您需要在单个命令上手动实现一个 Flag("-v")
。
SwiftCLI 在设计时考虑了合理的默认值,但也具备在各个层级进行自定义的能力。CLI
具有三个属性,可以从默认实现更改为自定义实现。
Parser
遍历参数以找到相应的命令,更新其参数值,并识别选项。每个 CLI 都有一个 parser
属性,该属性具有三个可变属性:routeBehavior
、parseOptionsAfterCollectedParameter
和 responders
。
routeBehavior
具有三个可能的值。默认值 .search
会遍历用户传递的参数,并尝试查找具有匹配名称的命令。如果失败,则会打印帮助消息。git
是一个以这种方式运行的程序的示例。第二个选项 .searchWithFallback(Command)
也首先尝试查找具有与用户传递的参数匹配的名称的命令,但如果失败,它不会打印帮助消息,而是回退到某个特定命令。'bundle' 是一个以这种方式运行的程序的示例。最后一个选项是 .automatically(Command)
。在这种路由行为中,CLI 会自动路由到给定的命令,而不考虑参数。'ln' 是一个以这种方式运行的程序的示例。
parseOptionsAfterCollectedParameter
控制在遇到收集的参数后是否识别选项。它默认为 false
。给定以下命令
class Run: Command {
let name = "run"
@Param var executable: String
@CollectedParam var arguments: [String]
@Flag("-a") var all: Bool
...
}
如果用户调用 swift run myExec -a
且 parseOptionsAfterCollectedParameter
为 false,则 executable
的值为 "myExec",arguments
的值为 ["-a"]
,all
的值为 false。 如果 parseOptionsAfterCollectedParameter
在这种情况下为 true,则 executable
的值为 "myExec",arguments
的值为 []
,all
的值为 true。
responders
允许完全自定义解析器。有关其默认功能以及如何以任何方式进行自定义的更多信息,请查看 Parser.swift
。
可以通过 CLI 上的 aliases
属性创建别名。在路由到匹配的命令时,Parser
会考虑这些别名。例如,如果您编写
myCLI.aliases["-c"] = "command"
并且用户调用 myapp -c
,由于别名,解析器将搜索名称为 "command" 的命令,而不是名称为 "-c" 的命令。
默认情况下,"--version" 是 "version" 的别名,但如果需要,您可以删除它
myCLI.aliases["--version"] = nil
ArgumentListManipulator
s 在 Parser
开始之前执行。 它们接收用户给出的参数,并可以稍微更改它们。 默认情况下,唯一使用的参数列表操作器是 OptionSplitter
,它将诸如 -am
之类的选项拆分为 -a -m
。
您可以在您自己的类型上实现 ArgumentListManipulator
并更新 CLI 的属性
cli.argumentListManipulators.append(MyManipulator())
SwiftCLI 生成的消息也可以自定义
cli.helpMessageGenerator = MyHelpMessageGenerator()
只需调用 swift run
。 为了确保您的 CLI
获得命令行上传递的参数,请务必调用 CLI.go()
,而不是 CLI.go(with: [])
。