#
name

ok
dd
..
  • ,
..
.
..
© Copyright 2023 joppot. All rights reserved.

差分を見ながら安全にキーワードを置換できるreplace-regexp-as-diffがまぁまぁ便利

#
emacs
2025年12月03日
thumbnail

概要

これまで、Emacsで正規表現を用いた大規模なテキスト置換はそれなりにリスキーで、意図しない変更が紛れ込む可能性がありました。
Emacs 30.1で導入されたreplace-regexp-as-diffは、変更を即座に適用するのではなく、まずdiff形式でプレビューし、ユーザーが内容を確認した上で適用するというアプローチをとります。

完成済みコード

最初に実際に使えるコード例を提示しておきます。
(use-package replace-regexp-as-diff
  :bind (("C-c r d" . replace-regexp-as-diff)
          ("C-c r m" . multi-file-replace-regexp-as-diff)
          ("C-c r f" . my-replace-regexp-in-folder-as-diff)
          ("C-c r r" . my-replace-regexp-in-folder-recursive-as-diff))
  :init
  ;; フォルダ内のファイル置換コマンド(共通実装)
  (defun my-replace-regexp-in-folder-base (recursive-p)
    "置換パターンを入力させて、指定ファイルに対して置換をdiffで表示。
     RECURSIVE-P が非nil の場合はサブフォルダも対象。"
    (require 'misearch)
    (let* ((dir (read-directory-name "置換対象のフォルダを選択: "))
           (files (if recursive-p
                      (directory-files-recursively dir "^[^.]" nil)
                    (seq-filter #'file-regular-p
                                (directory-files dir t "^[^.]" nil)))))
      (if (not files)
          (message "ファイルが見つかりません")
        (let ((common (query-replace-read-args
                       (concat "Replace"
                               (if current-prefix-arg " word" "")
                               (if recursive-p
                                   " regexp as diff in folder (recursive)"
                                 " regexp as diff in folder"))
                       t t)))
          (multi-file-replace-as-diff
           files (nth 0 common) (nth 1 common) t (nth 2 common))))))

  ;; フォルダ内のファイル置換コマンド
  (defun my-replace-regexp-in-folder-as-diff ()
    "フォルダを選択して、その直下のファイルに対して置換をdiffで表示。
     サブフォルダ内のファイルは対象外。"
    (interactive)
    (my-replace-regexp-in-folder-base nil))

  ;; フォルダ内のファイル置換コマンド(再帰版)
  (defun my-replace-regexp-in-folder-recursive-as-diff ()
    "フォルダを選択して、その配下の全ファイルに対して置換をdiffで表示。
     サブフォルダ内のファイルも対象。"
    (interactive)
    (my-replace-regexp-in-folder-base t)))

(use-package diff-mode
  :config
  (define-key diff-mode-map (kbd "C-c C-a") 'diff-apply-hunk)
  (define-key diff-mode-map (kbd "C-c a") 'diff-apply-buffer))

replace-regexp-as-diffの使い方

ファイルバッファ内でreplace-regexp-as-diffコマンドを使用する基本的な手順を解説します。
まず、このようなサンプルファイルがあるとします:
This is foo file
foo is great
I love foo
このファイルを対象に、以下の手順で置換を実行します:
1コマンドの実行
M-x replace-regexp-as-diff
コマンドを実行します。
2 検索正規表現の入力 ミニバッファに、検索対象となる正規表現を入力するよう求められます。 正規表現を必ず使う必要はなく、キーワードでも構いません。 文字を入力するとハイライト対象がハイライトされます。おおよそ良さそうであれば、エンターを押します。
例:\\bfoo\\b   (単語としての「foo」を検索)
3 置換文字列の入力 次に、マッチした部分を置き換えるための文字列を入力します。 エンターを押します。
例:bar
4 diffバッファでの変更確認 コマンドを実行しても、元のバッファは一切変更されません。代わりに変更された場所をdiff形式で表示する新しいバッファが生成されます。
article image
5 パッチとしての変更適用 diffバッファの内容を確認し、問題がなければ、その変更をパッチとして元のファイルバッファに適用します。diffバッファで以下のコマンドを実行します:
C-c C-a   (diff-apply-hunk)

複数ファイルへの一括置換

replace-regexp-as-diffの関連する関数を使って、プロジェクト全体など、複数のファイルにまたがる一括置換が可能になります。

multi-file-replace-regexp-as-diffによるファイルリストベースの置換

このコマンドは、あらかじめ指定されたファイルリストに対して一括置換を実行します。
M-x multi-file-replace-regexp-as-diff
以下の手続きで変更します。
1 ファイルリストの指定 ミニバッファでファイルを選びます。対象ファイルは幾つでも選べます。 エンターを押すとファイル選択が終わり次のステップに進みます。
article image
2 検索・置換パラメータの入力 単一ファイルの場合と同じく、正規表現と置換文字列を入力します。
3 統合diffの表示 リストに含まれるすべてのファイルに対する変更がまとめて表示されます。
article image
4 一括適用 確認後、すべてのファイルに対してC-c C-aでパッチを適用できます。
C-c C-a   (diff-apply-buffer)

Diredを使うdired-do-replace-regexp-as-diff

dired-do-replace-regexp-as-diffは、EmacsのファイルマネージャであるDiredと連携して利用するコマンドです。
使い方:
  1. Diredバッファを開く
  2. 置換したいファイルを含むディレクトリに移動する
  3. 置換対象のファイルをmキーでマーク
  4. C-c r d(またはコマンド実行)でこのコマンドを実行
  5. 正規表現と置換文字列を入力
  6. 生成されたdiffバッファで内容確認
  7. C-c C-aでパッチ適用
ファイル選択がdiredなだけで、他は同じ手順で置換作業を進められます。

差分の一括適用と保存

複数ファイルに対する置換を実行した場合、diffバッファには多数の変更箇所(hunk)が表示されます。標準のC-c C-a(diff-apply-hunk)では、1つのhunkずつ適用していく必要があり、変更箇所が多いと時間がかかります。

diff-apply-bufferによる一括適用

diffバッファで全ての変更を確認した後、以下のコマンドで一括適用できます:
C-c a   (diff-apply-buffer)
このコマンドは、diffバッファ内の全てのhunkを順次自動適用します。さらに、全てのhunkの適用が成功した場合、変更されたバッファを自動的に保存してくれます。
大量の置換を行う際に特に有効で、適用から保存までを一度の操作で完了できます。
注意点としては、このコマンドは全ての変更を一度に適用するため、事前にdiffバッファの内容をよく確認してください。また、個別に適用したい場合は、従来通りC-c C-aで1つずつ適用することもできます。

フォルダ単位で置換する自作コマンド

標準のmulti-file-replace-regexp-as-diffでは、ファイルを1つずつ手動で選択する必要があります。しかし、特定のフォルダ配下のファイルをまとめて置換したい場合、この手順は煩雑です。
そこで、フォルダを指定するだけで配下のファイルを一括置換できるカスタムコマンドを作ったので、その使い方も紹介しておきます。

フォルダ直下のファイルのみ対象(非再帰版)

my-replace-regexp-in-folder-as-diffは、選択したフォルダの直下にあるファイルのみを対象とします。サブフォルダ内のファイルは除外されます。
使い方:
  1. C-c r fを実行またはM-x my-replace-regexp-in-folder-as-diff
  2. 対象フォルダを選択
  3. 正規表現と置換文字列を入力
  4. フォルダ直下のファイルのみが対象となり、diffバッファが表示される
  5. C-c C-aでパッチを適用

サブフォルダも含めて全ファイル対象(再帰版)

my-replace-regexp-in-folder-recursive-as-diffは、選択したフォルダ配下の全ファイルを再帰的に対象とします。
使い方:
  1. C-c r rを実行またはM-x my-replace-regexp-in-folder-recursive-as-diff
  2. 対象フォルダを選択
  3. 正規表現と置換文字列を入力
  4. サブフォルダ内のファイルも含めて全ファイルが対象となり、diffバッファが表示される
  5. C-c C-aでパッチを適用
フォルダ単位での一括置換が効率的に行えるようになります。

replace-regexp-as-diffのまとめ

replace-regexp-as-diffとその関連機能は、以下の2つの側面に集約されます。

安全性

メリットは変更を適用する前にdiff形式で視覚的に確認できる点にあります。これにより、意図しない変更が紛れ込むリスクが低減され、大規模な置換作業を実行できます。

効率性

単一ファイルから複数ファイル、Diredでマークしたファイル群まで、様々なスコープに対して一貫した操作感で一括置換を行えます。これにより、従来は複数のステップを要したワークフローが、単一のプロセスに統合されます。
Emacsをプログラミング以外でも活用している人には便利なツールになっていくでしょう
© Copyright 2023 joppot. All rights reserved.