ArgumentEncoding

ci codecov

一个用于将类型编码为字符串数组,或称“参数”的库。

稳定性

该库仍在开发中。在达到 1.0.0 版本之前,应将其视为实验性的,并且在功能发布之间可能会有重大更改。

用法

通常,对 CLI 工具进行建模将从 TopLevelCommandRepresentable 开始。 这是入口点,必须通过 commandValue 函数显式声明其自身的名称。

struct MyCommand: TopLevelCommandRepresentable {
    func commandValue() -> Command { "my-command" }
    let flagFormatter = FlagFormatter(prefix: .doubleDash) }
    let optionFormatter = OptionFormatter(prefix: .doubleDash) }
}

每个命令或子命令可能对标志和选项有自己的格式要求。 因此,TopLevelCommandRepresentableCommandRepresentable 都继承自 FormatterNode。 这要求声明 FlagFormatterOptionFormatter

MyCommand 中,我们需要能够对布尔值进行建模,以启用/禁用某些功能。 这就是 Flag 的用武之地。 作为某种符合 ArgumentGroup 类型的属性包装器,它最方便。

struct MyCommand: TopLevelCommandRepresentable {
    func commandValue() -> Command { "my-command" }
    let flagFormatter = FlagFormatter(prefix: .doubleDash) }
    let optionFormatter = OptionFormatter(prefix: .doubleDash) }

    @Flag var myFlag: Bool = false
}

除了对启用/禁用功能的能力进行建模之外,我们还需要针对某个变量设置一个值。 为此,我们可以使用 Option。 对于可以有多个值的选项,可以使用 OptionSet

struct MyCommand: TopLevelCommandRepresentable {
    func commandValue() -> Command { "my-command" }
    let flagFormatter = FlagFormatter(prefix: .doubleDash) }
    let optionFormatter = OptionFormatter(prefix: .doubleDash) }

    @Flag var myFlag: Bool = false
    @Option var myOption: Int = 0
    @OptionSet var myOptions: [String] = ["value1", "value2"]
}

通过 Positional 类型支持仅作为值的,没有键的位置参数。

struct MyCommand: TopLevelCommandRepresentable {
    func commandValue() -> Command { "my-command" }
    let flagFormatter = FlagFormatter(prefix: .doubleDash) }
    let optionFormatter = OptionFormatter(prefix: .doubleDash) }

    @Flag var myFlag: Bool = false
    @Option var myOption: Int = 0
    @OptionSet var myOptions: [String] = ["value1", "value2"]
    @Positional var myPositional: String = "positional"
}

动机

当使用 Swift 运行可执行文件时,将结构化类型(struct、class、enum)编码为传递给可执行文件的参数数组可能会很有帮助。

public enum SwiftCommand {
    case run(String)
    case test(TestCommand)

    public func asArgs() -> [String] {
        let childArgs: [String]
        switch self {
        case let .run(executableProduct):
            childArgs = [executableProduct]
        case let .test(testCommand):
            childArgs = testCommand.asArgs()
        }
        return ["swift"] + childArgs
    }
}

public struct TestCommand {
    public let parallel: Bool
    public let numWorkers: Int
    public let showCodecovPath: Bool
    public let testProducts: [String]

    public init(
        parallel: Bool = true,
        numWorkers: Int = 1,
        showCodecovPath: Bool = false,
        testProducts: [String]
    ) {
        self.parallel = parallel
        self.numWorkers = numWorkers
        self.showCodecovPath = showCodecovPath
        self.testProducts = testProducts
    }

    public func asArgs() -> [String] {
        var args = [String]()
        if parallel {
            args.append("--parallel")
        }
        args.append(contentsOf: ["--num-workers", numWorkers.description])
        if showCodecovPath {
            args.append("--show-codecov-path")
        }
        args.append(contentsOf: testProducts)
        return args
    }
}

这种方法繁琐且容易出错。 有一些方法可以在仍然手动编写 asArgs 的同时进行改进,但它仍然远非理想。

ArgumentEncoding 允许编写可以轻松编码为参数数组的类型,而无需手动编码。

import ArgumentEncoding

enum SwiftCommand: TopLevelCommandRepresentable {
    func commandValue() -> Command { "swift" }

    var flagFormatter: FlagFormatter { FlagFormatter(prefix: .doubleDash, body: .kebabCase) }
    var optionFormatter: OptionFormatter { OptionFormatter(prefix: .doubleDash, body: .kebabCase) }

    case run(RunCommand)
    case test(TestCommand)
}

struct RunCommand: CommandRepresentable {
    @Positional var executable: String
}

extension RunCommand: ExpressibleByStringLiteral {
    init(stringLiteral value: StringLiteralType) {
        self.init(executable: Positional(wrapped: value))
    }
}

struct TestCommand: CommandRepresentable {
    @Flag var parallel: Bool = true
    @Option var numWorkers: Int = 1
    @Flag var showCodecovPath: Bool = false
    @OptionSet var testProducts: [String] = []
}