Swift CommandLineKit

Platform: macOS Platform: Linux Language: Swift 5.8 IDE: Xcode 14 Carthage: compatible License: BSD

概述

这是一个支持在 macOS 上的 Swift 编程语言中开发命令行工具的库。它也可以在 Linux 下编译。该库提供以下功能:

命令行参数

基础知识

CommandLineKit 使用以下协议处理命令行参数:

  1. 为系统提供的命令行参数或自定义参数序列创建一个新的 Flags 对象。
  2. 对于每个标志,都会创建一个 Flag 对象并在 Flags 对象中注册。
  3. 一旦所有标志对象都被声明和注册,命令行就会被解析。解析完成后,可以使用标志对象来访问提取的选项和参数。

CommandLineKit 定义了不同类型的 Flag 子类,用于处理选项(即没有参数的标志)和参数(即带有参数的标志)。参数可以是单例参数(即它们只有一个值)或重复参数(即它们有多个值)。参数使用类型进行参数化,该类型定义了如何解析值。该框架原生支持 intdoublestringenum 类型,这意味着在实践中,仅仅使用内置的标志类几乎总是足够的。然而,该框架是可扩展的,并支持任意参数类型。

标志由短名称字符和长名称字符串标识。两者中至少需要定义一个。例如,“help”选项可以通过短名称“h”和长名称“help”来定义。在命令行中,用户可以使用 -h--help 来引用此选项;即,短名称以单破折号为前缀,长名称以双破折号为前缀。

参数是参数化的标志。参数直接跟在标志标识符之后(通常用空格分隔)。例如,带有长名称“size”的整数参数可以定义为:--size 64。如果参数是重复的,则多个参数可以跟在标志标识符之后,如本例所示:--size 2 4 8 16。序列以命令行参数的结尾、另一个标志或终止符“---”终止。终止符之后的所有命令行参数都不会被解析,并返回在 Flags 对象的 parameters 字段中。

程序化 API

这是一个来自 LispKit 项目的 示例。它使用 Flags 类提供的工厂方法(如 flags.stringflags.intflags.optionflags.strings 等)来创建和注册单个标志。

// Create a new flags object for the system-provided command-line arguments
var flags = Flags()

// Define the various flags
let filePaths  = flags.strings("f", "filepath",
                               description: "Adds file path in which programs are searched for.")
let libPaths   = flags.strings("l", "libpath",
                               description: "Adds file path in which libraries are searched for.")
let heapSize   = flags.int("x", "heapsize",
                           description: "Initial capacity of the heap", value: 1000)
let importLibs = flags.strings("i", "import",
                               description: "Imports library automatically after startup.")
let prelude    = flags.string("p", "prelude",
                              description: "Path to prelude file which gets executed after " +
                                           "loading all provided libraries.")
let prompt     = flags.string("r", "prompt",
                              description: "String used as prompt in REPL.", value: "> ")
let quiet      = flags.option("q", "quiet",
                              description: "In quiet mode, optional messages are not printed.")
let help       = flags.option("h", "help",
                              description: "Show description of usage and options of this tools.")

// Parse the command-line arguments and return error message if parsing fails
if let failure = flags.parsingFailure() {
  print(failure)
  exit(1)
}

该框架支持通过 Flags.usageDescription 函数打印支持的选项。对于上面定义的命令行标志,此函数返回以下用法描述:

usage: LispKitRepl [<option> ...] [---] [<program> <arg> ...]
options:
  -f, --filepath <value> ...
      Adds file path in which programs are searched for.
  -l, --libpath <value> ...
      Adds file path in which libraries are searched for.
  -h, --heapsize <value>
      Initial capacity of the heap
  -i, --import <value> ...
      Imports library automatically after startup.
  -p, --prelude <value>
      Path to prelude file which gets executed after loading all provided libraries.
  -r, --prompt <value>
      String used as prompt in REPL.
  -q, --quiet
      In quiet mode, optional messages are not printed.
  -h, --help
      Show description of usage and options of this tools.

命令行工具可以检查是否通过 Flag.wasSet 字段设置了标志。对于带有参数的标志,参数存储在 Flag.value 字段中。此字段的类型取决于标志类型。对于重复标志,使用数组。

这是一个关于如何使用上面代码片段定义的标志的示例:

