SwiftCLI 5.2.1

SwiftCLI 5.2.1

Jake Heiser 维护。



SwiftCLI 5.2.1

  • Jake Heiser

SwiftCLI

Build Status

一款强大框架,可用于使用 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 将识别类型为 ParameterOptionalParameterCollectedParameterOptionalCollectedParameter 的实例变量。这些实例变量应按照命令预期用户传递参数的顺序出现

class GreetCommand: Command {
    let name = "greet"
    let firstParam = Parameter()
    let secondParam = Parameter()
}

在这个例子中,如果用户运行greeter greet Jack JillfirstParam将更新为值为JacksecondParam将更新为值为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!

收集参数

命令可能有一个收集参数,即CollectedParameterOptionalCollectedParameter。这些参数允许用户传递任何数量的参数,并将这些参数收集到收集参数的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有两个内置命令:HelpCommandVersionCommand

帮助命令

HelpCommand可以使用myapp helpmyapp -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 versionmyapp -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

ArgumentListManipulatorParser 开始前进行操作。它们接收用户提供的参数并可以对其进行轻微修改。默认情况下,仅使用 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 找到。