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。如果设置了它们,它们将按此顺序执行。
inheritablePreRunpreRunrunpostRuninheritablePostRun当命令即将执行时。它将首先搜索其父列表。如果它的任何父级具有 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 即可!我们不会咬人 ;)