// If help flag was provided, print usage description and exit tool
if help.wasSet {
  print(flags.usageDescription(usageName: TextStyle.bold.properties.apply(to: "usage:"),
                               synopsis: "[<option> ...] [---] [<program> <arg> ...]",
                               usageStyle: TextProperties.none,
                               optionsName: TextStyle.bold.properties.apply(to: "options:"),
                               flagStyle: TextStyle.italic.properties),
        terminator: "")
  exit(0)
}
...
// Define how optional messages and errors are printed
func printOpt(_ message: String) {
  if !quiet.wasSet {
    print(message)
  }
}
...
// Set heap size (assuming 1234 is the default if the flag is not set)
virtualMachine.setHeapSize(heapSize.value ?? 1234)
...
// Register all file paths
for path in filePaths.value {
  virtualMachine.fileHandler.register(path)
}
...
// Load prelude file if it was provided via flag `prelude`
if let file = prelude.value {
  virtualMachine.load(file)
}

声明式 API

下面的代码说明了如何将 Command 协议与声明各种命令行标志的属性包装器结合使用。像这样声明的命令行工具的整个生命周期将自动管理。在标志被解析后,将调用 run()fail(with:) 方法(取决于标志解析是成功还是失败)。

@main struct LispKitRepl: Command {
  @CommandArguments(short: "f", description: "Adds file path in which programs are searched for.")
  var filePath: [String]
  @CommandArguments(short: "l", description: "Adds file path in which libraries are searched for.")
  var libPaths: [String]
  @CommandArgument(short: "x", description: "Initial capacity of the heap")
  var heapSize: Int = 1234
  ...
  @CommandOption(short: "h", description: "Show description of usage and options of this tools.")
  var help: Bool
  @CommandParameters // Inject the unparsed parameters
  var params: [String]
  @CommandFlags // Inject the flags object
  var flags: Flags
  
  mutating func fail(with reason: String) throws {
    print(reason)
    exit(1)
  }
  
  mutating func run() throws {
    // If help flag was provided, print usage description and exit tool
    if help {
      print(flags.usageDescription(usageName: TextStyle.bold.properties.apply(to: "usage:"),
                                   synopsis: "[<option> ...] [---] [<program> <arg> ...]",
                                   usageStyle: TextProperties.none,
                                   optionsName: TextStyle.bold.properties.apply(to: "options:"),
                                   flagStyle: TextStyle.italic.properties),
            terminator: "")
      exit(0)
    }
    ...
    // Define how optional messages and errors are printed
    func printOpt(_ message: String) {
      if !quiet {
        print(message)
      }
    }
    ...
    // Set heap size
    virtualMachine.setHeapSize(heapSize)
    ...
    // Register all file paths
    for path in filePaths {
      virtualMachine.fileHandler.register(path)
    }
    ...
    // Load prelude file if it was provided via flag `prelude`
    if let file = prelude {
      virtualMachine.load(file)
    }
  }
}

文本样式和颜色

CommandLineKit 提供了一个 TextProperties 结构,用于将文本颜色、背景颜色和文本样式捆绑到一个对象中。文本属性可以使用 with(:) 函数合并,并使用 apply(to:) 函数应用于字符串。

用于 TextColorBackgroundColorTextStyle 的单独枚举定义了各个属性。

读取字符串

CommandLineKit 包含库 Linenoise-Swift 最初定义的“readline”API 的显着改进版本。它支持 Unicode 文本、多行文本输入和样式化文本。它支持所有现有功能,例如高级键盘支持历史记录文本补全提示

以下代码说明了 LineReader API 的用法:

if let ln = LineReader() {
  ln.setCompletionCallback { currentBuffer in
    let completions = [
      "Hello!",
      "Hello Google",
      "Scheme is awesome!"
    ]
    return completions.filter { $0.hasPrefix(currentBuffer) }
  }
  ln.setHintsCallback { currentBuffer in
    let hints = [
      "Foo",
      "Lorem Ipsum",
      "Scheme is awesome!"
    ]
    let filtered = hints.filter { $0.hasPrefix(currentBuffer) }
    if let hint = filtered.first {
      let hintText = String(hint.dropFirst(currentBuffer.count))
      return (hintText, TextColor.grey.properties)
    } else {
      return nil
    }
  }
  print("Type 'exit' to quit")
  var done = false
  while !done {
    do {
      let output = try ln.readLine(prompt: "> ",
                                   maxCount: 200,
                                   strippingNewline: true,
                                   promptProperties: TextProperties(.green, nil, .bold),
                                   readProperties: TextProperties(.blue, nil),
                                   parenProperties: TextProperties(.red, nil, .bold))
      print("Entered: \(output)")
      ln.addHistory(output)
      if output == "exit" {
        break
      }
    } catch LineReaderError.CTRLC {
      print("\nCaptured CTRL+C. Quitting.")
      done = true
    } catch {
      print(error)
    }
  }
}

要求

版权

作者:Matthias Zenger (matthias@objecthub.com)
版权所有 © 2018-2023 Google LLC。
请注意:这不是 Google 的官方产品。