変更したファイルだけにSwift Formatしたい

## 問題

iOS/macプロジェクトでswift formatを使っているが、プロジェクトの全ファイルではなく変更したファイルだけをformat掛けたい。

Xcodeのビルドスクリプトに入れていることが多くて、プロジェクトが大きくなった時に全ファイルにかけるとビルド時間に影響し、開発の効率が落ちる。だから、差分のファイルだけに掛けたい。ソースバージョン管理(Gitとか)をしているなら対応はできる！

**追記:** SwiftUIを使っている場合は Previewのために内部的にビルドが走ることがよくある。その場合は この[answerの様に](https://stackoverflow.com/questions/77844783/detect-if-build-is-triggered-from-swiftui-preview-in-build-phases-run-script ) `if [ "${ENABLE_PREVIEWS}" = "YES" ]; then ...`  制御し、lintのみとした方がいい。そうしないと整形処理で新しく生成されるファイルと自分が編集しているファイルの中身が違っていてXcodeが頻繁にびっくりする。 

**2025/05/02追記:** Xcode16.3では `ENABLE_PREVIEWS`が削除されている。Previewsビルドと普通のビルドの見分けが不可。（Appleの言い訳: Previewsのビルド仕組みが改善され　Xcodeの中で行われている処理が　普通のビルドと全く同じだから `ENABLE_PREVIEWS` が不要になったんだ！。処理が同じででもユーザーケースが違うので、見分ける方法がないと困るじゃんー　ちょっと考えていることが狭いと感じました。今どうしようもない。


## アプローチ
`git status`コマンドで取得するファイル一覧をswift formatに渡すことができる。そうすることで　余計に整形処理を実行せずに済む！



#### [Apple製のSwift-Format](https://github.com/apple/swift-format)の場合

空白で区切ってファイル一覧を作成しパラメータとして渡すことが期待されている。変更ファイルが０個の場合はコマンドとしておかしいので　０個の場合は何もしないようにする

実装:
```bash
# 変更されたSwiftファイルを取得(1行で空白区切り）
changedPaths=$(git status -s . | awk '
    /^D/ {next} 
    !/\.swift$/ {next}
    /^R|^C/ {print $4; next}
    {print $2}
    ' | xargs -n1)


if [ -n "$changedPaths" ]; then
    # 整形外科通院
    echo "Format and Lint Swift Sources: $changedPaths"
    swift-format format --in-place --configuration my-config-file.json $changedPaths
    swift-format lint --configuration my-config-file.json $changedPaths
else
    echo "Format and Lint Swift Sources: No Swift files have changed."
fi
```
ちなみに、全部掛けたかったら　以下で良い

```bash
# 全部整形外科通院
echo "Lint and Format Swift Sources: doing all files"
swift-format format --in-place --configuration my-config-file.json --recursive .
swift-format lint --configuration my-config-file.json --recursive .
```

#### [Nicklockwood製のSwiftFormat](https://github.com/nicklockwood/SwiftFormat)の場合

環境変数でファイル一覧を渡す必要がある。`SCRIPT_INPUT_FILE_COUNT` と `SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`, ... shell scriptではよくあるパターン

実装:
```bash
# 変更されたSwiftファイルを取得
changedPaths=$(git status -s . | awk '
    /^D/ {next} 
    !/\.swift$/ {next}
    /^R|^C/ {print $4; next}
    {print $2}
    ' | xargs -n1)
counter=0
for file in $changedPaths; do
  export SCRIPT_INPUT_FILE_$counter="$SRCROOT/$file"
  counter=$((counter + 1))
done
export SCRIPT_INPUT_FILE_COUNT=$counter
# 整形外科通院
swiftformat --config my-config-file.yml --scriptinput
```

#### [Realm製のSwiftLint](https://github.com/realm/SwiftLint)の場合

ドキュメント上、他と同様に環境変数として渡せると書かれているが何故かlintの方が効かないので　ファイルリストとして渡す

実装:
```bash
# 変更されたSwiftファイルを取得(1行で空白区切り）
changedPaths=$(git status -s . | awk '
    /^D/ {next} 
    !/\.swift$/ {next}
    /^R|^C/ {print $4; next}
    {print $2}
    ' | xargs -n1)
# 整形外科通院
# auto correct
swiftlint $changedPaths --fix --format
# lint
swiftlint $changedPaths --config my-config-file.yml
```

:::note info
## あのawkコマンドの詳細を気になるあなたへ

まず、`git status -s` は以下のような出力をする

```bash
M path/to/modified/file.js
M path/to/modified/file.swift
D path/to/deleted/file.swift
R path/to/old/file.swift -> path/to/new/file.swift
?? path/to/non/tracked/file.swift
```

この出力をawkのパターンマッチの力でパース！

- `/^D/ {next}` 削除済のファイルを整形出来ないので Dで始まるものをスキップし、次の行へ
- `!/\.swift$/ {next}` .swiftで終わる行をマッチしないものをスキップし、次の行へ
- `/^R|^C/ {print $4; next}` 名前変更されたRや競合されたCで始まる場合は ４番目のトークンをプリントし、次の行へ
- `{print $2}` 上記の条件全て一致しなかったので２番目のトークンをプリント
:::
以上！


誰かに役に立つといい

See you later! 🐊