SwiftCLI
一款强大框架,可用于使用 Swift 开发从最简单到最复杂的 CLI。
import SwiftCLI
class GreetCommand: Command {
let name = "greet"
let person = Parameter()
func execute() throws {
stdout <<< "Hello \(person.value)!"
}
}
let greeter = CLI(name: "greeter")
greeter.commands = [GreetCommand()]
greeter.go()
~ > greeter greet world
Hello world!
使用 SwiftCLI,您将免费获得
- 命令路由
- 选项解析
- 帮助消息
- 用法语句
- 当命令使用不正确时的错误消息
- Zsh 补全
目录
安装
Ice 包管理器
> ice add jakeheis/SwiftCLI
Swift Package Manager
将SwiftCLI作为依赖项添加到项目中
dependencies: [
.package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.0.0")
]
更新到 SwiftCLI 5.0
请参阅迁移信息。
创建一个 CLI
创建一个 CLI
时,需要一个 名称
,一个 版本
和 描述
都是可选的。
let myCli = CLI(name: "greeter", version: "1.0.0", description: "Greeter - your own personal greeter")
您通过 .commands
属性设置命令
myCli.commands = [myCommand, myOtherCommand]
最后,要实际启动 CLI,您可以调用 go
方法之一。在生产应用程序中,应使用 go()
或 goAndExit()
。这些方法使用在启动时传递给您的 CLI 的参数。
// Use go if you want program execution to continue afterwards
myCli.go()
// Use goAndExit if you want your program to terminate after CLI has finished
myCli.goAndExit()
当您创建和调试应用程序时,可以使用 debugGo(with:)
,这使得在开发期间传递参数字符串到您的应用程序变得更加容易。
myCli.debugGo(with: "greeter greet")
命令
为了创建一个命令,您必须实现 Command
协议。所需做的只是实现一个 name
属性和一个 execute
函数;Command
的其他属性都是可选的(尽管强烈建议提供 shortDescription
)。一个简单的问候世界命令可以创建如下
class GreetCommand: Command {
let name = "greet"
let shortDescription = "Says hello to the world"
func execute() throws {
stdout <<< "Hello world!"
}
}
参数
一个命令可以通过某些实例变量指定它接受的参数。使用反射,SwiftCLI 将识别类型为 Parameter
、OptionalParameter
、CollectedParameter
和 OptionalCollectedParameter
的实例变量。这些实例变量应按照命令预期用户传递参数的顺序出现
class GreetCommand: Command {
let name = "greet"
let firstParam = Parameter()
let secondParam = Parameter()
}
在这个例子中,如果用户运行greeter greet Jack Jill
,firstParam
将更新为值为Jack
,secondParam
将更新为值为Jill
。这些参数的值可以在func execute()
中通过调用firstParam.value
等方式访问。
必需参数
必需参数的形式为类型Parameter
。如果命令没有传递足够多的参数来满足所有必需参数,则命令将失败。
class GreetCommand: Command {
let name = "greet"
let person = Parameter()
let greeting = Parameter()
func execute() throws {
stdout <<< "\(greeting.value), \(person.value)!"
}
}
~ > greeter greet Jack
Expected 2 arguments, but got 1.
~ > greeter greet Jack Hello
Hello, Jack!
可选参数
可选参数的形式为类型OptionalParameter
。可选参数必须跟在所有必需参数之后。如果用户没有传递足够多的参数来满足所有可选参数,则这些未满足的参数的.value
将被设置为nil
。
class GreetCommand: Command {
let name = "greet"
let person = Parameter()
let greeting = OptionalParameter()
func execute() throws {
let greet = greeting.value ?? "Hey there"
stdout <<< "\(greet), \(person.value)!"
}
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack Hello
Hello, Jack!
收集参数
命令可能有一个收集参数,即CollectedParameter
或OptionalCollectedParameter
。这些参数允许用户传递任何数量的参数,并将这些参数收集到收集参数的value
数组中。
class GreetCommand: Command {
let name = "greet"
let people = CollectedParameter()
func execute() throws {
let peopleString = people.value.joined(separator: ", ")
stdout <<< "Hey there, \(peopleString)!"
}
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack Jill
Hey there, Jack, Jill!
~ > greeter greet Jack Jill Hill
Hey there, Jack, Jill, Hill!
选项
命令支持两种类型的选项:标志选项和键控选项。这两种类型的选项可以用一个短横线后跟一个字母来表示(例如git commit -a
),或者用两个短横线后跟选项名称来表示(例如git commit --all
)。单个字母选项可以连续使用短横线和所有所需的选项:git commit -am "message"
等于 git commit -a -m "message"
。
选项作为命令类的实例变量指定,就像参数一样。
class ExampleCommand: Command {
...
let flag = Flag("-a", "--all")
let key = Key<Int>("-t", "--times")
...
}
标志选项
标志选项是作为布尔开关的简单选项。例如,如果您实现git commit
,-a
就是一个标志选项。它们的形式是Flag
类型的变量。
可以将GreetCommand
修改为接受“大声”标志
class GreetCommand: Command {
...
let loudly = Flag("-l", "--loudly", description: "Say the greeting loudly")
func execute() throws {
if loudly.value {
...
} else {
...
}
}
}
键控选项
键控选项是有相关值的选项。以“git commit”为例,“-m”就是键控选项,因为它有一个相关值 - 提交信息。它们的形式是泛型类型Key
的变量,其中T
是选项的类型。
可以将GreetCommand
修改为接受“重复次数”选项
class GreetCommand: Command {
...
let numberOfTimes = Key<Int>("-n", "--number-of-times", description: "Say the greeting a certain number of times")
func execute() throws {
for i in 0..<(numberOfTimes.value ?? 1) {
...
}
}
}
相关选项类型是VariadicKey
,它允许用户以不同的值多次传递相同的键。例如,使用类似以下的关键字声明
class GreetCommand: Command {
...
let locations = VariadicKey<String>("-l", "--location", description: "Say the greeting in a certain location")
...
}
用户可以编写greeter greet -l Chicago -l NYC
,然后locations.value
将被设置为["Chicago", "NYC"]
。
选项组
可以通过选项组指定多个选项之间的关系。选项组允许命令指定用户必须传递一个组中的最多一个选项(传递多个是错误),必须传递一个组中的正好一个选项(传递零个或多个是错误),或者必须传递一个组中的一个或多个选项(传递零个是错误)。
要添加选项组,Command
应该实现属性optionGroups
。例如,如果GreetCommand
有一个loudly
标志和一个whisper
标志,但不希望用户能传递两者之一,可以使用OptionGroup
class GreetCommand: Command {
...
let loudly = Flag("-l", "--loudly", description: "Say the greeting loudly")
let whisper = Flag("-w", "--whisper", description: "Whisper the greeting")
var optionGroups: [OptionGroup] {
let volume: OptionGroup = .atMostOne(loudly, whipser)
return [volume]
}
func execute() throws {
if loudly.value {
...
} else {
...
}
}
}
全局选项
全局选项可以用来指定每个命令都应该有一个特定的选项。这是所有命令实现-h
标志的方式。只需将选项添加到CLI的.globalOptions
数组中(并可选择扩展Command
以使该选项容易在您的命令中访问)
private let verboseFlag = Flag("-v")
extension Command {
var verbose: Flag {
return verboseFlag
}
}
myCli.globalOptions.append(verboseFlag)
有了这个,现在每个命令都有一个verbose
标志。
默认情况下,每个命令都将有一个打印帮助信息的-h
标志。您可以将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
Shell补全
Zsh补全可以自动为CLI生成(bash补全即将推出)。
let myCli = CLI(...)
let generator = ZshCompletionGenerator(cli: myCli)
generator.writeCompletions()
会自动生成命令名称和选项的补全。可以指定参数补全模式
let noCompletions = Parameter(completion: .none)
let aFile = Parameter(completion: .filename)
let aValue = Parameter(completion: .values([
("optionA", "the first available option"),
("optionB", "the second available option")
]))
let aFunction = Parameter(completion: .function("_my_custom_func"))
默认参数补全模式是.filename
。如果您使用.function
指定了一个自定义函数,则在创建补全生成器时必须提供该函数
class MyCommand {
...
let pids = Parameter(completion: .function("_list_processes"))
...
}
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
。
帮助命令
HelpCommand
可以使用myapp help
或myapp -h
来调用。首先,HelpCommand
会打印应用描述(如果在CLI.setup()
期间提供了)。然后,它会遍历所有可用的命令,打印它们的名称和简短描述。
~ > 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 -v
可以调用 VersionCommand
。版本命令打印在初始化期间设置的 app 版本 CLI(name:version:)
。如果没有给出版本,则此命令不可用。
~ > greeter -v
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
:在接收输入之前打印的消息(例如:“输入:”)secure
:如果为 true,则输入在用户键入时被隐藏validation
:一个闭包,定义输入是否有效,或用户是否需要重新提示errorResponse
:一个闭包,在用户输入无效时执行
例如,您可以编写
let percentage = Input.readDouble(
prompt: "Percentage:",
validation: { $0 >= 0 && $0 <= 100 },
errorResponse: { (input) in
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 run("echo", "hello")
try run(bash: "while true; do echo hi && sleep 1; done")
// Execute a command and capture the output:
let currentDirectory = try capture("pwd").stdout
let sorted = try 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
请参考 Sources/SwiftCLI/Task.swift
获取关于 Task
的完整文档。
单命令 CLI
如果您的 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》有三个可以从默认实现更改为自定义实现的属性。
解析器
解析器
会遍历参数以找到相应的命令、更新其参数值并识别选项。解析器
有两个阶段,首先是受其路由器
驱动,其次是受其参数填充器
驱动。SwiftCLI为这两个阶段提供了默认实现,分别是`DefaultRouter`和`DefaultParameterFiller`。DefaultRouter
根据传递的第一个参数(或者,在命令组的案例中,前几个参数)来找到命令,而DefaultParameterFiller
则使用剩下的参数(不是以短横线开头的参数)来满足命令的参数。
SwiftCLI还提供了一个名为`SingleCommandRouter`的`Router`实现,当您使用`CLI(singleCommand: myCmd)`创建您的CLI时将自动使用。例如,如果您正在实现`ln`命令,您可以手动编写`myCLI.parser = DefaultParser(router: SingleCommandRouter(command: LinkCommand()))`。然后,此路由器将始终返回相同的命令并将所有参数留给了`ParameterFiller`。如果用户写入`cli my.txt`,那么`DefaultRouter`将查找名为`my.txt`的具有无参数的命令,而`SingleCommandRouter`将把'my.txt'视为单个命令的参数。
您可以在自己的类型上实现`Router`或`ParameterFiller`,并将您的CLI的属性更新为使用它们。
myCLI.parser = Parser(router: MyRouter(), parameterFiller: MyParameterFiller())
别名
可以通过CLI上的`aliases`属性来创建别名。DefaultRouter
会在路由时考虑这些别名,以找到匹配的命令。例如,如果您写入:
myCLI.aliases["-c"] = "command"
然后用户调用myapp -c
时,解析器将基于别名查找名为"command"的命令,而不是名为"-c"的命令。
默认情况下,"-h"被别名为"help","-v"被别名为"version",但您可以移除这些名称,如果不需要这些别名。
myCLI.aliases["-h"] = nil
argumentListManipulators
ArgumentListManipulator
在 Parser
开始前进行操作。它们接收用户提供的参数并可以对其进行轻微修改。默认情况下,仅使用 OptionSplitter
作为参数列表操作器,它将像 -am
这样的选项拆分为 -a -m
。
您可以为自己定义的 ArgumentListManipulator
实现和更新 CLI 的属性
public var argumentListManipulators: [ArgumentListManipulator] = [OptionSplitter()]
helpMessageGenerator
SwiftCLI 生成的消息也可以进行自定义
public var helpMessageGenerator: HelpMessageGenerator = DefaultHelpMessageGenerator()
运行您的 CLI
只需调用 swift run
即可。要确保您的 CLI
获得命令行上的参数,请确保调用 CLI.go()
,而不是 CLI.debugGo(with: "")
。
示例
使用 SwiftCLI 开发的 CLI 示例可在 https://github.com/jakeheis/Ice 找到。