使用Swift Package Manager ArgumentParser处理命令

在命令行应用程序中解析参数不是一件容易的事。 随着时间的推移,应用程序可以演变为支持许多功能,这使得支持的参数数量大大增加。

例如,令人印象深刻的curl命令行工具具有207个不同的参数选项,包括参数和标志。

大量的选择使curl非常灵活和有力。 但是,它也很难使用。 很难学习和掌握。

较新的命令行工具(例如gitgemcarthagetravis通过将其分组在子命令中来更好地组织其功能。

例子:

  • git clonegit fetchgit branchgit commitgit push
  • gem installgem updategem build
  • carthage updatecarthage buildcarthage archive
  • travis logintravis monitortravis status

让我们看看如何使用Swift Package Manager中的现成的ArgumentParser类来创建支持这些类型的子命令的命令行工具。

我们将编写一个非常基本的命令行计算器来计算基本的数学运算。 这是建议的接口:

  • 加法: calculator add ...
  • 减法: calculator subtract ...
  • 乘法: calculator multiply ...
  • 除法: calculator divide ...

首先,我们创建新工具:

 $ mkdir calculator && cd calculator 
$ swift package init --type executable
$ swift run calculator
Hello, world!

添加Swift Package Manager作为对Package.swift的依赖项:

 // swift-tools-version:4.0 
import PackageDescription
// swift-tools-version:4.0
import PackageDescription
let package = Package (
name: "calculator",
dependencies: [

.package (url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0"),
],
targets: [

.target (name: "calculator", dependencies: ["Utility"]),
]
)
let package = Package (
name: "calculator",
dependencies: [

.package (url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0"),
],
targets: [

.target (name: "calculator", dependencies: ["Utility"]),
]
)

并生成Xcode项目:

 $ swift package generate-xcodeproj 
$ open calculator.xcodeproj

我们的命令将基于ArgumentParser提供的subparser功能。

 public func add (subparser command: String , overview: String ) -> ArgumentParser 

此功能允许创建嵌套的“解析器”,可以在其相应级别处理参数。

子解析器由其命令名称和概述定义。 我们可以将此信息外推到协议中。 为了注册我们的命令,我们将使用主ArgumentParser实例对其进行初始化。 最后,我们将主命令执行代码放在run方法中。

 protocol Command { 

let command: String
let overview: String
protocol Command {

let command: String
let overview: String
init (parser: ArgumentParser )

func run (with arguments: ArgumentParser.Result ) throws }
init (parser: ArgumentParser )

func run (with arguments: ArgumentParser.Result ) throws }

然后,我们可以创建第一个命令:

  struct AdditionCommand : Command { let command = "add" 

let overview = "Compute the sum of all the numbers."
let command = "add"

let overview = "Compute the sum of all the numbers."
init (parser: ArgumentParser ) {
parser
.add (subparser: command, overview: overview)
}
func run (with arguments: ArgumentParser.Result ) throws {

// }
}

如果我们不能传递一些数字进行运算,计算器将毫无用处。

在这种情况下,我们正在寻找一个整数类型的位置参数,该参数可以包含一个或多个值(一个数组)。

  private let numbers: PositionalArgument< [ Int ] > init (parser: ArgumentParser ) { 

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}

这将允许我们传递任意数量的整数来计算总和:

 $ calculator add 1 2 3 4 
10

现在我们可以在run方法中使用reduce来计算总和:

 func run (with arguments: ArgumentParser.Result ) throws { 

guard let integers = arguments .get (numbers) else {

return }

let result = integers .reduce (0, + )

print (result)
}

🛠AdditionCommand.swift的完整代码:

 import Foundation 
import Utility
import Basic
import Foundation
import Utility
import Basic
struct AdditionCommand : Command { let command = "add"

let overview = "Compute the sum of all the numbers."
let command = "add"

let overview = "Compute the sum of all the numbers."
private let numbers: PositionalArgument< [ Int ] > init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
func run (with arguments: ArgumentParser.Result ) throws {

guard let integers = arguments .get (numbers) else {

return }

let result = integers .reduce (0, + )

print (result)
}
}

现在我们可以创建命令了,我们想轻松地注册它们,以便ArgumentParser可以帮助我们确定要执行的命令。

为此,我们将创建一个命令注册表,该注册表将跟踪我们的命令。 我们需要跟踪的只是命令数组和ArgumentParser主类的实例。

  struct CommandRegistry { private var commands: [ Command ] = [] mutating func register (command: Command.Type ) { 
commands
.append (command .init (parser: parser))
}
}
mutating func register (command: Command.Type ) {
commands
.append (command .init (parser: parser))
}
}

为了使main.swift保持整洁,我们将让CommandRegistry处理以下任务:

  • 解析参数
  • 确定要执行的命令
  • 通过调用run()执行命令

首先,我们将创建主ArgumentParser

  private let parser: ArgumentParser init (usage: String , overview: String ) { 
parser
= ArgumentParser (usage: usage, overview: overview)
}
init (usage: String , overview: String ) {
parser
= ArgumentParser (usage: usage, overview: overview)
}

此方法将解析命令行参数(只需调用一次):

 private func parse () throws -> ArgumentParser.Result { 

let arguments = Array ( ProcessInfo. processInfo . arguments .dropFirst ())

return try parser .parse (arguments)
}

我们可以通过在解析后的参数结果中调用subparser()方法并从注册表中获取相应的命令来确定要运行的命令:

 private func process (arguments: ArgumentParser.Result ) throws { 

guard let subparser = arguments .subparser (parser),

let command = commands .first (where: { $0 . command == subparser }) else {
parser
.printUsage (on: stdoutStream)

return }

try command .run (with: arguments)
}

ArgumentParser包括一个方便的printUsage方法,在用户输入未知命令的情况下,我们在此使用printUsage方法。

Command CommandRegistry.swift完整代码:

 import Foundation 
import Utility
import Basic
import Foundation
import Utility
import Basic
struct CommandRegistry { private let parser: ArgumentParser
private var commands: [ Command ] = []
private let parser: ArgumentParser
private var commands: [ Command ] = []
private let parser: ArgumentParser
private var commands: [ Command ] = []
init (usage: String , overview: String ) {
parser
= ArgumentParser (usage: usage, overview: overview)
}
init (usage: String , overview: String ) {
parser
= ArgumentParser (usage: usage, overview: overview)
}
mutating func register (command: Command.Type ) {
commands
.append (command .init (parser: parser))
}
func run () {

do {

let parsedArguments = try parse ()

try process (arguments: parsedArguments)
}

catch let error as ArgumentParserError {

print (error . description)
}

catch let error {

print (error . localizedDescription)
}
}
private func parse () throws -> ArgumentParser.Result {

let arguments = Array ( ProcessInfo. processInfo . arguments .dropFirst ())

return try parser .parse (arguments)
}
private func process (arguments: ArgumentParser.Result ) throws {

guard let subparser = arguments .subparser (parser),

let command = commands .first (where: { $0 . command == subparser }) else {
parser
.printUsage (on: stdoutStream)

return }

try command .run (with: arguments)
}
}

我们已经设法将命令执行代码封装在命令类中,而主要参数解析代码则由命令注册表处理。

这给我们留下了一个非常苗条的main.swift文件,非常易于维护和扩展。

  var registry = CommandRegistry (usage: " ", overview: "Basic Calculator") registry .register (command: AdditionCommand.self ) registry .run () 

运行我们的命令会生成漂亮的用法输出:

 $ swift run calculator 
OVERVIEW: Basic Calculator
USAGE: calculator SUBCOMMANDS:
add Compute the sum of all the numbers.
USAGE: calculator SUBCOMMANDS:
add Compute the sum of all the numbers.
SUBCOMMANDS:
add Compute the sum of all the numbers.

add详细帮助:

 $ swift run calculator add --help 
OVERVIEW: Compute the sum of all the numbers.
POSITIONAL ARGUMENTS:
numbers List of numbers to operate with.

我们可以通过传递一些整数来测试它:

 $ swift run calculator add 400 600 
1000
$ .build/debug/calculator add 1 2 3 4 5 6 7 8 9 10
55
$ .build/debug/calculator add 40000000000000000000000 1
'40000000000000000000000' is not convertible to Int for argument numbers; use --help to print usage

🛠SubtractionCommand.swift的完整代码:

 import Foundation 
import Utility
import Basic
import Foundation
import Utility
import Basic
struct SubtractionCommand : Command { let command = "subtract"

let overview = "Compute the difference of all the numbers."
let command = "subtract"

let overview = "Compute the difference of all the numbers."
private let numbers: PositionalArgument< [ Int ] > init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
func run (with arguments: ArgumentParser.Result ) throws {

guard var integers = arguments .get (numbers) else {

return }

let first = integers .removeFirst ()

let result = integers .reduce (first, - )

print (result)
}
}

🛠MultiplicationCommand.swift的完整代码:

 import Foundation 
import Utility
import Basic
import Foundation
import Utility
import Basic
struct MultiplicationCommand : Command { let command = "multiply"

let overview = "Compute the product of all the numbers."
let command = "multiply"

let overview = "Compute the product of all the numbers."
private let numbers: PositionalArgument< [ Int ] > init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
func run (with arguments: ArgumentParser.Result ) throws {

guard let integers = arguments .get (numbers) else {

return }

let result = integers .reduce (1, * )

print (result)
}
}

DivisionCommand.swift完整代码:

 import Foundation 
import Utility
import Basic
import Foundation
import Utility
import Basic
struct DivisionCommand : Command { let command = "divide"

let overview = "Compute the division of all the numbers."
let command = "divide"

let overview = "Compute the division of all the numbers."
private let numbers: PositionalArgument< [ Int ] > init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
init (parser: ArgumentParser ) {

let subparser = parser .add (subparser: command, overview: overview)
numbers
= subparser .add (positional: "numbers", kind: [ Int ] .self ,
usage: "List of numbers to operate with.")
}
func run (with arguments: ArgumentParser.Result ) throws {

guard var integers = arguments .get (numbers) else {

return }

let first = integers .removeFirst ()

let result = integers .reduce (first, / )

print (result)
}
}

我们更新的main.swift

  var registry = CommandRegistry (usage: " ", overview: "Basic Calculator") registry .register (command: AdditionCommand.self ) 
registry
.register (command: SubtractionCommand.self )
registry
.register (command: MultiplicationCommand.self )
registry
.register (command: DivisionCommand.self )
registry .register (command: AdditionCommand.self )
registry
.register (command: SubtractionCommand.self )
registry
.register (command: MultiplicationCommand.self )
registry
.register (command: DivisionCommand.self )
registry .register (command: AdditionCommand.self )
registry
.register (command: SubtractionCommand.self )
registry
.register (command: MultiplicationCommand.self )
registry
.register (command: DivisionCommand.self )
registry .run ()

不带参数运行我们的工具会输出更新的用法概述:

 $ swift run calculator 
OVERVIEW: Basic Calculator
USAGE: calculator SUBCOMMANDS:
add Compute the sum of all the numbers.
divide Compute the division of all the numbers.
multiply Compute the product of all the numbers.
subtract Compute the difference of all the numbers.
USAGE: calculator SUBCOMMANDS:
add Compute the sum of all the numbers.
divide Compute the division of all the numbers.
multiply Compute the product of all the numbers.
subtract Compute the difference of all the numbers.
SUBCOMMANDS:
add Compute the sum of all the numbers.
divide Compute the division of all the numbers.
multiply Compute the product of all the numbers.
subtract Compute the difference of all the numbers.

一些测试💯:

 $ swift run calculator subtract 10 7 7 
-4
$ swift run calculator multiply 1 2 3 4 5
120
$ swift run calculator divide 20 5 2
2

而且当然:

 $ swift run calculator divide 20 0 
Fatal error: Remainder of or division by zero
Illegal instruction: 4

😅

如您所见,Swift Package Manager开箱即用,提供了一系列强大的工具。 ArgumentParser是命令行参数的出色解析器。

注意:本文最初发布在我的个人博客上, 网址 http://www.enekoalonso.com/articles/handling-commands-with-swift-package-manager

Interesting Posts