Swift Package Manager (SwiftPM) で作るコマンドラインツール

Swift 3でSwift本体も安定してきて、Swift Package Manager (SwiftPM) もライブラリ配布・コマンドラインツール作成などには充分使える状態だと思っています。

ただ、SwiftPM を使ってコマンドラインツールを作るまとまった説明があまり出回っていなくて、その意味で少し敷居が高いかな？と思ったので一通りの手順を書いてみます。

# コマンドラインツールのお題

説明のためのサンプルとして、[MonoGenerator](https://github.com/mono0926/MonoGenerator)を作りました。

```console
$ MonoGenerator おはようございます
→ おはようございます(　´･‿･｀)
$ MonoGenerator おはようございます --suffix （｀・ω・´）
→ おはようございます（｀・ω・´）
$ MonoGenerator おはようございます --suffix （｀・ω・´） --length 10
→ おはようございます（
```

- 入力された文字列の最後に「(　´･‿･｀)」を付けて出力
- `--suffix`オプションを付けると、末尾に付ける文字列を変更可能
- `--length`オプションを付けると、その文字数以下になるようにカット
    - Twitterの140文字以下に調整、などの用途

```console
$ MonoGenerator とても便利ですね
→ とても便利ですね(　´･‿･｀)
```

# SwiftPMでのコマンドラインツール作成手順

## Swiftパッケージディレクトリ作成

今回の場合、`MonoGenerator`という名前なので、その名前のディレクトリを作って、そのルートで以下を実行します。

```console
$ swift package init --type executable
```

`--type`は他に以下を指定でき、フレームワーク(ライブラリ)を作る場合は、`library`が良いでしょう。

- `empty`
- `library`
- `system-module`

今回は`executable`を指定したので、以下のように`Sources/main.swift`(エントリーポイントとなるファイル)とPackage.swiftが生成されます。

```
MonoGenerator/
  Package.swift
  Sources/
    main.swift
```

`main.swift`の中身はこうなっています。

```main.swift
print("Hello, world!")
```

以下のコマンドを実行すると、`Hello, world!`と出力されます👏

```console
$ swift build
$ .build/debug/MonoGenerator
```

ちなみに、リリースビルドしたい場合は、`$ swift build -c release`です。
参考: [swift-package-manager/Reference.md at master · apple/swift-package-manager](https://github.com/apple/swift-package-manager/blob/master/Documentation/Reference.md#customizing-builds)

## 実装ファイルを追加

`main.swift`にベタに書いても良いですが、別ファイルに書きつつそのテストも書いてみます。
`Sources`配下に`Generator.swift`という実装クラスと、`Tests`配下にそれに対応するテストクラスの`GeneratorTests.swift`を作成します。
SwiftPMは規約ベースになっていて、テストファイルは`Tests`配下に、`Tests.swift`で終わる名前で配置する必要があります。

```
MonoGenerator/
  Package.swift
  Sources/
    main.swift
    Generator.swift 🆕
  Tests/
    GeneratorTests.swift 🆕
```

あるいは、`--type library`で生成すると、これらのファイルが自動生成されるので、その後`main.swift`を手で追加、という方が楽かもしれません。

## Xcodeプロジェクトを生成

ここから軽くコーディングするわけですが、やはり慣れているXcodeが補完なども効いて良いです。
(他のテキストエディタでもプラグインなど工夫して同じくらい快適にコーディング出来るように整えられればそれでも良いと思っています。)

以下のコマンドを実行すると、ディレクトリ構成などを解釈して、良い感じのXcodeプロジェクトファイルを生成してくれます。

```console
$ swift package generate-xcodeproj
```


<img width="228" alt="Screen Shot 2016-11-17 at 10.28.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/19398/a701ffab-f73e-1855-e9cf-8ab6c4c4ef17.png">


`swift package init`実行時に自動生成された`.gitignore`は以下のようになっていて、デフォルトではXcodeプロジェクトファイルはバージョン管理外です。

```.gitignore
.DS_Store
/.build
/Packages
/*.xcodeproj
```

[swift-evolution/0082-swiftpm-package-edit.md at master · apple/swift-evolution](https://github.com/apple/swift-evolution/blob/master/proposals/0082-swiftpm-package-edit.md)

あくまでフォルダ構成やファイルがソースが揃っていれば良く、Xcodeプロジェクトファイルは必要に応じて手元で生成すれば良い、という扱いなのだと思います。

## ロジックを記述

冒頭に書いたコマンドラインツールの要件を満たせるようなメソッドと、そのテストを記述します。
(このソース自体はこの記事の本筋では無いので、細かい微妙な点はご容赦ください。)

```Generator.swift
import Foundation

class Generator {
    let value: String
    init(value: String) {
        self.value = value
    }

    func generate(suffix: String = "(　´･‿･｀)", maxLength: Int? = nil) -> String {
        let r = value + suffix
        guard let maxLength = maxLength else {
            return r
        }
        // こうも書けるけどprefixの方がベター
//        return r[r.startIndex..<r.index(r.startIndex, offsetBy: min(maxLength, r.characters.count))]
        return String(r.characters.prefix(maxLength))
    }
}
```

```GeneraterTests.swift
import XCTest
@testable import MonoGenerator

class GeneratorTests: XCTestCase {
    private var target: Generator!

    override func setUp() {
        super.setUp()
        target = Generator(value: "value")
    }

    func testGenerate() {
        XCTAssertEqual(target.generate(), "value(　´･‿･｀)")
    }

    func testGenerate_suffix() {
        XCTAssertEqual(target.generate(suffix: "(　´･‿･｀)🐶🍎📱"), "value(　´･‿･｀)🐶🍎📱")
    }

    func testGenerate_maxCount() {
        XCTAssertEqual(target.generate(maxLength: 6), "value(")
        XCTAssertEqual(target.generate(maxLength: 100), "value(　´･‿･｀)")
    }

}
```

## テストを実行

Xcodeで`command + U`で実行出来ると快適なのですが、実行ファイル形式(`main.swift`を含むもの)に対しては、現状ではXcode上でテスト実行出来なさそうです。

まず、以下のコンパイルエラーが発生してしまいます。

```
GeneratorTests.swift:10:18: Module 'MonoGenerator' was not compiled for testing
```

プロジェクト設定で、`Enable Testability`を`YES`にすると、これは解決しますが、その後次のコンパイルエラーが発生してしまいます。

```
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

色々がんばればもしかしたら解決するかもしれませんが、そもそも、Xcodeプロジェクトは`swift package generate-xcodeproj`でカジュアルに再生成するもので、それに対してあとから設定変えてもまた吹き飛んでしまったりして良くないと思っています。

ただ、ターミナルから以下のコマンドを実行してテストすることは問題無く出来ますので、それで我慢しています。

```console
$ swift test
```

あるいは、ライブラリ(`--type library`)の場合は問題無くXcodeでテスト実行出来るので、ロジックはライブラリとして別リポジトリに用意(SwiftPMは1リポジトリ・1ライブラリ構成)して、コマンドラインツール側はそれを呼び出すだけの薄っぺらい構成にする、というのも良いと思います。

### 【追記】Sources配下で別ディレクトリに分けるという、以下に記載の方法が良さそうです

[Building a command line tool using the Swift Package Manager](https://medium.com/@johnsundell/building-a-command-line-tool-using-the-swift-package-manager-3dd96ce360b1)

## 簡単なコマンドラインツールを自前実装

まずは簡単に、オプションなど無しで、以下の要件だけ満たすシンプルな作りで組んでみます。

> 入力された文字列の最後に「(　´･‿･｀)」を付けて出力

```main.swift
private func main(arguments: [String]) {
    let arguments = arguments.dropFirst()
    guard let input = arguments.first else {
        print("inputの引数を入力してください😡")
        return
    }
    let generator = Generator(value: input)
    print(generator.generate())
}

main(arguments: CommandLine.arguments)
```

簡単ですね(　´･‿･｀)
もうこれで、以下の動作をするコマンドラインツールが出来ました🎉

```console
$ MonoGenerator おはようございます
→ おはようございます(　´･‿･｀)
```

次に、各種オプションを追加していくわけですが、入力をパースする処理ってけっこう面倒なので、このあたりからライブラリ利用すると良い気がしてきます :thinking: 

## 外部ライブラリの導入

というわけで、コマンドラインツール用のライブラリである[kylef/Commander: Compose beautiful command line interfaces in Swift](https://github.com/kylef/Commander)を導入していきます。

以下のように、`dependencies`の項目を追加して、CommanderのGit URLである`https://github.com:kylef/Commander.git`を指定します。

```Package.swift
import PackageDescription

let package = Package(
    name: "MonoGenerator",
    dependencies: [
        .Package(url: "https://github.com:kylef/Commander.git",
                 majorVersion: 0),
        ]
)
```

さらに、`swift build`をすると依存パッケージが取り込まれつつリビルドが走ります。
(単純に依存パッケージ更新したいだけであれば`swift package update`でも良いです。)

Packages配下にCommanderとさらにそれが依存しているSpectreが追加されます。

```
MonoGenerator/
  Package.swift
  Sources/
    main.swift
    Generator.swift
  Packages/
    Commander-0.5.0/ 🆕
    Spectre-0.7.2/ 🆕
  Tests/
    GeneratorTests.swift
```

このままではXcodeからは認識されないので、`swift package generate-xcodeproj`で再生成すると、Commanderをimportして利用可能になります。

## Commanderの利用

`main.swift`を次のように、Commanderを利用して、さらにオプション引数対応させました。
詳しい使い方は、[CommanderのREADME](https://github.com/kylef/Commander/blob/master/README.md)を参照してください。


```main.swift
import Commander

let main = command(Argument<String>("input"),
                   Option("suffix", "(　´･‿･｀)", flag: "s"),
                   Option("length", -1, flag: "l")) { input, suffix, length in
                    let generator = Generator(value: input)
                    print(generator.generate(suffix: suffix, maxLength: length < 0 ? nil : length))
}

main.run()
```

`swift build`すると、以下を実行出来るようになりました :tada: 

```console
$ MonoGenerator おはようございます
→ おはようございます(　´･‿･｀)
$ MonoGenerator おはようございます --suffix （｀・ω・´）
→ おはようございます（｀・ω・´）
$ MonoGenerator おはようございます --suffix （｀・ω・´） --length 10
→ おはようございます（
```

### 他のコマンドラインライブラリ

初めは愛用している`Carthage`配下のライブラリということもあり、[Carthage/Commandant: Type-safe command line argument handling](https://github.com/Carthage/Commandant)を使おうと思いました。
ただ、以下のように微妙に思う点があって、使いやすいと感じたCommanderに変えました。

- READMEがメンテナンスされてないため、使い方を把握するために色々ソースを追う必要があった
- 使い方がけっこう難しい

> 使い方がけっこう難しい

関数型ぽい技巧的な作りになっていて、コマンドラインツール作る概ね簡単な要件に対して、学習コストが高く感じました。
技巧的な作りになっているメリットが把握出来なかったのですが、それが分かれば再評価するかもしれません(README・ドキュメント欲しい…)。

READMEのサンプルコードのスクショ:
<img width="880" alt="Screen Shot 2016-11-17 at 11.14.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/19398/d923e872-1bc0-4d2e-f979-b4db5b5b412a.png">

また、今回使わなかったですが、対話式にする場合、[oarrabi/Swiftline: Swiftline is a set of tools to help you create command line applications.](https://github.com/oarrabi/Swiftline)が便利そうです。

# Ubuntuで実行

単にMacで動くコマンドラインツールであれば、普通Xcodeで以下のように新規プロジェクト作る方が慣れていて簡単で、SwiftPM使ったメリットが現状薄いです。

<img width="769" alt="Screen Shot 2016-11-17 at 11.18.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/19398/af6b5b34-1320-446b-571b-944078f9d1d5.png">

というわけで、Ubuntuでも動かしてみましょう。

[🍂Swiftレター #7🍂](https://medium.com/swift-column/issue7-280f5a077335#.rvznblhv5)でも触れましたが、Ubuntuの14・15系のサポートが切れたようなので、16.04で試してみました。

1. [Download Ubuntu Desktop | Download | Ubuntu](https://www.ubuntu.com/download/desktop)から`Ubuntu 16.04.1 LTS`をダウンロード
- 仮想マシンなどでUbuntuを立ち上げる
    - 僕はParallesを使いました
- [Swift.org - Download Swift](https://swift.org/download/)からUbuntu 16.04版のSwiftをダウンロードして適当なパスに配置
- [Swift.org - Getting Started](https://swift.org/getting-started/)のLinuxの項目に従う
    - `$ sudo apt-get install clang`
    - `$ export PATH=/path/to/Swift/usr/bin:"${PATH}"`
- SwiftPMで利用するので、Gitをインストール
    - `$ sudo apt install git` 
    - ssh周りも適当に設定してGitHubリポジトリをクローン可能にしておく
- 今回作ったMonoGeneratorをクローン・ビルド
    - `$ git clone https://github.com:mono0926/MonoGenerator.git`
    - `$ swift build`

そうして、Macと同じく無事にコマンド実行出来るようになりました🎉

## Linux上でのテスト実行対応

Linuxでテストを拾えるように、`Tests`配下に`LinuxMain.swift`を配置する必要があります。

```LinuxMain.swift
import XCTest
@testable import MonoGeneratorTests

XCTMain([
     testCase(GeneratorTests.allTests),
])
```

合わせて、`GeneratorTests.swift`にも、実行対象のテストを羅列しておきます(ちょっと面倒ですね)。

```GeneratorTests.swift
import XCTest
@testable import MonoGenerator

class GeneratorTests: XCTestCase {
    static var allTests : [(String, (GeneratorTests) -> () throws -> Void)] {
        return [
            ("testGenerate", testGenerate),
            ("testGenerate_suffix", testGenerate_suffix),
            ("testGenerate_maxCount", testGenerate_maxCount),
        ]
    }
    // 以下略
```

これで普通はいけるはずですが、今回うまく出来ず…。
僕の書き方のどこかが悪いことを疑っていますが、Ubuntu上で`$ swift test`を実行すると、次のエラーが出てしまいました(´・︵・｀)
まだあまり調べて無いのですが、分かったら更新します。

【追記】
現状の制約のようです: [[SR-1503] Can't test module under linux that has main.swift in it - Swift](https://bugs.swift.org/browse/SR-1503)

> あるいは、ライブラリ(`--type library`)の場合は問題無くXcodeでテスト実行出来るので、ロジックはライブラリとして別リポジトリに用意(SwiftPMは1リポジトリ・1ライブラリ構成)して、コマンドラインツール側はそれを呼び出すだけの薄っぺらい構成にする、というのも良いと思います。

やはり、この構成が良さそうです 🤔


```console
mono@ubuntu:~/Desktop/temp/MonoGenerator$ swift test
Cloning https://github.com:kylef/Commander.git
Enter passphrase for key '/home/mono/.ssh/id_rsa': 
Enter passphrase for key '/home/mono/.ssh/id_rsa': 
HEAD is now at 03ae17a Release 0.5.0
Resolved version: 0.5.0
Cloning https://github.com/kylef/Spectre
HEAD is now at e46b75c chore: Release 0.7.2
Resolved version: 0.7.2
Compile Swift Module 'Spectre' (8 sources)
Compile Swift Module 'Commander' (9 sources)
Compile Swift Module 'MonoGenerator' (2 sources)
Compile Swift Module 'MonoGeneratorTests' (1 sources)
Linking ./.build/debug/MonoGenerator
Linking ./.build/debug/MonoGeneratorPackageTests.xctest
/usr/bin/ld.gold: error: /home/mono/Desktop/temp/MonoGenerator/.build/debug/MonoGenerator.build/main.swift.o: multiple definition of 'main'
/usr/bin/ld.gold: /tmp/LinuxMain-6b834d.o: previous definition here
clang: error: linker command failed with exit code 1 (use -v to see invocation)
<unknown>:0: error: link command failed with exit code 1 (use -v to see invocation)
<unknown>:0: error: build had 1 command failures
```

---

というわけで、多少躓いた箇所もありながら、充分実用的・快適にSwiftPMでコマンドラインツール作れる状態であると思っています。
コマンドラインツールは、Go・Python・Rubyなど適材適所で使い分けていくのが良いと思ってはいますが、Swiftで良い要件なら慣れていてかつ一番好きな言語でもあるので、Swiftでガシガシ書いていきたいなと思っています(　´･‿･｀)

# 参考リンク

- [apple/swift-package-manager: The Package Manager for the Swift Programming Language](https://github.com/apple/swift-package-manager)
- [実践 Swift Package Manager // Speaker Deck](https://speakerdeck.com/ikesyo/shi-jian-swift-package-manager)
- [iOSアプリのライブラリ依存管理ツールとして Swift Package Managerを使うのは まだしばらく先かなと思った話](http://www.slideshare.net/mono0926/ios-swift-package-manager)