SwiftPM compatible Swift Version Build Status codecov Platform License MIT Analytics

Guaka - 适用于 Swift 的智能且美观的 POSIX 兼容 CLI 框架。
它可以帮助您创建现代且熟悉的 CLI 应用程序,类似于 Docker、Kubernetes、OpenShift、Hugo 等广泛使用的项目。

Guaka 既是一个 swift 库,也是一个命令行应用程序,可以帮助生成 Guaka 项目。灵感来源于 Golang 生态系统中出色的 Cobra 包

它好用吗?

是的

为什么?


在本自述文件中

功能特性

计划功能

简介

使用 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 中。--allps 接受的标志。

标志有很多自定义对象。创建标志并将其添加到 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 的最简单方法是使用 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 create

要创建新的 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 add

运行 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 并检查创建的命令结构。

查看 添加标志文档

手动实现 Guaka

或者,您可以通过在 swift 项目中实现 Guaka 来创建 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 代码块

至少,命令需要一个用法字符串和一个 Run 代码块。用法字符串描述了如何使用此命令。

let c = Command(usage: "command-name") { _, args in
}

Run 代码块被调用时带有两个参数。Flags 类,其中包含传递给命令的标志,以及 args,它是传递给命令的参数数组。

Command 构造函数接受许多参数。但是,其中大多数都具有合理的默认值。随意填写您想要的参数,无论多少。

Command(usage: "...",
        shortMessage: "...",
        longMessage: "...",
        flags: [],
        example: "...",
        parent: nil,
        aliases: [],
        deprecationStatus: .notDeprecated,
        run: {..})

至少,您需要传递 usagerun 代码块。有关参数的信息,请参阅代码文档。

查看 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 定义了 shortMessagelongMessage。这些是在显示 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")

当用户调用此命令时,将显示弃用消息。

别名有助于为命令提供替代名称。我们可以让 printecho 都代表同一个命令

printCommand.aliases = ["echo"]

不同类型的 Run Hook

命令可以有不同的 run Hook。如果设置了它们,它们将按此顺序执行。

当命令即将执行时。它将首先搜索其父列表。如果它的任何父级具有 inheritablePreRun,则 Guaka 将首先执行该代码块。

接下来,执行当前命令的 preRun。然后是 runpostRun

之后,与 inheritablePreRun 一样,Guaka 将搜索任何具有 inheritablePostRun 的父级并也执行它。

所有 inheritablePreRunpreRunpostRuninheritablePostRun 代码块都返回一个布尔值。如果它们返回 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")

我们创建了一个 longNameversion 的标志。默认值为 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 类定义了读取所有不同类型标志的方法

查看完整的 Flags 文档

可继承标志

通过将 true 传递给标志构造函数中的 inheritable 参数,设置给父 Command 的标志也可以继承给子命令。

要创建可继承标志

var version = Flag(longName: "version", value: false,
                   description: "print in bold", inheritable: true)

rootCommand.add(flag: version)

这使得 --version 成为可以在 rootCommand 及其任何子命令中设置的标志。

标志弃用

Command 一样,可以通过设置 deprecationStatusFlag 设置为已弃用

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 即可!我们不会咬人 ;)