ソースコードの一括置換に便利なCombyを使ってみた

![プロダクト アドベントカレンダー01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387/9b91d029-b16b-4ec3-f669-0d1f541225b1.jpeg)

:::note info
この記事は[**株式会社ログラス Productチーム Advent Calendar 2024**](https://qiita.com/advent-calendar/2024/loglass) のシリーズ 1、**3日目**の記事です。
:::

エンジニアの三田です。

最近、リファクタリングのためにソースコードを一括置換したい場面があり、IDEやsedコマンドで正規表現と格闘していたのですが、あまりに辛すぎたので何か良いツールはないものかと探していました。

そんな時に出会ったCombyを紹介します。

## Combyとは

https://comby.dev/

Combyとは、検索や置換に特化したコマンドラインツールです。

単なる文字列の置換ではなく、コードの構造を考慮してくれたり、正規表現を使わずとも高度な置換が出来るのが気に入ったところです。

プログラミング言語も主要なものは概ね対応しています。

| Bash | C/C++ | C# | Clojure | Coq | CSS | Dart | Elm | Elixir |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Erlang | Fortran | F# | Go | Haskell | HTML | Java | JavaScript | JSX |
| JSON | Kotlin | Julia | LaTeX | Lisp | Nim | MATLAB | Move | OCaml |
| Pascal | PHP | Plain Text | Python | R | ReScript | Ruby | Rust | Scala |
| Solidity | SQL | Terraform | Swift | TSX | TypeScript | XML | Zig |  |

## セットアップ

Mac、Windows、Ubuntu、Dockerに対応しています。

自分はMacにHomebrewでインストールしました。

```kotlin
brew install comby
```

また、プレイグラウンドが用意されているのでちょっと試したい方はこちらもどうぞ。

https://bit.ly/2XpttJG

## 基本的な使い方

```shell
comby 'swap(:[a], :[b])' 'swap(:[b], :[a])' example.py -i
```

1つ目の引数がmatch template(検索条件)、2つ目がrewrite template(置換条件)、3つ目が対象ファイルになっています。

また、`:[a]`と`:[b]`の部分はHoleと呼び、正規表現のキャプチャグループのようなものです。`a`とか`b`の部分は任意の名前を指定できます。

上記のサンプルでは、swap関数の引数の順番を入れ替える指定をしています。

コマンドラインから実行すると、このような感じで置換されます。

```diff_python
------ example.py
++++++ example.py
@|-1,3 +1,3 ============================================================
 |def test(self, foo, bar):
-|  swap(foo, bar)
+|  swap(bar, foo)
 |
```

これだけのことでも正規表現でやろうとすると、スペースや改行などのホワイトスペースの考慮が必要だったり結構大変だと思いますが、Combyはこのあたりを良い感じにやってくれてとても楽です。

## プログラムの構造を考慮した置換

次にKotlinのプログラムをサンプルにプログラムの構造を考慮した置換の例を見たみたいと思います。

```diff
comby 'User(:[1], :[2])' 'User(:[2], :[1])' Test.kt
```

上記コマンドを実行すると、コンストラクタとコンストラクタの呼び出し箇所が置換されました。

すごいのは変数と同時に型も入れ替わっていることです。これはIDE使っても出来ますがコマンドから一括で置換できるのはとても便利です。

```diff_kotlin
------ Test.kt
++++++ Test.kt
@|-1,7 +1,7 ============================================================
-|data class User(val id: Int, val name: String) {
+|data class User(val name: String, val id: Int) {
 |  companion object {
 |    fun of(id: Int, name: String): User {
-|      return User(id, name)
+|      return User(name, id)
 |    }
 |  }
 |}
```

## 置換後に変換をかける

Rewrite propertiesを使うと置換後に更に変換をかけることができます。

ひとつ例を見てみましょう。

```shell
comby ':[[table]].:[[column]]' ':[[table]].UPPER_SNAKE_CASE.:[[column]].UPPER_SNAKE_CASE' .sql
```

置換後のHoleに対してUPPER_SNAKE_CASEを指定して大文字に変換しています

```diff_sql
------ test.sql
++++++ test.sql
@|-1,9 +1,9 ============================================================
 |select
-|  user.id
-|  user.name,
-|  mail.mail_address
+|  USER.ID
+|  USER.NAME,
+|  MAIL.MAIL_ADDRESS
 |from
 |  user
 |  join mail
-|  on user.id = mail.id
+|  on USER.ID = MAIL.ID
-|where user.id = ?
+|where USER.ID = ?
```

Rewrite propertiesは他にも種類があるので、こちらを参照ください

https://comby.dev/docs/rewrite-properties

## 条件を更に絞り込む置換

検索条件を更に絞り込みたい場合にはruleというもので指定できます。

下記サンプルでは、`:[1]`の部分が ”records” と合致しない場合に置換する例ですが、`-rule 'where :[1] != "records"'` と指定することで実現できます。

```diff_shell
comby 'import com.example.tables.:[1].:[2]' 'import com.example.tables.references.:[2]' .kt -rule 'where :[1] != "records"'
```

実行すると下記のように"redords"に合致する行は置換の対象外になっています。これも正規表現でやろうとするとかなり大変ですし、指定の仕方も直感的で分かりやすいです。

```diff_kotlin
------ Test.kt
++++++ Test.kt
@|-1,5 +1,5 ============================================================
 |import com.example.tables.records.UserRecord   <--- この行は置換されていない
-|import com.example.tables.table.USER
+|import com.example.tables.references.USER
 |
 |data class User(val id: Int, val name: String) {
 |  companion object {
```

ちなみに、前述したRewrite propertiesはここでも使えました。

## 設定ファイルを使用する

改行を含むような長くて複雑な条件を使いたい場合や、どんな変換を行なったか残しておきたい時などにTOML形式の設定ファイルが使えます。

```toml
[import-table1]
match="import com.example.Tables.:[1]"
rewrite="import com.example.tables.references.:[1]"

[import-table2]
match="import com.example.tables.:[1].:[2]"
rewrite="import com.example.tables.references.:[2]"
rule='where :[1] != "records"'
```

実行はこのように設定ファイルを—configで指定して実行します。

```shell
comby -config comby.toml -f .kt
```

## おわりに

Combyを使うことでやりたかったリファクタリングの作業が楽にできました。
とても便利ですのでリファクタリングのお供に使ってみてはいかがでしょうか。