使用 Swift 编写 CLI 工具的入门教程

2021年11月23日 阅读数:3
这篇文章主要向大家介绍使用 Swift 编写 CLI 工具的入门教程,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。


  • 概述前端

    • Why Scriptingreact

    • Why Swiftgit

  • 使用 SPM 搭建开发框架github

    • 项目里的文件web

    • 将代码划分为 framework 和 executable编程

    • 构建 Xcode 项目swift

  • 开始动手后端

    • 定义程序入口数组

    • Hello,Worldxcode

    • 增长依赖

    • 安装/更新依赖

    • 参数解析

    • Argument Parser

  • 编写单测

  • 安装工具

  • 调试技巧

  • 让开源社区加速你的开发

  • 总结

概述

最近在工做中过程当中参与了两款 Swift 命令行工具的开发::

  • Nezuko(未开源) ,一款面向美团壳工程的,类 create-react-app 的脚手架工具。
  • ImportSanitizer [1],一款可以修复不规范头文件引用方式的自动化工具。

在整个开发过程当中,咱们体会到了 Swift 带来的一些变化,固然这里既有好的,也有坏的,不过总的来讲,使用 Swift 进行脚本开发仍是一件让人愉悦的事情,因此咱们火烧眉毛的邀请你,也就是这篇文章的读者,和咱们一块儿加入 Swift 开发的大军中!

这篇文章经过 Step-By-Step 的方式,指导你完成一个基于 Swift 的命令行工具,不过在开始以前,咱们先聊聊为何要写脚本,以及为何选用 Swift。

Why Scripting

对于软件工程师来讲,咱们常常会遇到这样的工做,例如重命名设计师提供的图片素材,在海量的数据中提取一些特定信息。这种工做有一些共性,就是它的逻辑很简单,就像代码里的 if-else 同样,只要你够严谨,就必定能获得想要的答案,

但这种机械性的工做很容易由于人为的因素致使错误,例如手抖,眼瞎,以及间歇性失忆,哈哈,而这时候,机器会显得比人靠谱多了!

同时,重复一样的工做是一件低效的事情,咱们彻底能够经过编写相应的代码将任务自动化,这将极大的提高咱们的工做效率。

固然可能有人会说,我仍是本身弄吧,但你不以为这种重复的,机械性的工做很无聊么,咱们但是要改变世界的工程师啊!

Why Swift

至于为何使用 Swift 写脚本,我想有人可能会给出这样的答案:

swift 真的太棒了,我喜欢 Swift,它是最好的语言!我要用它写后端,要用它写前端,用它写 iOS,写 Android,用它写 CLI,总之,我要用它解决一切的编程问题!

但说实话,这是一个很主观的判断,并不该该成为咱们使用 Swift 写 CLI 的客观因素,在实际使用了一段时间后,我认为下面几个因素才是咱们使用 Swift 编写脚本的主要缘由:

  • 下降了 App 开发者编写脚本的门槛,减小了上下文切换的负担
  • Swift 提供了一些真的很不错的内置库,例如 combine,core graphics,urlsession 等
  • 能够将 App 里的代码引入脚本,避免重复工做

关于前两点,我想你们应该会比较好理解,毕竟每次在 Swift 代码和 Bash,Ruby,Python,JavaScript 中切换,会让我产生一种深深的抗拒感,另外在 iOS 上已经获得证实的 core graphics 有理由让咱们相信它的品质,同时自然内置的 Combine 让咱们在异步编程上有了很好的体验,这让我想起被 JavaScript 里 yield 支配的恐惧,问我为何要用 yield,不妨来试试 React + Redux + Redux-Saga 的大礼包呀!

关于最后一点,我想展开说说,一方面是由于它基于我真实的开发体验,另外一方面是它确实让我真正意识到 Swift 写脚本的优点所在。

在年初的时候,我曾经一度痴迷 SpriteKit,并尝试开发一款属于本身的横版过关游戏,这其中涉及到大量的图片处理,例如使用 Animation 的方式将多张静态图片整合成动态效果,大致的效果就像下面的 gif 同样

这背后的代码大概以下所示,经过读取相应顺序和数量的图片构建 gif 动画

Animation(
    name: "Units/swordsman/male/attack/right",
    frameCount: 8,
    duration: 1.12,
    tintColor: .blue
)

