Xcodeの.pbxproj内のファイルをソートするSwift製コマンドラインツール - SortPbxproj

普段のiOS開発において、ファイルを追加するたびにProject Navigator上のファイルをソートしていると思います。
開発中、ファイル名が正しくソートされていないのを見つけてソートしてみるも、本筋と関係ない project.pbxproj の変更が含まれてレビュアーを困惑させることがあります。
これを解決させるためのスクリプトを作りました。

# 作ったもの

- [WorldDownTown/SortPbxproj](https://github.com/WorldDownTown/SortPbxproj)
    - Swift Package Managerでコマンドラインツールを作りました
    - Homebrewからインストールすることができます

## インストール

```sh
$ brew install WorldDownTown/taps/sort-pbxproj
```

## 使い方

```sh
$ sort-pbxproj $SRCROOT/Path/To/Project.xcodeproj
or
$ sort-pbxproj $SRCROOT/Path/To/Project.xcodeproj/project.pbxproj
```

このようにProject Navigator上のファイルやBuild Phase のCompile Sources 内のファイルもソートされます

|before|after|
|:---|:---|
|![file_inspector_before.png](https://github.com/WorldDownTown/SortPbxproj/raw/master/screenshots/file_inspector_before.png)|![file_inspector_after.png](https://github.com/WorldDownTown/SortPbxproj/raw/master/screenshots/file_inspector_after.png)
|![compile_sources_before.png](https://github.com/WorldDownTown/SortPbxproj/raw/master/screenshots/compile_sources_before.png)|![compile_sources_after.png](https://github.com/WorldDownTown/SortPbxproj/raw/master/screenshots/compile_sources_after.png)

### 自動化

- XcodeのRun Script や `.git/hooks/pre-commit` などに書けば自動的にソートできるようになります
- プロジェクトで共有する場合はコマンドの有無を確認すると優しいですね

```sh
if type sort-pbxproj > /dev/null 2>&1; then
  sort-pbxproj $SRCROOT/Path/To/project.pbxproj
fi
```

## ベースとなっているもの
- [webkit/sort-Xcode-project-file](https://github.com/WebKit/webkit/blob/master/Tools/Scripts/sort-Xcode-project-file)
    - WebKit の開発で使われている Perl スクリプト
    - WebKit 固有の要件に合わせたロジックも含まれています

# 実装について

## ロジック
下記のようなシンプルな流れになっています

1. pbxprojを一行一行読み込む
2. 正規表現でソート対象行を特定
3. ソート対象業ではファイルの名前を基準にソート並べ替える
4. tmpファイルに書き込んで行く
5. すべての行を読み込み終わったら、tmpファイルを `project.pbxporj` にリネームして終了

## コマンドライン引数
### `--no-warnings` オプション
- デフォルトでは 与えられたパスに読み込み可能な `project.pbxproj` があるかチェックしており、見つからなかった場合には終了スクリプトを終了させます
- 別名を使っている場合などを考慮して、そのチェックを省略するようなコマンドラインオプション `--no-warnings` を用意しています
    - 別名を使うことがあるのか正直よくわかりませんが、[本家 WebKit版](https://github.com/WebKit/webkit/blob/master/Tools/Scripts/sort-Xcode-project-file#L68) を踏襲しました
    - コマンドラインオプションを実装したかっただけなので、将来消すかもしれません……

### 実装
- コマンドラインの引数とオプションは [CommandLine.arguments](https://developer.apple.com/documentation/swift/commandline/2294816-arguments) から取得できますが、型は `[String]` となっていて、スクリプト名や引数・オプションの区別はないためとても扱いやすいとは言えません
- [kylef/Commander](https://github.com/kylef/Commander) を使うことで簡単に実装することができました
    - Swift Package Manager の dependencies に含めています
- 引数やオプションを宣言的に書くだけで実装できるうえに、 `--help` も自動的に実装されるため実装コストがかなり抑えられました

```swift
// main.swift
import Commander

private func main(path: String, suppress: Bool) throws {
    // ソート処理
}

command(
    Argument<String>("path", description: "File path to *.xcodeproj or project.pbxproj"),   // 引数
    Flag("no-warnings", description: "ignore file path is valid or not"),                   // オプション
    main).run()
```

# まだやってないこと
- ユニットテスト
- ユーザーに優しいエラーメッセージ表示
- Homebrew の [Bottles](https://github.com/Homebrew/brew/blob/master/docs/Bottles.md) 対応 (バイナリ形式の配布)
- Project Navigator のグループの中のグループは上にソート並ぶようなオプション
![ダウンロード.png](https://qiita-image-store.s3.amazonaws.com/0/11454/fc3de85c-173b-95e2-f523-b7be3406328c.png)

# 追記
### 2018/12/05 00:10

> Project Navigator のグループの中のグループは上にソート並ぶようなオプション

私の勘違いで [webkit/sort-Xcode-project-file](https://github.com/WebKit/webkit/blob/master/Tools/Scripts/sort-Xcode-project-file#L159-L163) でも [SortPbxProj](https://github.com/WorldDownTown/SortPbxproj/blob/master/Sources/SortPbxprojCore/ProjectFileReader.swift#L147-L152) でも、オプションではなくデフォルトの機能としてすでに実装されていました😅 (リンク先が該当の処理です)

# 参照
- SwiftPM, Homebrewについて @mono0926 さんの記事を全面的に参考にさせていただきました
    - [Swift Package Manager (SwiftPM) で作るコマンドラインツール](https://qiita.com/mono0926/items/e8fdd97115780204f797)
    - [Swift Package Manager(SwiftPM)で作ったコマンドラインツールをHomebrewに登録する方法](https://qiita.com/mono0926/items/c32c008384df40bf4e41)
- [Swift.org - Package Manager](https://swift.org/package-manager/)
- [Getting to Know Swift Package Manager - WWDC 2018 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2018/411/)
- [How to Create and Maintain a Tap — Homebrew Documentation](https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap)
- [Formula Cookbook — Homebrew Documentation](https://docs.brew.sh/Formula-Cookbook)