CLIKit


已弃用: 请不要再使用此软件包。请改用 Swift 的参数解析器。


SwiftPM compatible MIT License language Swift 5.1 platform macOS platform Linux Build Status

CLIKit 框架包含各种方便的实用工具,可以更轻松地用 Swift 编写命令行工具。

许可证

CLIKit 在 MIT 许可证下发布。有关更多详细信息,请参见 LICENSE 文件。

目录

开始使用

通过将以下内容添加到 Package.swift 文件中的 dependencies 数组,将 CLIKit 添加到你的 Swift 包

.package(url: "https://github.com/apparata/CLIKit.git", from: "<version>")

如果你使用的是 Xcode 11 或更高版本,可以通过 File 菜单输入存储库的 URL 来添加 CLIKit

File > Swift Packages > Add Package Dependency...

注意: CLIKit 需要 Swift 5.1 或更高版本。

参考文档

有生成的 参考文档 可用。

功能

以下各节包含一些关于 CLIKit 中最突出功能的初步信息以及示例。

命令行解析器

命令定义的示例

class FibonacciCommand: Command {
    
    let description = "Calculate fibonacci numbers"

    @CommandFlag(short: "v", description: "Prints verbose output")
    var verbose: Bool
    
    @CommandOption(short: "i", default: 5, regex: #"^\d+$"#,
                   description: "Number of iterations to perform.")
    var iterations: Int
    
    func run() {
        let result = fibonacci(iterations, printSteps: verbose)
        print("Result: \(result)")
    }
}

在可执行参数上运行解析器,然后在命令解析后运行命令处理程序的示例

let command = try CommandLineParser().parse(FibonacciCommand())
try command.run()

如果二进制文件名为 fibonacci,则可以在 shell 中像这样运行该命令

$ fibonacci -i 4

可以将多个命令组合为子命令

class MathCommand: Commands {
    
    let description = "Perform math operations"
    
    let fibonacci = FibonacciCommand()
    let factorize = FactorizeCommand()
    let sum = SumCommand()
}

在可执行参数上运行解析器,然后在命令解析后运行命令处理程序的示例

let command = try CommandLineParser().parse(MathCommand())
try command.run()

如果二进制文件名为 math,则可以像这样在 shell 中运行 fibonacci 子命令

$ math fibonacci -i 4

有几种不同类型的参数

@CommandFlag(short: "v", description: "Prints verbose output")
var verbose: Bool

@CommandOption(short: "i", default: 5, regex: #"^\d+$"#,
               description: "Number of iterations to perform.")
var iterations: Int

@CommandRequiredInput(description: "First number")
var numberA: Int

@CommandOptionalInput(description: "Number to factorize")
var number: Int?

@CommandVariadicInput(description: "More numbers")
var numbers: [Int]

子进程

启动子进程并捕获其输出的示例

import CLIKit

// Search for Swift using PATH environment variable.
guard let path = ExecutableFinder.find("swift") else {
    print("Didn't find swift, exiting.")
    exit(1)
}

do {
    // Launch Swift as a subprocess and capture its output.
    let subprocess = Subprocess(executable: path,
                                arguments: ["-h"],
                                captureOutput: true)
    try subprocess.spawn()

    // Wait for the process to finish.
    let result = try subprocess.wait()

    // Print the captured output from the subprocess.
    print(try result.capturedOutputString())
} catch {
    dump(error)    
}

终端输出

使用 TerminalString 结构打印带有 ANSI 终端代码的字符串的示例

Console.print("\(.green)This is green.\(.reset)\(.bold)This is bold.\(.reset)")

如果控制台是“哑”终端或 Xcode 控制台,则将过滤掉 ANSI 终端代码。

Console 类具有一些用于控制台输入和输出的便捷方法

if Console.confirmYesOrNo(question: "Clear the screen?", default: false) {
    // Clear the screen.
    Console.clear()
} else {
    // Do not clear the screen.
}

执行

