用swift 编写第一个CLI(command line tool)程序
起因
因为在研究swiftLint的时候,发现了熟悉的swift code,但却无法理解到底实现了什么?
发现swiftlint 都是基于开源库swift-argument-parser 做的,于是乎去了解apple的开源库swift-argument-parser , 然后才知道了,这里面还大有玄机嘞!
swift-argument-parser 这个是用来解析命令行参数的苹果开源库,应用这个库能很方便的进行命令行参数解析。
后来非常好奇,那么swiftLint工程里的Package.swift文件又是啥呀?
一练的疑问,一头的雾水,于是乎扎入到探究的深坑中…
经过一两天的研究,翻看apple的官方文档、开源库swift-argument-parser 的使用、别人的博客,最后进入了另一个天地。 下面我将逐个解开里边的谜题(当然我是第一次知道,因为之前没接触过…,不要笑话哦😄)
依次来说
首先说一说,我们看到Package.swift文件
其实这是苹果新出的一种代码管理方案,叫做Swift Package Manager, 简称为SPM, 我们之前应该对Cocoapods、Carthage(专门用于swift工程无嵌入式工程管理)都比较了解,但是其实也比较类似,SPM也是用来管理三方库的依赖的。 对应到我们Cocoapods 这个Package.swift文件就相当于.podfile。
那么我们怎么创建呢?
两种方式可以创建:
1. 我们可以通过命令行的方式创建:swift package init [–type executable]
- 我们可以通过打开xcode创建,
这样我们就可以创建出来这样一个工程,如下图:
用xcode 打开Package.swift 即可打开工程。
然后我们需要配置Package.swift :
import PackageDescription
let package = Package(
name: "Counter",
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.executableTarget(
name: "Counter",
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),
]
)
如果需要了解所有的参数解释,我们可以通过看xcode Package这个结构体了解。那其实上我们简单的配置包名、包名、依赖、可执行目标已然足够。 这样我们的一个swift package 就配置完成了,当配置完成后,xcode会自动检测包名、可执行名字 和 类名是否对应,以及检测依赖并且下载安装。
紧接着我们说说命令行能干什么
比如我们用的git命令、pod 命令其实都是命令行程序。只是他们可能不是用swift实现的而已,比如我们熟知的cocapods就是用ruby实现。那么相必这么举例我们就知道命令行能干什么?可干的事情可太多了,当然最早说的swiftLint也是一个命令行程序。
接下来我们根据swift-argument-parser 官方示例,写一个小小的CLI
写一个经典的,读取a文件的内容并统计文件中单词的数量,输出到b文件。
如果不用开源库swift-argument-parser ,我们需要一个main.swift 作为程序的入口:
// main.swift
let counter = Counter()
do {
try counter.run()
} catch {
print("An error occurred")
}
因为原生的参数解析比较麻烦,我们就写一个比较简单输出吧,
这个时候我们需要写一个Counter:
// Counter.swift
import Foundation
public struct Counter {
private var arguments: [String]
init(arguments: [String] = CommandLine.arguments) {
self.arguments = arguments
}
public func run() throws {
print("test")
}
}
然后我们再当前跟目录下,执行命令: swift build
, 然后我们执行 swift run Counter
, 就可以看见终端的输出了
这样我们算是一个简单的CLI就开发完了。
ok,我们再使用swift-argument-parser 去实现,我们最初的统计
使用ArgumentParser, 我们就不需要在给main.swift入口,只需要命名类或者结构体,继承ParsableCommand 或者AsyncParsableCommand,然后标识为@main即可。
ArgumentParser 有三种类型标识: @Argument 标识必须的参数,带有顺序的, 不可指定名字 @Option 是无序的,可以指定名字。以及是否简写等, @Flag 通常是一个bool指,一个标识。下面分别举例这三种对应到命令行
// 1 @Argument
@Argument var inputFile: String
@Argument var outputFile: String
// 这种情况对应到命令行我们就需要 Counter test1.txt test2.txt ,以属性的顺序给定参数。并且不需要指定
// 2. @Option
@Option var inputFile: String
@Option var outputFile: String
// 这种情况对应到命令行我们就需要 Counter --input-file test1.txt --output-file test2.txt ,当然顺序可以随意,只要指定即可。当然我们也可以@Option(name: help:)等初始化方法指定提示语、简写等等
// 2. @Flag
@Option var inputFile: String
@Option var outputFile: String
@Flag var verbose: Bool = false
// 这种情况对应到命令行我们就需要 Counter --verbose --input-file test1.txt --output-file test2.txt。
OK,大概swift-argument-parser就介绍这么多,具体的可以查看github, 或者官网参考用法。
接下来是我们统计的命令行实现:
import ArgumentParser
import Foundation
@main
struct Counter: ParsableCommand {
@Option(name: [.short, .customLong("input")], help: "A file to read")
var inputFile: String
@Option(name: [.short, .customLong("output")], help: "A file to save word counts to")
var outputFile:String
@Flag(name: .shortAndLong, help: "Print status updates while counting") var verbose: Bool = false
mutating func run() throws {
if verbose {
print("""
Counting words in '\(inputFile)' \
and write result into '\(outputFile)'
""")
}
guard let input = try? String(contentsOfFile: inputFile) else {
throw RuntimeError("Can't read \(inputFile) contents")
}
let words = input.components(separatedBy: .whitespacesAndNewlines)
.map { word in
word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted).lowercased()
}
.compactMap { word in
word.isEmpty ? nil : word
}
let counts = Dictionary(grouping: words, by: { $0 })
.mapValues { $0.count }
.sorted(by: { $0.value > $1.value })
if verbose {
print("found \(counts.count) words")
}
let output: String = counts.map { word, count in
"\(word): \(count)"
}.joined(separator: "\n")
guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else {
throw RuntimeError("Can't write to \(outputFile)")
}
}
}
struct RuntimeError: Error, CustomStringConvertible {
var description: String
init(_ description: String) {
self.description = description
}
}
在我们理解了ArgumentParser之后和了解了命令行开发之后,在看这些swift的代码就比较简单了。写起来也很简单了。
我们测试一下:
新建test1.txt、test2.txt, 在test1.txt里加入
然后,执行 swift run Counter -i test1.txt -o test2.txt -v, 查看输出
我们查看test2.txt文件
确实也写进去了。
我们的命令现在只能在当前工程目录下,并且通过swift run命令执行,如何使得命令在任何目录终端都可以执行?
我们需要打一个release的可执行包,手动移到/urs/local/bin下。
# build 打包默认是根据你的mac系统打包的
swift build --configuration release --product Counter
# 我们还可以增加参数--build-system [native | xcode] 进行打包,xcode 打出来的包适合x86和arm系统
swift build --configuration release --build-system xcode --product Counter
cd .build/release/Counter
cp -f Counter /usr/local/bin/counter
这样,不管在哪,我们都可以直接执行counter命令啦!
当然如果我们需要将我们CLI进行推广使用,肯定不能像以上那样进行,这样我们可以进行以下的方式进行安装:
1、写一个自动安装脚本,下载已经打好的可执行文件,然后将可执行文件移动到/usr/local/bin/目录下
2、我们可以制作home brew 通过home brew 自动完成安装。感兴趣的可以参考我的另一篇文章:创建三方home brew
结束语(鸡汤来一碗~)
往往有些事情超出了我们了解的范围之后,我们内心是有些陌生、一头雾水、抗拒的,但是只要你沉下心来,去以一颗学习的心,去了解,去学习,那么就打开一个新世界。
弄清楚以上这些之后,回头在看swiftLint代码,也变得容易多了。好了,接着学习swiftlint实现去了~
参考
https://blog.csdn.net/Desgard_Duan/article/details/111878243
https://www.avanderlee.com/swift/command-line-tool-package-manager/
https://developer.apple.com/documentation/packagedescription
https://github.com/apple/swift-argument-parser
https://apple.github.io/swift-argument-parser/documentation/argumentparser/gettingstarted/