虽然看起来代码很少,手写一下就 ok 了,但你得知道,就这样一个杂兵角色,就会有攻击,跳跃,行走,跑动等等等动做,再加上各个方向,以及特殊效果,若是再考虑到,咱们的游戏里面大概有 20 多个兵种和 4 个英雄,我想你大概已经体会到这个工做的痛苦了!

在一开始,我用的是 Ruby 来解决重复代码的生成工做,但这里为了减小上下文切换,我将采用 Swift 类型的伪代码来作展现,方便你们快速理解

let unitKinds = ["swordsman""archer""knight""catapult"]

for kind in unitKinds {
    guard let config = tryFile(path: "\(kind.identifer)/Config"else {
        continue
    }
    try codeGenerator.generateCode(from: config)
}

虽然这么写已经帮我节省了很多时间,但我仍是得每次手动维护 unitKinds 数组,并保持它与游戏中的模型数据同步,这其实也挺烦人的,不是么?

class Unit {
    enum KindInt {
        case swordsman
        case archer
        case knight
        case catapult
    }
}

某日,看着上面的游戏模型数据,我忽然顿悟,若是我用 Swift 编写脚本的话,我彻底能够复用游戏里的数据模型啊!因而乎,便有了下面的代码:

import GameModels

for kind in EnumSequence<Unit.Kind>() {
    guard let config = tryFile(path: "\(kind.identifer)/Config"else {
        continue
    }
    try codeGenerator.generateCode(from: config)
}

至此,我终于作到了不用再手动维护脚本里的任何代码,就能够直接与 App 里的源码保持一致,这样是否是很 cool!一样的道理,咱们还能够应用到不少方面,例如直接利用网络层的 modle 文件生成 mock 数据,而不用在 JSON 编辑器上当心翼翼的粘贴复制了!

因此还在等什么呢,让咱们开始写一个 Swift CLI 吧!

使用 SPM 搭建开发框架

为了开发 command line tool(CLI),咱们须要建立一个新的文件夹,并使用 swift package manager(SPM)来初始化项目

$ mkdir CommandLineTool
cd CommandLineTool
$ swift package init --type executable

最后一行的 type executable 参数将告诉 SPM,咱们想建立一个 CLI,而不是一个 Framework。

项目里的文件

在 SPM 初始化项目后,咱们会获得以下的一个文件夹结构

.
├── Package.swift
├── README.md
├── Sources
│   └── CommandLineTool
│       └── main.swift
└── Tests
    ├── CommandLineToolTests
    │   ├── CommandLineToolTests.swift
    │   └── XCTestManifests.swift
    └── LinuxMain.swift

其中有几个须要关心的文件

  • Package.swift 文件: 用于描述当前 Package 的信息及其依赖,须要记住的是,在 SPM 的世界里,再也不有 Pod 的概念,与之对应概念是 Package,而 CLI 自己也是一个 Package
  • main.swift 文件:这个文件在 Sources 目录下,它表明整个命令行工具的入口,另外记住不要更换这个文件的名字!
  • Tests 文件夹:这个文件夹是用于放置测试代码的。
  • .gitignore 文件:经过这个文件,git 会自动忽略 SPM 生成的 build 文件夹( .build 目录)以及 Xcode Project

将代码划分为 framework 和 executable

个人一个我的建议是,在一开始就最好将源代码分红两个模块,一个是 framework 模块,一个是 executable 模块。

这样作的缘由有 2 点:

  • 会让测试变得更加容易
  • 让你的命令行工具也能够做为其余工具依赖的 Package

具体怎么作呢?

首先,咱们要保证 Sources 目录下有两个文件夹,一个用于存放 executable 相关的逻辑,一个用于存放 framework 相关的逻辑,就像下面同样:

cd Sources
$ mkdir CommandLineToolCore

SPM 的一个很是好的方面是,它使用文件系统做为它的处理依据,也就是说,只要采用上述操做提供的文件结构,就等于定义了两个模块。

紧接着,咱们在 Package.swift 里定义了两个target, 一个是 CommandLineTool 模块,一个是 CommandLineToolCore

import PackageDescription

let package = Package(
    name: "CommandLineTool",
    targets: [
        .target(
            name: "CommandLineTool",
            dependencies: ["CommandLineToolCore"]
        ),
        .target(name: "CommandLineToolCore")
    ]
)

经过上面这种方式,咱们让 executable 模块依赖了 framework 模块。

构建 Xcode 项目

为了能方便的运行和调试代码,咱们还须要使用配套的开发工具!

好消息是 SPM 能够根据文件信息自动建立 Xcode 工程,这意味着咱们可使用 Xcode 来开发 CLI 了。

并且在 .gitignore 中会自动忽略这个工程项目,这同时意味着,咱们不须要更新 Xcode Project 文件,也不须要担忧这类文件的冲突问题,只须要经过下面的命令便可完成工程文件的生成。

$ swift package generate-xcodeproj

记得须要在根目录下执行上面的命令,另外在执行过程当中会获得一个 warning,让咱们暂且忽略它,在后面咱们会将其修复!

开始动手

定义程序入口

为了可以在命令行和测试用例中方便的运行咱们的代码,咱们最好不要在 main.swift 中添加过多的逻辑,而是经过程序调用的方式唤起 framework 中的主逻辑。

为了实现这样的目的,咱们须要建立一个名为 CommandLineTool.swift 的文件,将其放在 framework 模块中(Sources/CommandLineToolCore),它里面的内容以下

import Foundation

public final class CommandLineTool {
    private let arguments: [String]

    public init(arguments: [String] = CommandLine.arguments) { 
        self.arguments = arguments
    }

    public func run() throws {
        print("Hello world")
    }
}

同时在 main.swift 中添加 run() 方法

import CommandLineToolCore

let tool = CommandLineTool()

do {
    try tool.run()
catch {
    print("Whoops! An error occurred: \(error)")
}

Hello,World

让咱们用命令行看看执行效果吧!不在在真正的运行前,咱们还须要完成编译工做,让咱们在根目录下执行 swift build 吧,而后再执行 swift run 命令。

$ swift build
$ swift run
> Hello world

咱们其实能够经过直接调用 swift run 命令来达到运行程序的目的,由于若是须要的话,它会自动编译咱们的项目,但学习一下底层命令的工做原理老是有益的。

增长依赖

除非你正在构建一些十分“特殊”的东西,不然你会发现本身须要为你的命令行工具添加一些依赖关系,毕竟有好用的轮子为啥不用呢?

任何 Swift Package 均可以被添加为依赖项,只需在 Package.swift 中指定它。

import PackageDescription

let package = Package(
    name: "CommandLineTool",
    dependencies: [
        .package(
            name: "Files",
            url: "https://github.com/johnsundell/files.git",
            from: "4.2.0"
        )
    ],
    targets: [
        .target(
            name: "CommandLineTool",
            dependencies: ["CommandLineToolCore"]
        ),
        .target(
            name: "CommandLineToolCore",
            dependencies: ["Files"])
    ]
)

上面我添加了对 Files 组件的依赖,它可让咱们在 Swift 中轻松处理文件和文件夹的相关操做。在后面的教程中,咱们将使用它在当前文件夹中建立一个文件。

安装/更新依赖

一旦咱们声明了新的依赖关系,只须要求 SPM 解析新的依赖关系并安装它们,而后从新生成 Xcode 项目便可。

$ swift package update
$ swift package generate-xcodeproj

参数解析

让咱们修改一下 CommandLineTool.swift 里的内容。

将其从打印 Hello, World 的逻辑变为根据命令行参数建立文件的逻辑

import Foundation
import Files

public final class CommandLineTool {
    private let arguments: [String]

    public init(arguments: [String] = CommandLine.arguments) { 
        self.arguments = arguments
    }

    public func run() throws {
        guard arguments.count > 1 else {
            throw Error.missingFileName
        }
        
        // The first argument is the execution path
        let fileName = arguments[1]

        do {
            try Folder.current.createFile(at: fileName)
        } catch {
            throw Error.failedToCreateFile
        }
    }
}

public extension CommandLineTool {
    enum ErrorSwift.Error {
        case missingFileName
        case failedToCreateFile
    }
}

如上所述,咱们把对 Folder.current.createFile() 的调用包装在本身的 do、try、catch 中,以便为用户提供统一的,自定义的错误 API。

Argument Parser

除了刚才提到的参数解析方式,Apple 官方还提过了一个更优化的解决方案 - Swift Argument Parser[2]

这里咱们作一下简单的介绍,以官方代码为参考示例:

import ArgumentParser

struct RepeatParsableCommand {
    @Flag(help: "Include a counter with each repetition.")
    var includeCounter = false

    @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
    var countInt?

    @Argument(help: "The phrase to repeat.")
    var phrase: String

    mutating func run() throws {
        let repeatCount = count ?? .max

        for i in 1...repeatCount {
            if includeCounter {
                print("\(i): \(phrase)")
            } else {
                print(phrase)
            }
        }
    }
}

Repeat.main()

咱们能够看到,它的使用方式并不麻烦。

  • 首先遵照 ParsableCommand 协议
  • 其次声明一个参数类型(Flag,Option,Argument),定义你须要从命令行中收集的信息,并用 ArgumentParser 的属性包装器来装饰每一个存储属性
  • 最后在 run() 方法中实现核心逻辑。

在实际的运行过程当中,ArgumentParser 会解析命令行参数,实例化你的命令类型。同时 ArgumentParser 会使用属性名,类型信息,以及你在属性包装器里提供的细节,来提供有用的错误信息和帮助信息,具体效果以下所示。

$ repeat hello --count 3
hello
hello
hello
$ repeat --count 3
Error: Missing expected argument 'phrase'.
Usage: repeat [--count <count>] [--include-counter] <phrase>
  See 'repeat --help' for more information.
$ repeat --help
USAGE: repeat [--count <count>] [--include-counter] <phrase>

ARGUMENTS:
  <phrase>                The phrase to repeat.

OPTIONS:
  --include-counter       Include a counter with each repetition.
  -c, --count <count>     The number of times to repeat 'phrase'.
  -h, --help              Show help for this command.

因为本文的例子较为简单,咱们这里就不增长 ArgumentParser 的依赖来增长项目的复杂度了。

即便如此,我相信经过上面的介绍,你也大体了解到了 ArgumentParser 的使用方式了,记得将其用在你本身的项目中吧!

编写单测

咱们几乎已经准备好发布这个命令行工具了,但在这样作以前,咱们仍是须要经过编写一些测试来确保它真正的按照预期工做。

因为咱们以前将整个项目划分红了 framework 和 executable 的结果,因此测试将变得十分容易。咱们所要作的就是以程序调用的方式运行它,并断言它建立了一个具备指定名称的文件。

首先在 Package.swift 文件中添加一个测试模块,在你的 target 数组中添加如下内容。

.testTarget(
    name: "CommandLineToolTests",
    dependencies: ["CommandLineToolCore""Files"]
)

最后,从新生成 Xcode 项目。

$ swift package generate-xcodeproj

再次打开 Xcode 项目,跳到 CommandLineToolTests.swift 中,添加如下内容。

import Foundation
import XCTest
import Files
import CommandLineToolCore

class CommandLineToolTestsXCTestCase {
    func testCreatingFile() throws {
        // Setup a temp test folder that can be used as a sandbox
        let tempFolder = Folder.temporary
        let testFolder = try tempFolder.createSubfolderIfNeeded(
            withName: "CommandLineToolTests"
        )

        // Empty the test folder to ensure a clean state
        try testFolder.empty()

        // Make the temp folder the current working folder
        let fileManager = FileManager.default
        fileManager.changeCurrentDirectoryPath(testFolder.path)

        // Create an instance of the command line tool
        let arguments = [testFolder.path, "Hello.swift"]
        let tool = CommandLineTool(arguments: arguments)

        // Run the tool and assert that the file was created
        try tool.run()
        XCTAssertNotNil(try? testFolder.file(named: "Hello.swift"))
    }
}

此外,还能够添加另外一个测试,以验证在没有给定文件名或文件建立失败时是否抛出了正确的错误。

要运行测试,只需在命令行上运行 swift test 便可。

安装工具

如今咱们已经构建并测试了咱们的命令行工具!下面开始,咱们会尝试安装它,并使它可以在任何地方运行。

要作到这一点,须要在 swift build 后面增长 release 的配置,也就是 -c relase 参数,而后将编译后的二进制文件移到 /usr/local/bin

$ swift build -c release
cd .build/release
$ cp -f CommandLineTool /usr/local/bin/commandlinetool

调试技巧

命令行大可能是须要输入参数的,因此在实际的开发过程当中,咱们如何在 Xcode 里添加入参呢?

首先,在 Xcode 的 Toolbar 中,咱们点击 choose scheme 面板中的 Edit Scheme... 按钮

在弹出的界面中点击左侧 Run 面板,并继续点击右侧的 Argument 的 Tab 按钮,咱们会看到以下的界面,此时咱们能够在 Arguments Passed On Launch 中添加命令行所需的参数,例如这里咱们添加了一个 Hello.swift 的参数。

此时,咱们再次经过 CMD+R 的方式运行程序,就会在构建产物的目录中,看到生成的 Hello.swift 文件

让开源社区加速你的开发

除了 Foundation 自带的 Combine,CoreGraphes,URLSession 等,在官方的技术社区还有不少不错的组件可以加速你的开发,例如

  • Swift Tool Support Core [3]:SPM 和 llbuild 里通用的基础架构代码,能够将其看作是 Foundation 在 CLI 方向的增强库。
  • Swift NIO [4]:若是 URLSession 不能知足你的需求或者你须要进行跨平台开发,SwiftNIO 这个网络库应该是能知足你的诉求,它是一个跨平台异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。
  • Swift Log [5]:一个用于作日志记录工做的组件。
  • Swift Metrics [6]:当咱们须要为某个系统,某个服务作监控,作统计的时候,Swift Metrics 就是你的不二之选!
  • Swift Crypto [7]:一个跨平台的加密库,基于 Apple 自身的 CryptoKit 改造而来。
  • Swift numerics [8]:这个库为 Swift 提供了许多与数值计算相关的功能模块。
  • Swift Protobuf [9]:若是你在网络通讯的过程当中传输的是 Protobuf 类型的文件,能够经过这个库进行解析
  • Swift Atomics [10]:为各类 Swift 类型提供原子操做,包括整数和指针值。
  • Swift Backtrace [11]:这个 Package 为项目提供了自动打印程序崩溃信息的能力。

总之,这个社区在不断的发展中,不少新的官方库也在如火如荼的建设中,若是你发现这里的内容还不够用,能够关注一下 Vapor[12]PerfectlySoft Inc[13]SwiftWasm[14]Crossroad Labs[15] 的 Group,也能找到不少不错的 package 资源。

固然,也有不少我的开发者提供了不错的 Package 资源,例如:

  • Guitar [16]:这绝对会是你在开发中须要到的东西,一个正则匹配增强库!
  • Rainbow [17]:能够对命令行里的输出内容增长文本颜色,背景样式等。
  • SwiftShell [18]:能够在 Swift 里调用 Shell 命令的 Package
  • Swift-SH [19]:一样是一个 Swift 里调用 Shell 的 Package,介绍它的缘由是由于它的做者是 Homebrew 的开发者 mxcl
  • Files [20]:与文件操做相关的 Package,在教程里已经说起过。
  • Path [21]:Files 在处理一些路径上仍是有短板,mxcl 开发的这个组件很好的补充了 Files 的功能。
  • Release [22]:能够经过 Swift 脚本或命令行工具轻松解析 Git 仓库中的发布版本,支持远程仓库和本地仓库。
  • XGen [23]:经过 Swift 脚本或命令行工具中轻松地生成 Xcode Project 和 Playground。

总结

经过这篇文章,你已经掌握了如何从零编写 Swift CLI 项目的全部基础知识,也了解了社区里的一些优秀资源,十分期待你开始使用 Swift 编写属于本身的命令行工具!

参考资料

[1]

ImportSanitizer: https://github.com/SketchK/import-sanitizer

[2]

Swift Argument Parser: https://github.com/apple/swift-argument-parser

[3]

Swift Tool Support Core: https://github.com/apple/swift-tools-support-core

[4]

Swift NIO: https://github.com/apple/swift-nio

[5]

Swift Log: https://github.com/apple/swift-log

[6]

Swift Metrics: https://github.com/apple/swift-metrics

[7]

Swift Crypto: https://github.com/apple/swift-crypto

[8]

Swift numerics: https://github.com/apple/swift-numerics

[9]

Swift Protobuf: https://github.com/apple/swift-protobuf

[10]

Swift Atomics: https://github.com/apple/swift-atomics

[11]

Swift Backtrace: https://github.com/swift-server/swift-backtrace

[12]

Vapor: https://github.com/vapor

[13]

PerfectlySoft Inc: https://github.com/PerfectlySoft

[14]

SwiftWasm: https://github.com/swiftwasm

[15]

Crossroad Labs: https://github.com/crossroadlabs

[16]

Guitar: https://github.com/artsabintsev/guitar

[17]

Rainbow: https://github.com/onevcat/Rainbow

[18]

SwiftShell: https://github.com/kareman/SwiftShell

[19]

Swift-SH: https://github.com/mxcl/swift-sh

[20]

Files: https://github.com/JohnSundell/Files

[21]

Path: https://github.com/mxcl/Path.swift

[22]

Release: https://github.com/johnsundell/releases

[23]

XGen: https://github.com/johnsundell/xgen


本文分享自微信公众号 - 一瓜技术(tech_gua)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。