手把手教你用 Swift 实现命令行工具

为什么使用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 的argparseArgumentParser有如下优点:

  • 使用 Swift 的强类型系统,可以在编译时捕获许多错误。
  • 支持复杂的命令行接口,包括子命令、标志、选项等。
  • 能自动生成更详细且格式良好的帮助信息。
  • 声明式的语法,更简单清晰。

核心逻辑

我们主要实现两个功能:

  1. 输入若干个数,输出相加的结果,并能够选择是否使用16进制输出。使用起来是这样caculator add 0 1 2 -x
  2. 输入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 DataBuild/Products/Debug 目录,打开终端,执行类似命令./Caculator -h,就跟实际使用时一样,只是把主命令名换成./Caculator。

这种方法好处是改参数快,更接近实际使用场景,坏处是只能使用打印语句来进行调试。

发布安装

终于到最后一步啦,写完验证完没问题,该怎么全局使用,并分享给小伙伴用呢?

选择 Product -> Archive,大概几秒后弹出一个页面。

选择 Distribute Content,按照步骤导出到某个路径,导出的文件夹是这样的:

把最终的二进制文件拷贝到系统的 /usr/local/bin/ 下,就能在任意目录使用啦。

可以把二进制文件直接发给小伙伴,或者分发到第三方的包管理器中,比如 Homebrew 或者 SPM,这个步骤就不赘述了,请自行查阅。

总结

能实现命令行的语言有很多,可以说是个编程语言基本上都可以,我觉得 Swift 是其中非常优雅现代化的,也是最适合iOS开发者的,而且也是经得起考验的,比如微信支付跨平台代码生成器就是通过 Swift 编写的。

有时候很难推动业务代码迁移到 Swift,那就从一些自动化小工具开始写起吧!

相关文档

ArgumentParser 源码和相关文档

Swift 结构化并发

相关推荐
美狐美颜sdk39 分钟前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
郭庆汝1 小时前
pytorch、torchvision与python版本对应关系
人工智能·pytorch·python
思则变4 小时前
[Pytest] [Part 2]增加 log功能
开发语言·python·pytest
漫谈网络5 小时前
WebSocket 在前后端的完整使用流程
javascript·python·websocket
恋猫de小郭5 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
泓博6 小时前
Objective-c把字符解析成字典
开发语言·ios·objective-c
try2find6 小时前
安装llama-cpp-python踩坑记
开发语言·python·llama
Daniel_Coder7 小时前
Xcode 中常用图片格式详解
ios·xcode·swift
博观而约取7 小时前
Django ORM 1. 创建模型(Model)
数据库·python·django
瓜子三百克7 小时前
Objective-C 路由表原理详解
开发语言·ios·objective-c