2024-01-31

Emacsでdiffの文字化けを回避する(様々な文字エンコーディングに対応する)

何だか時代錯誤感のあるタイトルで申し訳ないのですが、私は長年Emacsを使っていてもdiffやらgrepやら基本的なコマンドの使い方が分かっていない人間なのです。ご容赦ください。grepの方は最近はripgrepの登場で大分マシになりましたが。いや、そうじゃ無くて、2024年にもなって文字化けなどと書かねばならないというところですよ!

日常的に複数の文字エンコーディング(文字符号化方式、簡単に言えば文字コード、Emacs用語ではコーディングシステム)を使っている人はdiffをどうしているのでしょうか。まぁ、使う文字エンコーディングが一つに偏っているならそれに合わせて残りは場当たり的に対処すれば良いのでしょう。私もそうしていました。UTF-8以外使うな! などと過激なことを言う方も昨今いらっしゃいますが、私はそうは思いません。長年コンピュータを使ってきた人間にとって、過去に作った物を無かったことには出来ませんからね。

とは言えdiffを取ったときに文字化けしているバッファを見ると煩わしさを感じるのも事実です。

そういうときはdiffのバッファの中で M-x revert-buffer-with-coding-system (C-x RET r) の後、文字エンコーディングを選ぶのが簡単です。diffは取り直しになりますが。

他にもread onlyを解除して、バッファ全体をencode-coding-regionしてからdecode-coding-regionしてやると直せる場合もあります。diffの取り直しは回避できますが、常に直せるかはちょっと分からないです。

ediffで済むならそれを使うという手もあります。

いずれにせよ煩わしいことには変わりないので、ある程度自動的に対処するように次のようなコードを書きました。

(defun my-diff-detect-coding-system (file)
  "FILEのcoding systemを返す。分からなかったらnilを返す。"
  (let ((cs
         (when (file-regular-p file) ;;ディレクトリは除外する
           (with-temp-buffer
             (insert-file-contents file nil nil 1000000) ;;1MBくらい読んでおく?
             ;; これが一番簡単で確実っぽい
             last-coding-system-used))))
    (message "Detected coding system: %s" cs)
    (unless (memq cs '(nil undecided no-conversion)) ;;変なのは返さない
      cs)))

(defun my-diff-around (orig-fun old new &rest args)
  "diffにひっかけるaroundアドバイス。"
  ;; NEWのcoding systemに合わせてdiffを取る
  (let ((coding-system-for-read (or coding-system-for-read ;;すでに指定されている場合はそれを使う
                                    (my-diff-detect-coding-system new))))
    (apply orig-fun old new args)))

(advice-add 'diff :around 'my-diff-around)

要するにファイル(NEW側のみ)の文字エンコーディングを判別して、それをcoding-system-for-readに設定してからdiffを実行するだけです。

my-diffという関数を作ろうか迷いましたが、diffはいろんな場所から呼び出されているような気がしたので全てに適用させるためにdiffに対するadviceにしてみました。

文字エンコーディングを判別しているところですが、insert-file-contentsの後にlast-coding-system-usedを参照するのが見つけた方法の中では一番簡単でした。最初はdetect-coding-regionを使ったのですが、UTF-16が判別できないこととファイルローカル変数の指定が効かないことが問題になりました。UTF-16はどのみち別の問題があるので諦めるとして、 -*- coding:cp932 -*- のような指定は効いてほしいところ。半角カナでCP932(SJIS)で「ミエ」と書いたらUTF-8の「д」と区別が付かないんですよ(どんなシチュエーションだよ)。そんなときにcoding:の指定を入れれば解決できるわけです。set-auto-coding関数を使えばUTF-16(auto-coding-regexp-alist)やファイルローカル変数の判別が可能になるのですが、今度は行末タイプ(unix、dos、mac)が判別できません。行末タイプだけを判別するような関数を探したのですが見当たりませんでした。自分で \r\n を検索すれば良いのでしょうが、そんな面倒なことをするよりもlast-coding-system-usedを参照するだけで済むようでした。それらの判別処理は全てinsert-file-contentsの中で行われていますので。

UTF-16はどうしましょうね。こればっかりはUTF-8にでも変換してからdiffを取るくらいしか思いつきません。--textを指定するとして、diff自身が出力するヘッダーの文字エンコーディングと合いませんからね。

ディレクトリ単位の比較は相変わらず化けるので必要に応じて C-x RET r するということで。

あ、diff自体が出力する日本語メッセージが化けますね。「のみに存在」とかいうやつ。実行前に環境変数も変えようかな……。

こうして今日も一つ直すと何個も直すところが増えていくのでした。

まだまだdiffの事はよく分かりません。