命令行程序通常在主线程上没有更多代码要运行时结束。要执行异步工作,例如网络请求或在调度队列上运行代码,需要启动运行循环。可以使用 Execution 类的 runUntilTerminated() 方法启动一个运行循环,该循环将一直运行,直到程序被终止,无论是通过编程方式使用 exit() 或类似方法,还是被系统显式终止,例如,如果用户按下 Ctrl-C。

示例

Execution.runUntilTerminated()

有一个可选的闭包参数,用于处理程序终止时任何必要的清理。如果进程收到 SIGINT(通常是用户按下 Ctrl-C 时)、SIGHUP(终端断开连接)或 SIGTERM(终止),则调用该闭包。

示例

Execution.runUntilTerminated { signal in 

    switch signal {
    case .terminate:
        // Do any necessary cleanup here.
        ...
        
        // Return true to allow the system to handle the SIGTERM signal.
        return true
        
    case .interrupt:
        // Do any necessary cleanup here.
        ...

        // Return false to suppress the SIGINT signal.
        // This will not allow Ctrl-C to terminate the program.
        return false
    }
    
    case .terminalDisconnected:
        // Do any necessary cleanup here.
        ...

        // Return false to suppress the SIGHUP signal.
        // This will allow the process to run without the terminal.
        return false
    }
}

读取-求值-输出循环

ReadEvaluatePrintLoop 类具有内置的命令行编辑器,支持各种常见的键盘快捷键、可自定义的制表符补全、命令行历史记录和多行支持。如果终端是“哑”终端或附加了调试器(例如,如果你想在 Xcode 控制台中运行),它会回退到仅从 stdin 读取缓冲的行。

示例

let readEvaluatePrintLoop = try ReadEvaluatePrintLoop()

readEvaluatePrintLoop.textCompletion = SimpleWordCompletion(completions: [
    "banana",
    "discombobulated",
    "water",
    "whatever"
])

try readEvaluatePrintLoop.run { input in
    guard !["quit", "exit"].contains(input) else {
        return .break
    }
    
    Console.write(terminalString: "You entered: \(input)\n")
    return .continue
}

路径管理

CLIKit 包含一个 Path 结构,可以更轻松地处理文件系统路径。

示例

let absolutePath = Path("/usr/bin/zip")
absolutePath.isAbsolute
absolutePath.isRelative

let relativePath = Path("bin/whatever")
relativePath.isAbsolute
relativePath.isRelative

let concatenatedPath = Path("/usr") + Path("/bin")

let messyPath = Path("//usr/../usr/local/bin/./whatever")
messyPath.normalized

let pathFromLiteralString: Path = "/this/is/a/path"
let pathFromEmptyString: Path = ""
let pathFromConcatenatedStrings: Path = "/usr" + "/bin"

let pathFromComponents = Path(components: ["/", "usr/", "bin", "/", "swift"])
let pathFromEmptyComponents = Path(components: [])

let appendedPath = Path("/usr/local").appendingComponent("bin")
let appendedPath3 = Path("/usr/local").appending(Path("bin"))
let appendedPath2 = Path("/usr/local") + Path("bin")

let imagePath = Path("photos/photo").appendingExtension("jpg")
imagePath.extension

let imagePathWithoutExtension = imagePath.deletingExtension
let imagePathWithoutLastComponent = imagePath.deletingLastComponent

absolutePath.exists
absolutePath.isFile
absolutePath.isDirectory
absolutePath.isDeletable
absolutePath.isExecutable
absolutePath.isReadable
absolutePath.isWritable

// Return an array of Path objects representing files in the current directory.
let filesInDirectory = try Path.currentDirectory.contentsOfDirectory

// Change directory to the user's home directory
Path.homeDirectory?.becomeCurrentDirectory()

if let desktop = Path.desktopDirectory {
    Path("/path/myfile.txt").copy(to: desktop)
}

desktop.appendingComponent("myfile.txt").remove()

try (desktop + "My Folder").createDirectory()

Path("/path/myscript.sh").setPosixPermissions(0o700)