⛷ Bariloche

Bariloche 是一个参数解析库,旨在用 Swift 构建声明式、多彩且类型安全的命令行界面。支持高级 zsh 自动补全。

安装

Swift Package Manager

将 Bariloche 添加为项目的依赖项

dependencies: [
    .package(url: "https://github.com/Subito-it/Bariloche", from: "1.0.0")
]

运行示例代码

在 Xcode 中运行 Bariloche 演示

  git clone https://github.com/Subito-it/Bariloche; 
  cd Bariloche
  swift package update; swift package generate-xcodeproj
  xed .

选择 BarilocheExample scheme 并运行。

用法

Bariloche 允许为扁平型(如 lscpcat)和嵌套型(如 poddocker)命令行工具声明接口。

用法很简单,只需定义一个遵守 Command 协议的类,并将 ArgumentFlag 属性添加到您自定义的命令中。通过内省,Bariloche 将确定如何解析命令行参数,提取定义的标志和参数值。

解析

一旦定义了自定义命令,您需要将它的一个实例传递给 Bariloche 的初始化器并调用 parse() 来执行命令行解析。

let parser = Bariloche(command: CatCommand())
let result = parser.parse()

parse() 返回一个 [Command],其中包含所有成功解析的命令。 对于扁平型工具,此数组将始终包含 1 个项目;对于嵌套型工具,它可能包含 1 个或多个项目。

Command 协议

该库的基本组件是 Command 协议,它定义了命令的接口。

protocol Command: AnyObject {
    var name: String? { get }
    var usage: String? { get }
    var help: String? { get }
    
    func shouldShowUsage() -> Bool
    func run() -> Bool
}

帮助横幅数据

函数回调

Argument

Argument 类允许您将参数(命名、位置和可变参数)添加到命令。

class Argument<Value> : Equatable, CustomStringConvertible {
    var value: Value?

    init(name: String, 
         kind: Kind, 
         optional: Bool, 
         help: String?, 
         autocomplete: Autocomplete)
}

帮助横幅数据

其他

要初始化一个实例,您需要指定一个泛型类型,该类型将用于确定 value 属性的类型。 当前支持的类型包括: Int, UInt, Float, TimeInterval, Bool, URL。 当指定 URL 时,它将自动转换网络和文件系统 URL。

可变参数应指定为上述类型的数组(例如 Argument<[Int]>())。

当添加 Argument 属性到您自定义的 Command 时,添加它们的顺序将反映在解析命令行参数时。

采用以下两个命令

class MyCustomCommand1: Command {
    let arg1 = Argument<String>(name: "arg1", kind: .positional, optional: false)
    let arg2 = Argument<String>(name: "arg2", kind: .positional, optional: false)
}

class MyCustomCommand2: Command {
    let arg2 = Argument<String>(name: "arg2", kind: .positional, optional: false)
    let arg1 = Argument<String>(name: "arg1", kind: .positional, optional: false)
}

Flag

Flag 类允许您将标志添加到命令。

class Flag: Equatable, CustomStringConvertible {
    let short: String?
    let long: String?
    let help: String?
    var value: Bool = false
    
    init(short: String? = nil, long: String? = nil, help: String?)
}

帮助横幅数据

其他

自动补全

Bariloche 开箱即用地支持 zsh 自动补全。 每次执行命令时,当前用户 $fpath 中的自动补全文件都会更新,因此在第一次调用可执行文件后,它应该可以正常工作。 如果仍然不行,您可能需要关闭并重新打开终端一次。

您填充的 help 属性越多(在 FlagArgumentAutocomplete.Item 中),自动补全的结果就越好。

Autocomplete

一个枚举,用于确定如何在 shell 中自动补全参数

    enum Autocomplete {
        case none
        case items(_ items: [Item])
        case files(extension: String)
        case directories
        case paths(pattern: String?)
    }

Autocomplete.Item

一个结构体,用于建模在 shell 中自动补全参数时所需的预期值

extension Autocomplete {
    struct Item {
        let value: String
        let help: String?
    }
}

一个使用预期值的 Argument 示例

class MyCustomCommand: Command {
    let arg = Argument<String>(
        name: "arg",
        kind: .named(short: "t", long: "type"),
        optional: true,
        help: "Some help for the whole argument",
        autocomplete: .items([.init(value: "type1", help: "Type 1 help"),
                              .init(value: "type2", help: "Type 2 help")]))
}

示例

为了便于演示,我们重新创建了 cat 和 cocoapods 命令的部分实现。

扁平命令

下面显示了一个采用单个字符串参数的扁平命令

import Bariloche

class FlatCommand: Command {
    let usage: String? = "The cat utility reads files..."
    let verboseFlag = Flag(short: "v", long: "verbose", help: "Display non-printing characters...")
    let fileArgument = Argument<String>(name: "file", kind: .positional, optional: false, help: nil)
        
    func run() -> Bool {
        guard let value = fileArgument.value else {
            return false
        }
        
        print("Running with argument \(value)")
        return true
    }
}

let parser = Bariloche(command: CatCommand())
let result = parser.parse()

帮助横幅输出将如下所示

嵌套命令

下面显示了一个采用单个字符串参数的嵌套命令

class CocoaPodsCommand: Command {
    let usage: String? = "CocoaPods, the Cocoa library package manager."
    let subcommands: [Command] = [SearchCommand(), CacheCommand()]
    
    func run() -> Bool {
        return true
    }
}

class SearchCommand: Command {
    let name: String? = "search"
    let usage: String? = "Searches for pods, ignoring case, whose..."
    let help: String? = "Search for pods"
    let simple = Flag(short: nil, long: "simple", help: "Search only by name")
    let query = Argument<String>(name: "QUERY", kind: .positional, optional: false, help: nil)
    
    func run() -> Bool {
        guard let query = query.value else {
            return false
        }

        print("Running with query \(query)")
        return true
    }
}

class CacheCommand: Command {
    let name: String? = "cache"
    let usage: String? = "Manipulate the download cache for pods, like printing the cache content or cleaning the pods cache."
    let help: String? = "Manipulate the CocoaPods cache"    
            
    func run() -> Bool {
        print("Running cache")
        return true
    }        
}

let parser = Bariloche(command: CocoaPodsCommand())
let result = parser.parse()

  根命令的帮助横幅输出将如下所示

search 子命令的帮助将如下所示

请注意 QUERY--simple 是如何在用法描述中自动着色的。

用户交互 (stdin)

获取用户输入

Bariloche 允许使用 static func ask<Value>(_ question: String, endOfMarker: String = "", validate: ((_ answer: Value) throws -> Value)? = nil) -> Value 方法从 stdin 提示输入。

参数

示例

假设您想请求用户输入一个 Int,范围在 1-5 之间,然后将其转换为 0-4 范围。

let targetIndex: Int = Bariloche.ask("Select an index:") { answer in
    guard 1...5 ~= answer else { throw Error("Invalid index") }
    return answer - 1
}

贡献

欢迎贡献! 如果您有错误要报告,请随时通过打开新 issue 或发送 pull request 来提供帮助。

作者

Tomas Camin (@tomascamin)

许可

Barloche 在 Apache License, Version 2.0 许可下可用。 有关更多信息,请参见 LICENSE 文件。