Guaka
- 适用于 Swift 的智能且美观的 POSIX 兼容 CLI 框架。
它可以帮助您创建现代且熟悉的 CLI 应用程序,类似于 Docker、Kubernetes、OpenShift、Hugo 等广泛使用的项目。
Guaka 既是一个 swift 库,也是一个命令行应用程序,可以帮助生成 Guaka 项目。灵感来源于 Golang 生态系统中出色的 Cobra 包。
git remote show
计划功能
使用 Guaka,您可以构建由 命令 和 标志 组成的现代命令行应用程序。
每个命令代表一个操作,标志代表该命令上的开关或修饰符。此外,每个命令下都可以有一组子命令。
使用 Guaka,您可以构建具有如下界面的命令行应用程序
> git checkout "NAME Of Branch"
git
命令 CLI 具有一个 checkout
子命令,它接受一个字符串作为其参数。
> docker ps --all
docker
命令 CLI 具有 ps
子命令,它接受 --all
标志。
Guaka 还会自动为您的命令树生成命令行帮助。可以通过将 -h
或 --help
传递给任何命令来访问此帮助
> docker --help
> git checkout -h
帮助显示命令、子命令和标志信息。
Command
是 Guaka CLI 项目的主要对象。它代表一个动词,带有一个将被执行的代码块。
在 docker ps --all
示例中。我们有一个 docker
命令,它有一个 ps
命令作为其子命令。
└── docker
├── ps
└── ...
Command
类有很多自定义对象。至少,一个命令必须具有以下内容
let command = Command(usage: "command") { flags, args in
// the flags passed to the command
// args the positional arguments passed to the command
}
查看 Command 文档
Flag
代表 Command
接受的选项或开关。Guaka 支持短标志和长标志格式(与 POSIX 标志一致)。
在 docker ps --all
中。--all
是 ps
接受的标志。
标志有很多自定义对象。创建标志并将其添加到 ps 的最简单方法如下
let flag = Flag(longName: "all", value: false, description: "Show all the stuff")
let command = Command(usage: "ps", flags: [flag]) { flags, args in
flags.getBool(name: "all")
// args the positional arguments passed to the command
}
上面我们定义了一个 Flag
,其中 all
作为 longName,默认值为 false
。
要在命令中读取此标志,我们使用 flags.getBool(...)
,它返回标志值。
查看 Flag 文档
您可以使用 guaka
生成器应用程序或手动创建 swift 项目来创建 Guaka 命令行应用程序。
使用 guaka 的最简单方法是使用 guaka
生成器命令行应用程序。此 CLI 应用程序可帮助您生成 Guaka 项目。
首先,让我们使用 brew 安装 guaka
> brew install getGuaka/tap/guaka
作为替代方案,您可以使用安装脚本安装 guaka
(这适用于 macOS 和 Linux)
> curl https://raw.githubusercontent.com/getGuaka/guaka-cli/master/scripts/install.sh -sSf | bash
(注意:有关其他安装选项,请查看 Guaka Generator 自述文件。)
检查 guaka
是否已安装
> guaka --version
Version x.x.x
为了理解 guaka
生成器,假设我们要创建以下命令树
要创建新的 Guaka 项目,您可以运行 guaka create
。此命令创建一个新的 swift 项目以及拥有最小 Guaka 项目所需的 swift 项目文件。
guaka create
的行为根据传递给它的参数而有所不同
要创建我们上面描述的 git
命令,我们执行以下操作
> guaka create git
生成的 Guaka swift 项目结构将如下所示
├── Package.swift
└── Sources
├── main.swift
├── root.swift
└── setup.swift
让我们运行这个新创建的项目。
> swift build
生成的构建二进制文件将位于 ./.build/debug/git
下。
> ./.build/debug/git --help
它将打印出
Usage:
git
Use "git [command] --help" for more information about a command.
运行 guaka create
后,我们有了一个 Guaka 项目骨架。此项目将只有一个根命令。
您可以向项目添加新的子命令,您可以使用 guaka add ...
。
让我们添加 checkout 和 remote 命令。这两个命令都是根命令的子命令。
> guaka add checkout
> guaka add remote
接下来,让我们为 remote
添加一个子命令
> guaka add show --parent remote
生成的 Guaka swift 项目结构将如下所示
├── Package.swift
└── Sources
├── main.swift
├── root.swift
├── checkout.swift
├── remote.swift
├── show.swift
└── setup.swift
要添加标志,我们需要更改命令 swift 文件。要将标志添加到我们的示例 Command
(git remote --some-flag)。我们编辑 Sources/remote.swift
。
找到 command.add(flags: [])
函数调用并编辑它,使其如下所示
command.add(flags: [
Flag(longName: "some-name", value: false, description: "...")
]
)
现在保存文件并使用 swift build
构建它。运行构建的二进制文件 ./.build/debug/git -h
并检查创建的命令结构。
查看 添加标志文档
或者,您可以通过在 swift 项目中实现 Guaka
来创建 Guaka 命令行应用程序。
我们首先创建一个 swift 可执行项目
swift package init --type executable
将 Guaka
库添加到您的 Package.swift
文件
import PackageDescription
let package = Package(name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/nsomar/Guaka.git", majorVersion: 0),
]
)
运行 swift package fetch
以获取依赖项。
接下来,让我们添加我们的第一个命令。转到 main.swift
并键入以下内容
import Guaka
let command = Command(usage: "hello") { _, args in
print("You passed \(args) to your Guaka app!")
}
command.execute()
运行 swift build
以构建您的项目。恭喜!您已经创建了您的第一个 Guaka 应用程序。
要运行它,请执行
> ./.build/debug/{projectName} "Hello from cli"
您应该得到
You passed ["Hello from cli"] to your Guaka app!
查看 Command 文档
让我们继续添加标志。转到 main.swift
并将其更改为以下内容
import Guaka
let version = Flag(longName: "version", value: false, description: "Prints the version")
let command = Command(usage: "hello", flags: [version]) { flags, args in
if let hasVersion = flags.getBool(name: "version"),
hasVersion == true {
print("Version is 1.0.0")
return
}
print("You passed \(args) to your Guaka app!")
}
command.execute()
上面添加了一个名为 version
的标志。请注意我们是如何使用 flags.getBool
获取标志的。
现在让我们通过构建和运行命令来测试它
> swift build
> ./.build/debug/{projectName} --version
Version is 1.0.0
查看 添加标志文档
要添加子命令,我们更改 main.swift
。在调用 command.execute()
之前添加以下内容
// Create the command
...
let subCommand = Command(usage: "sub-command") { _, _ in
print("Inside subcommand")
}
command.add(subCommand: subCommand)
command.execute()
现在构建并运行命令
> swift build
> ./.build/debug/{projectName} sub-command
Inside subcommand
查看 [添加子命令](查看 添加标志文档)
Guaka 自动为您的命令生成帮助。我们可以通过运行以下命令获取帮助
> ./.build/debug/{projectName} --help
Usage:
hello [flags]
hello [command]
Available Commands:
sub-command
Flags:
--version Prints the version
Use "hello [command] --help" for more information about a command.
请注意如何显示命令、子命令和标志信息。
阅读有关 帮助消息的更多信息
编写命令行应用程序不仅仅是解析命令行参数和标志。
Swift 生态系统仍然非常年轻,并且缺乏跨平台标准库。我们不想让 Guaka 依赖于 libFoundation,因此我们卷起袖子构建了一些小型跨平台(只要有可用的 C 标准库即可)库。这样您就不必这样做,并且可以立即高效工作。此外,它们可以单独使用。欢迎您也使用它们! <3
Command
代表 Guaka 中的主要类。它封装了 Guaka 定义的命令或子命令。
有关完整的 Command 文档
至少,命令需要一个用法字符串和一个 Run
代码块。用法字符串描述了如何使用此命令。
command-name
;则命令将具有该名称command-name args..
;则命令名称是字符串的第一个片段。let c = Command(usage: "command-name") { _, args in
}
Run
代码块被调用时带有两个参数。Flags
类,其中包含传递给命令的标志,以及 args
,它是传递给命令的参数数组。
Command
构造函数接受许多参数。但是,其中大多数都具有合理的默认值。随意填写您想要的参数,无论多少。
Command(usage: "...",
shortMessage: "...",
longMessage: "...",
flags: [],
example: "...",
parent: nil,
aliases: [],
deprecationStatus: .notDeprecated,
run: {..})
至少,您需要传递 usage
和 run
代码块。有关参数的信息,请参阅代码文档。
查看 Flags 文档
命令以树形结构组织。每个命令可以具有零个、一个或多个与其关联的子命令。
我们可以通过调用 command.add(subCommand: theSubCommand)
来添加子命令。如果我们想将 printCommand
添加为 rootCommand
的子命令,我们将执行以下操作
let rootCommand = //Create the root command
let printCommand = //Create the print command
rootCommand.add(subCommand: printCommand)
或者,您可以在创建 printCommand
时将 rootCommand
作为 parent
传递
let rootCommand = //Create the root command
let printCommand = Command(usage: "print",
parent: rootCommand) { _, _ in
}
我们的命令行应用程序现在将响应以下两者
> mainCommand
> mainCommand print
您可以以这种方式构建命令树,并创建现代、复杂、优雅的命令行应用程序。
Command
定义了 shortMessage
和 longMessage
。这些是在显示 Command
帮助时显示的两个字符串。
Command(usage: "print",
shortMessage: "prints a string",
longMessage: "This is the long mesage for the print command") { _, _ in
}
当命令是子命令时,显示 shortMessage
。
> mainCommand -h
Usage:
mainCommand [flags]
mainCommand [command]
Available Commands:
print prints a string
Use "mainCommand [command] --help" for more information about a command.
Program ended with exit code: 0
当获取当前命令的帮助时,显示 longMessage
> mainCommand print -h
This is the long message for the print command
Usage:
mainCommand print
Use "mainCommand print [command] --help" for more information about a command.
Program ended with exit code: 0
您可以通过两种方式向命令添加 Flag
。
您可以在构造函数中传递标志
let f = Flag(longName: "some-flag", value: "value", description: "flag information")
let otherCommand = Command(usage: "print",
shortMessage: "prints a string",
longMessage: "This is the long mesage for the print command",
flags: [f]) { _, _ in
}
或者,您可以调用 command.add(flag: yourFlag)
。
现在,该标志将与命令关联。如果我们显示命令的帮助,我们可以看到它。
> mainCommand print -h
This is the long message for the print command
Usage:
mainCommand print [flags]
Flags:
--some-flag string flag information (default value)
Use "mainCommand print [command] --help" for more information about a command.
您可以附加一个关于如何使用命令的文本示例。您可以通过设置 Command
中的 example
变量(或通过填写构造函数中的 example
参数)来执行此操作
printCommand.example = "Use it like this `mainCommand print \"the string to print\""
然后我们可以在命令帮助中看到它
> mainCommand print -h
This is the long message for the print command
Usage:
mainCommand print
Examples:
Use it like this `mainCommand print "the string to print"
Use "mainCommand print [command] --help" for more information about a command.
您可以通过在命令上设置 deprecationStatus
将命令标记为已弃用。
printCommand.deprecationStatus = .deprecated("Dont use it")
当用户调用此命令时,将显示弃用消息。
别名有助于为命令提供替代名称。我们可以让 print
和 echo
都代表同一个命令
printCommand.aliases = ["echo"]
命令可以有不同的 run Hook。如果设置了它们,它们将按此顺序执行。
inheritablePreRun
preRun
run
postRun
inheritablePostRun
当命令即将执行时。它将首先搜索其父列表。如果它的任何父级具有 inheritablePreRun
,则 Guaka 将首先执行该代码块。
接下来,执行当前命令的 preRun
。然后是 run
和 postRun
。
之后,与 inheritablePreRun
一样,Guaka 将搜索任何具有 inheritablePostRun
的父级并也执行它。
所有 inheritablePreRun
、preRun
、postRun
和 inheritablePostRun
代码块都返回一个布尔值。如果它们返回 false
,则命令执行将结束。
这允许您创建智能命令树,其中命令的父级可以决定其任何子命令是否必须继续执行。
例如。父命令可以定义一个 version 标志。如果设置了此标志,则父级将处理调用并从其 inheritablePreRun
返回 false。这样做有助于我们避免在每个子命令中重复版本处理。
下面的示例显示了此用例
// Create root command
let rootCommand = Command(usage: "main") { _, _ in
print("main called")
}
// Create sub command
let subCommand = Command(usage: "sub", parent: rootCommand) { _, _ in
print("sub command called")
}
// Add version flag to the root
// We made the version flag inheritable
// print will also have this flag as part of its flags
let version = Flag(longName: "version", value: false,
description: "Prints the version", inheritable: true)
rootCommand.add(flag: version)
rootCommand.inheritablePreRun = { flags, args in
if
let version = flags.getBool(name: "version"),
version == true {
print("Version is 0.0.1")
return false
}
return true
}
rootCommand.execute()
现在我们可以通过调用以下命令获取版本
> main --version
> main sub --version
在某些情况下,您可能想要从命令中提前退出,您可以使用 command.fail(statusCode: errorCode, errorMessage: "Error message")
let printCommand = Command(usage: "print",
parent: rootCommand) { _, _ in
// Error happened
printCommand.fail(statusCode: 1, errorMessage: "Some error happaned")
}
Flag
代表 Command
接受的选项或开关。Guaka 定义了 4 种类型的标志;整数、布尔值、字符串和自定义类型。
查看完整的 Flag 文档
要创建具有默认值的 Flag
,我们调用执行以下操作
let f = Flag(longName: "version", value: false, description: "prints the version")
我们创建了一个 longName
为 version
的标志。默认值为 false
并具有描述。这将创建一个 POSIX 兼容标志。要设置此标志
> myCommand --version
> myCommand --version=true
> myCommand --version true
Flag
是一个泛型类,在前面的示例中,由于我们将 false
设置为其值,因此创建了一个 boolean
Flag
。如果您尝试在终端中传递非布尔参数,Guaka 将显示错误消息。
与命令一样,标志构造函数定义了许多参数。其中大多数都具有合理的默认值,因此请随意传递您需要的数量,无论多少。
例如,我们可以通过执行以下操作来设置标志短名称
Flag(shortName: "v", longName: "version", value: false, description: "prints the version")
现在,我们在调用命令时可以使用 -v
或 --version
。
我们可以创建一个没有默认值的标志。这种类型的标志可以标记为可选或必需。
要创建可选标志
Flag(longName: "age", type: Int.self, description: "the color")
这里我们定义了一个具有 int 值的标志。如果我们使用非整数值执行命令,Guaka 将通知我们错误。
可以通过将 true 传递给 Flag
构造函数中的 required
参数来创建必需的标志
Flag(longName: "age", type: Int.self, description: "the color", required: true)
现在,如果我们调用命令而不设置 --age=VALUE
。Guaka 将显示错误。
当 Command
run
代码块被调用时,将向该代码块发送一个 Flags
参数。此 Flags
参数包含命令定义的每个标志的值。
此示例说明了标志读取
// Create the flag
var uppercase = Flag(shortName: "u", longName: "upper",
value: false, description: "print in bold")
// Create the command
let printCommand = Command(usage: "print", parent: rootCommand) { flags, args in
// Read the flag
let isUppercase = flags.getBool(name: "upper") ?? false
if isUppercase {
print(args.joined().uppercased())
} else {
print(args.joined())
}
}
// Add the flag
printCommand.add(flag: uppercase)
让我们执行此命令
> print "Hello World"
Hello World
> print -u "Hello World"
HELLO WORLD
Flags
类定义了读取所有不同类型标志的方法
func getBool(name: String) -> Bool?
func getInt(name: String) -> Int?
func getString(name: String) -> String?
func get<T: FlagValue>(name: String, type: T.Type) -> T?
查看完整的 Flags 文档
通过将 true
传递给标志构造函数中的 inheritable
参数,设置给父 Command
的标志也可以继承给子命令。
要创建可继承标志
var version = Flag(longName: "version", value: false,
description: "print in bold", inheritable: true)
rootCommand.add(flag: version)
这使得 --version
成为可以在 rootCommand
及其任何子命令中设置的标志。
与 Command
一样,可以通过设置 deprecationStatus
将 Flag
设置为已弃用
var version = Flag(longName: "version", value: false,
description: "print in bold", inheritable: true)
version.deprecationStatus = .deprecated("Dont use this flag")
每次设置此标志时,Guaka 都会发出警告。
开箱即用,您可以创建具有整数、布尔值和字符串值和类型的标志。但是,如果您想为标志定义自定义类型,可以通过实现 FlagValue
协议来完成。
让我们定义一个具有 User
类型的标志
// Create the enum
enum Language: FlagValue {
case english, arabic, french, italian
// Try to convert a string to a Language
static func fromString(flagValue value: String) throws -> Language {
switch value {
case "english":
return .english
case "arabic":
return .arabic
case "french":
return .french
case "italian":
return .italian
default:
// Wrong parameter passed. Throw an error
throw FlagValueError.conversionError("Wrong language passed")
}
}
static var typeDescription: String {
return "the language to use"
}
}
// Create the flag
var lang = Flag(longName: "lang", type: Language.self, description: "print in bold")
// Create the command
let printCommand = Command(usage: "print", parent: rootCommand) { flags, args in
// Read the flag
let lang = flags.get(name: "lang", type: Language.self)
// Do something with it
}
// Add the flag
printCommand.add(flag: lang)
// Execute the command
printCommand.execute()
请注意,如果参数不正确,我们会抛出 FlagValueError.conversionError
。此错误将打印到控制台。
> print --lang undefined "Hello"
Error: wrong flag value passed for flag: 'lang' Wrong language passed
Usage:
main print [flags]
Flags:
--lang the language to use print in bold
Use "main print [command] --help" for more information about a command.
wrong flag value passed for flag: 'lang' Wrong language passed
exit status 255
查看完整的 FlagValue 文档 和 FlagValueError 文档。
Guaka 允许您自定义生成的帮助的格式。您可以通过实现 HelpGenerator
并将您的类传递给 GuakaConfig.helpGenerator
来完成此操作。
HelpGenerator
协议定义了您可以子类化的帮助消息的所有部分。HelpGenerator
为所有部分提供了带有默认值的协议扩展。这允许您选择要更改的帮助部分。
HelpGenerator
中的每个变量和部分都对应于打印的帮助消息中的一个部分。要获取每个部分的文档,请参阅 HelpGenerator
的代码内文档。
假设我们只想更改帮助的 usageSection
,我们将执行以下操作
struct CustomHelp: HelpGenerator {
let commandHelp: CommandHelp
init(commandHelp: CommandHelp) {
self.commandHelp = commandHelp
}
var usageSection: String? {
return "This is the usage section of \(commandHelp.name) command"
}
}
GuakaConfig.helpGenerator = CustomHelp.self
任何 HelpGenerator
子类都将具有一个 commandHelp
变量,它是 CommandHelp
结构的实例。此结构包含命令的所有可用信息。
查看完整的 HelpGenerator 文档
测试可以在 这里 找到。
使用以下命令运行它们
swift test
有关计划任务的列表,请访问 Guaka GitHub 项目
只需发送 PR 即可!我们不会咬人 ;)