使用Swift Package Manager ArgumentParser处理命令
在命令行应用程序中解析参数不是一件容易的事。 随着时间的推移,应用程序可以演变为支持许多功能,这使得支持的参数数量大大增加。
例如,令人印象深刻的curl
命令行工具具有207个不同的参数选项,包括参数和标志。
大量的选择使curl
非常灵活和有力。 但是,它也很难使用。 很难学习和掌握。
较新的命令行工具(例如git
, gem
, carthage
或travis
通过将其分组在子命令中来更好地组织其功能。
例子:
-
git clone
,git fetch
,git branch
,git commit
,git push
… -
gem install
,gem update
,gem build
… -
carthage update
,carthage build
,carthage archive
… -
travis login
,travis monitor
,travis 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 PackageDescriptionlet 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: Stringprotocol Command {
let command: String
let overview: Stringinit (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 Basicimport Foundation
import Utility
import Basicstruct 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 Basicimport Foundation
import Utility
import Basicstruct 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 CalculatorUSAGE: 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 Basicimport Foundation
import Utility
import Basicstruct 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 Basicimport Foundation
import Utility
import Basicstruct 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 Basicimport Foundation
import Utility
import Basicstruct 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 CalculatorUSAGE: 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