Swift 3でSwift本体も安定してきて、Swift Package Manager (SwiftPM) もライブラリ配布・コマンドラインツール作成などには充分使える状態だと思っています。
ただ、SwiftPM を使ってコマンドラインツールを作るまとまった説明があまり出回っていなくて、その意味で少し敷居が高いかな?と思ったので一通りの手順を書いてみます。
コマンドラインツールのお題
説明のためのサンプルとして、MonoGeneratorを作りました。
$ MonoGenerator おはようございます
→ おはようございます( ´・‿・`)
$ MonoGenerator おはようございます --suffix (`・ω・´)
→ おはようございます(`・ω・´)
$ MonoGenerator おはようございます --suffix (`・ω・´) --length 10
→ おはようございます(
- 入力された文字列の最後に「( ´・‿・`)」を付けて出力
-
--suffix
オプションを付けると、末尾に付ける文字列を変更可能 -
--length
オプションを付けると、その文字数以下になるようにカット- Twitterの140文字以下に調整、などの用途
$ MonoGenerator とても便利ですね
→ とても便利ですね( ´・‿・`)
SwiftPMでのコマンドラインツール作成手順
Swiftパッケージディレクトリ作成
今回の場合、MonoGenerator
という名前なので、その名前のディレクトリを作って、そのルートで以下を実行します。
$ 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
の中身はこうなっています。
print("Hello, world!")
以下のコマンドを実行すると、Hello, world!
と出力されます👏
$ swift build
$ .build/debug/MonoGenerator
ちなみに、リリースビルドしたい場合は、$ swift build -c release
です。
参考: swift-package-manager/Reference.md at master · apple/swift-package-manager
実装ファイルを追加
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プロジェクトファイルを生成してくれます。
$ swift package generate-xcodeproj
swift package init
実行時に自動生成された.gitignore
は以下のようになっていて、デフォルトではXcodeプロジェクトファイルはバージョン管理外です。
.DS_Store
/.build
/Packages
/*.xcodeproj
swift-evolution/0082-swiftpm-package-edit.md at master · apple/swift-evolution
あくまでフォルダ構成やファイルがソースが揃っていれば良く、Xcodeプロジェクトファイルは必要に応じて手元で生成すれば良い、という扱いなのだと思います。
ロジックを記述
冒頭に書いたコマンドラインツールの要件を満たせるようなメソッドと、そのテストを記述します。
(このソース自体はこの記事の本筋では無いので、細かい微妙な点はご容赦ください。)
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))
}
}
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
でカジュアルに再生成するもので、それに対してあとから設定変えてもまた吹き飛んでしまったりして良くないと思っています。
ただ、ターミナルから以下のコマンドを実行してテストすることは問題無く出来ますので、それで我慢しています。
$ swift test
あるいは、ライブラリ(--type library
)の場合は問題無くXcodeでテスト実行出来るので、ロジックはライブラリとして別リポジトリに用意(SwiftPMは1リポジトリ・1ライブラリ構成)して、コマンドラインツール側はそれを呼び出すだけの薄っぺらい構成にする、というのも良いと思います。
【追記】Sources配下で別ディレクトリに分けるという、以下に記載の方法が良さそうです
Building a command line tool using the Swift Package Manager
簡単なコマンドラインツールを自前実装
まずは簡単に、オプションなど無しで、以下の要件だけ満たすシンプルな作りで組んでみます。
入力された文字列の最後に「( ´・‿・`)」を付けて出力
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)
簡単ですね( ´・‿・`)
もうこれで、以下の動作をするコマンドラインツールが出来ました🎉
$ MonoGenerator おはようございます
→ おはようございます( ´・‿・`)
次に、各種オプションを追加していくわけですが、入力をパースする処理ってけっこう面倒なので、このあたりからライブラリ利用すると良い気がしてきます
外部ライブラリの導入
というわけで、コマンドラインツール用のライブラリであるkylef/Commander: Compose beautiful command line interfaces in Swiftを導入していきます。
以下のように、dependencies
の項目を追加して、CommanderのGit URLであるhttps://github.com:kylef/Commander.git
を指定します。
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を参照してください。
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
すると、以下を実行出来るようになりました
$ MonoGenerator おはようございます
→ おはようございます( ´・‿・`)
$ MonoGenerator おはようございます --suffix (`・ω・´)
→ おはようございます(`・ω・´)
$ MonoGenerator おはようございます --suffix (`・ω・´) --length 10
→ おはようございます(
他のコマンドラインライブラリ
初めは愛用しているCarthage
配下のライブラリということもあり、Carthage/Commandant: Type-safe command line argument handlingを使おうと思いました。
ただ、以下のように微妙に思う点があって、使いやすいと感じたCommanderに変えました。
- READMEがメンテナンスされてないため、使い方を把握するために色々ソースを追う必要があった
- 使い方がけっこう難しい
使い方がけっこう難しい
関数型ぽい技巧的な作りになっていて、コマンドラインツール作る概ね簡単な要件に対して、学習コストが高く感じました。
技巧的な作りになっているメリットが把握出来なかったのですが、それが分かれば再評価するかもしれません(README・ドキュメント欲しい…)。
また、今回使わなかったですが、対話式にする場合、oarrabi/Swiftline: Swiftline is a set of tools to help you create command line applications.が便利そうです。
Ubuntuで実行
単にMacで動くコマンドラインツールであれば、普通Xcodeで以下のように新規プロジェクト作る方が慣れていて簡単で、SwiftPM使ったメリットが現状薄いです。
というわけで、Ubuntuでも動かしてみましょう。
🍂Swiftレター #7🍂でも触れましたが、Ubuntuの14・15系のサポートが切れたようなので、16.04で試してみました。
-
Download Ubuntu Desktop | Download | Ubuntuから
Ubuntu 16.04.1 LTS
をダウンロード
- 仮想マシンなどでUbuntuを立ち上げる
- 僕はParallesを使いました
- Swift.org - Download SwiftからUbuntu 16.04版のSwiftをダウンロードして適当なパスに配置
-
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
を配置する必要があります。
import XCTest
@testable import MonoGeneratorTests
XCTMain([
testCase(GeneratorTests.allTests),
])
合わせて、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
あるいは、ライブラリ(
--type library
)の場合は問題無くXcodeでテスト実行出来るので、ロジックはライブラリとして別リポジトリに用意(SwiftPMは1リポジトリ・1ライブラリ構成)して、コマンドラインツール側はそれを呼び出すだけの薄っぺらい構成にする、というのも良いと思います。
やはり、この構成が良さそうです 🤔
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でガシガシ書いていきたいなと思っています( ´・‿・`)