为什么使用Swift
一般而言,目前实现一个命令行工具使用最多的语言是 shell、C或者是Python,那么为什么这次要用 Swift,主要有两个原因:
- 第一是因为这篇文章主要针对的是会 Swift 的开发者,尤其是iOS开发者。
- 第二是因为 Swift 是完全开源的语言,目前也已广泛应用于各个平台,相比于 shell 和 C,语言抽象能力表达能力更强,类型系统更安全,相比于 Python,性能更好,可维护性和健壮性更优。
手把手教你实现一个计算器
本文通过一个计算器的例子来入门,更高阶的用法还请读者自行研究,文末附有相关文档。
创建项目
打开Xcode,File -> New -> Project ,选择macOS ,类型为Command Line Tool ,根据提示,输入项目名Caculator,完成创建。

完成创建后,删除掉main.swift文件。
引入依赖

点击**+**号,选择图中所示,然后等待下载安装。
ArgumentParser
是 Swift 编写的一个开源的强大的命令行参数解析库。它可以让我们轻松地在 Swift 命令行程序中定义和解析命令行参数。
相比于 Python 的argparse
,ArgumentParser
有如下优点:
- 使用 Swift 的强类型系统,可以在编译时捕获许多错误。
- 支持复杂的命令行接口,包括子命令、标志、选项等。
- 能自动生成更详细且格式良好的帮助信息。
- 声明式的语法,更简单清晰。
核心逻辑
我们主要实现两个功能:
- 输入若干个数,输出相加的结果,并能够选择是否使用16进制输出。使用起来是这样
caculator add 0 1 2 -x
。 - 输入RMB数额,根据当前最新的汇率输出相应的美元数额。使用起来是这样
caculator rmb 100 -usd
。
要实现这两个功能,首先我们得先定义一个主命令,新建一个 Swift 文件,取名为Caculator,填充如下代码:
swift
import ArgumentParser
@main
struct Caculator: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "",
abstract: "一个命令行计算器",
version: "1.0.0",
subcommands: [])
}
ParsableCommand
是一个协议,表示可解析的命令行,可以自动解析命令行参数,自动生成帮助信息。
swift
/// 可以作为嵌套命令树的一部分执行的类型。
public protocol ParsableCommand: ParsableArguments {
/// 用于配置命令或子命令的一些参数,如名称、帮助摘要、选项和子命令等.
static var configuration: CommandConfiguration { get }
/// 内部实现细节,自动生成的命令名称,一般不需要我们手动实现。
static var _commandName: String { get }
/// 此命令的主要功能逻辑。
///
/// 默认实现为打印这个命令的帮助信息。
mutating func run() throws
}
CommandConfiguration
表示一个命令的配置项,定义了一个命令的使用名、摘要描述、版本号、子命令等相关配置,主要用到的就这几种,更详细的配置项可查看文档。
subcommands
是子命令,稍后会填充。commandName
是实际使用时的主命令名,可以不传,默认是结构体的名字小写。
run()
方法就是该命令主要功能的实现方法。
实现相加
接下来在文件中添加如下代码,当然也可以新建一个文件取名Add。
swift
extension Caculator {
static func format(_ result: Int, usingHex: Bool) -> String {
usingHex ? String(result, radix: 16)
: String(result)
}
struct Add: ParsableCommand {
static var configuration =
CommandConfiguration(abstract: "Print the sum of the values.")
@Flag(name: [.customLong("hex-output"), .customShort("x")],
help: "Use hexadecimal notation for the result.")
var hexadecimalOutput = false
@Argument(
help: "A group of integers to operate on.")
var values: [Int] = []
mutating func run() {
let result = values.reduce(0, +)
print(format(result, usingHex: hexadecimalOutput))
}
}
}
我们给Caculator
嵌套了一个结构体Add
,让它和Caculator
一样遵循ParsableCommand
并实现配置项,然后给它新增values
属性用来接收需要相加的若干数,hexadecimalOutput
用来表示是否使用16进制输出结果。
简单介绍下两个属性包装器:
@Argument
用于声明命令行参数解析中的位置参数(Positional Arguments)。它不需要参数名,传值时必须按照定义的顺序。它是命令的必填项,除非它是可空类型或者有初始值。它有个常用参数help
,可为参数添加说明。
@Flag
用于定义命令行参数中的标志或开关参数。这类参数通常用于启用/禁用某功能或输出,以破折号为前缀进行使用。一般是Bool/Int
类型。
然后填充run()
方法实现若干数相加的逻辑,并输出指定的结果。
最后别忘记修改Caculator
主命令的subcommands
的值,传入Add.self
。
至于逻辑写的对不对,我们稍后在调试。
当然一个计算器不可能只有加法,需要实现加减乘除,这些操作需要的参数基本一致,如果每个操作都列一遍那两个属性,那未免扩展性有点低,此时我们需要把那两个属性抽到一个结构体里面:
swift
struct Options: ParsableArguments {
@Flag(name: [.customLong("hex-output"), .customShort("x")],
help: "Use hexadecimal notation for the result.")
var hexadecimalOutput = false
@Argument(
help: "A group of integers to operate on.")
var values: [Int] = []
}
然后把原来那两个删掉,替换成这一句:
swift
@OptionGroup var options: Options
这样其他运算操作也能复用了,这里就不实现了,读者可以自行尝试。
@OptionGroup
用于将多个相关的参数分组,并将它们作为一个整体进行处理。通过使用此包装器,可以在命令行工具中定义一组常用的参数,并在多个命令中复用这些参数。
实现汇率转换
接下来在文件中添加如下代码,当然也可以新建一个文件取名RMB。
swift
extension Caculator {
enum Currency: Float, EnumerableFlag {
case usd = 0.14
case jp = 20.2
func chinese() -> String {
switch self {
case .usd:
return "美元"
case .jp:
return "日元"
}
}
}
struct RMB: AsyncParsableCommand {
static var configuration =
CommandConfiguration(abstract: "计算人民币以最新汇率对应的其他币种数额")
@Argument(
help: "人民币数额")
var rmb: Float
@Flag(help: "要转换成的币种")
var currency: Currency
func getExchangeRate(with currency: Currency) async -> Float {
// 模拟网络请求异步获取汇率值
return await withCheckedContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: DispatchWorkItem(block: {
continuation.resume(returning: currency.rawValue)
}))
}
}
func run() async throws {
let rate = await getExchangeRate(with: currency)
let result = rmb * rate
print("\(rmb)人民币约等于\(result)\(currency.chinese())")
}
}
}
这段代码涉及到异步获取汇率值,但由于命令行程序是同步执行的,必须阻塞线程等待结果返回,这里使用了async/await
来简化异步操作。除了这个子命令需要遵循AsyncParsableCommand
,同时我们需要把之前主命令遵循的ParsableCommand
改成AsyncParsableCommand
,才能执行异步版本的run()
方法。
AsyncParsableCommand
是继承于ParsableCommand
的异步版本,重载了run方法,额外标记了async,用以执行一些异步操作。关于async/await,以及上面代码中的
withCheckedContinuation
方法,具体请查阅Swift结构化并发相关内容。本文只是提供在命令行程序中执行异步操作的方案,不进行赘述。
由于我们有转换多币种的需求,我们需要可以指定转换成哪个币种,这里使用了EnumerableFlag
,可以枚举出所有支持的币种,方便地指定要转换成哪种货币。
EnumerableFlag
允许我们定义一个命令行Flag参数,它可以接收多个值。通常Flag是Bool类型,表示开启或关闭某功能。而EnumerableFlag
可以视为一组相关的Flag。
我们最后再实现一个功能,可以选择是否把结果输出到某一个文件中。可以这样使用caculator rmb 100 --usd --output xx.excel
。
在RMB
结构体中添加如下代码:
swift
@Option(name: [.customLong("output")], help: "把结果输出到文件")
var outputFilePath: String?
swift
func run() async throws {
let rate = await getExchangeRate(with: currency)
let result = rmb * rate
if let output = outputFilePath {
//具体文件写入逻辑就不实现了
print("已输出到指定文件")
}
else {
print("\(rmb)人民币约等于\(result)\(currency.chinese())")
}
}
@Option 属性用于定义可选参数。与 @Argument 属性不同的是,@Option 属性对应的命令行参数前面需要使用 - 或 -- 前缀来标识。
调试
哇,已经写完这么多代码了,接下来我们该怎么调试呢?主要有两种调试方法。
Xcode调试
根据图片所示操作:
这种方法好处是可以断点调试,坏处是改参数不是很方便。
命令行调试法
首先 Build 一下项目,通过 Product -> **Show Build Folder in Finder **进入该项目的 Derived Data 的 Build/Products/Debug 目录,打开终端,执行类似命令./Caculator -h
,就跟实际使用时一样,只是把主命令名换成./Caculator。


这种方法好处是改参数快,更接近实际使用场景,坏处是只能使用打印语句来进行调试。
发布安装
终于到最后一步啦,写完验证完没问题,该怎么全局使用,并分享给小伙伴用呢?
选择 Product -> Archive,大概几秒后弹出一个页面。
选择 Distribute Content,按照步骤导出到某个路径,导出的文件夹是这样的:
把最终的二进制文件拷贝到系统的 /usr/local/bin/ 下,就能在任意目录使用啦。
可以把二进制文件直接发给小伙伴,或者分发到第三方的包管理器中,比如 Homebrew 或者 SPM,这个步骤就不赘述了,请自行查阅。
总结
能实现命令行的语言有很多,可以说是个编程语言基本上都可以,我觉得 Swift 是其中非常优雅现代化的,也是最适合iOS开发者的,而且也是经得起考验的,比如微信支付跨平台代码生成器就是通过 Swift 编写的。
有时候很难推动业务代码迁移到 Swift,那就从一些自动化小工具开始写起吧!
相关文档