Emacs で正規表現 + 関数を使った置換。

置換というと、エディタの根幹となる機能であり、正規表現はそのベストオブパートナーです。
ですが、Emacs正規表現はちょっと特殊で、しかも機能的にもしょぼかったりするので、正規表現を使った置換を行なう場合は、Perl など外部に任せるという方法を良く目にします。
それはそれで、Emacs の特殊な正規表現の文法を覚えなくて良いので便利なのですが、どうせだったら Emacs で完結させたいと思うのが世の常です。
ちょとうど、lower-camel caseな文字列とアンダースコア区切りな文字列の相互変換をRubyで - y_tsuda's blog - s21gという記事を見て、こういうのは Emacs が得意だよねと思いながら、脳トレのつもりで挑戦したら、意外と嵌り所満載でした。

LCC → underscore を Emacs で行なってみる。

Emacs正規表現を使った置換を行なうには、replace-regexp コマンドを使います。
大文字を小文字に変換する方法ですが、変換演算子はありません(Ruby もないのでしょう)。
新Emacsの強力な置換機能を使いこなす - ZDNet Japanという記事で紹介されているように、現行の Emacs では、置換に Elisp が使えますので、downcase 関数を使います。Ruby と一緒ですね。
M-x replace-regexp を実行すると、ミニバッファでマッチさせる対象を聞いてきますので、
Replace regexp: \([A-Z]\) と入力します。() には、バックスラシュエスケープが必要です。
次に、置換後の文字列の指定を行ないます。このときに \,の後には Elisp が使えますので、downcase 関数にマッチさせた文字を渡してあげればオッケーです。
Replace regexp \([A-Z]\) with: _\,(downcase \1)
これで、helloWorld -> hello_world に変換できます。置換前に Yes/No を聞いて欲しい場合は、query-replace-regexp コマンド(C-M-%)を使いましょう。

置換をコマンドにしてみる。

毎回、正規表現と関数をタイプするのは面倒なので、よく使う変換はコマンドにすべきです。ですが、ここで大きくつっぱまってしまった僕がいました。
リージョンを置換するコマンドを作りたいと思います。普通に考えると、リージョンを replace-regexp に渡せばいいはずなので、

(defun LCC2underscore (start end)
  (interactive "r")
  (replace-regexp "\\([A-Z]\\)" "_\\,(downcase \\1)" nil start end))

これでいいはずです。文字列として、正規表現を書く場合は、バックスラッシュをバックスラッシュエスケープしてあげなければならないので、そこはもちろん注意します。
ですが、これを実行すると、Invalid use of `\' in replacement text というエラーが発生します。
調べてみたところ、

replace-regexp は replace.el で定義されている。 replace-regexp は (interactive-p) ==> t の場合内部で (query-replace-read-args STRING REGEXP-FLAG) を呼び出している。 第二引数の REGEXP-FLAG が t の場合に `\,' や `\#' が処理されないといけ ないようだ。

その置き換え処理を実行しているのが、 (query-replace-compile-replacement TO REGEXP-FLAG) のようである。

http://eigyr.dip.jp/diary/200711.html#replace%2Dregexp

という話が出てきたので(結論は違うのですが)、

(defun LCC2underscore (start end)
  (interactive "r")
  (replace-regexp "\\([A-Z]\\)" (query-replace-compile-replacement "_\\,(downcase \\1)" t) nil start end))

という風に、置換の regexp を query-replace-compile-replacement 関数で変換した後に渡してあげると、希望通りの結果が実現できました。
これを、もうちょっとちゃんとしたコマンドにしたい場合は、save-excursion で包んであげて、カーソルを元通りにするようにしてあげます。

(defun LCC2underscore (start end)
  (interactive "r")
  (save-excursion
	(replace-regexp "\\([A-Z]\\)" (query-replace-compile-replacement "_\\,(downcase \\1)" t) nil start end)))

これで、M-x LCC2underscore でリージョンを置換できるようになりました。

番外編、replace-regexp を使わない方法。

replace-regexp を使わない方法としては、re-search-forward と replace-match を while ループで使う方法があります。

(defun LCC2underscore (start end)
  (interactive "r")
  (save-excursion
    (save-restriction
	  (narrow-to-region start end)
	  (goto-char (point-min))
	  (while
		  (let ((case-fold-search nil))
			(re-search-forward "[A-Z]" nil t))
		(replace-match (concat "_" (downcase (match-string 0))) t)))))

こっちの方がどうしても長くなってしまいます。

  • re-search-forward は前方に正規表現を使って検索します。
  • replace-match はマッチした部分を引数で置換します。
  • match-string を使うをマッチした部分を呼び出せます。
  • このままだとリージョンを越えてしまうので、narrow-to-region でリージョン以外を隠します。
  • Emacs の検索は大文字小文字を区別しない設定になっている場合があるため、case-fold-search の値を一時的に nil にします。

お好きな方を使うと良いでしょう。
ちなみに、

(query-replace-compile-replacement "_\\,(downcase \\1)" t)

を評価すると、

(replace-eval-replacement concat "_" (replace-quote (downcase (match-string 1))))

という値が返ってくるように、replace-regexp に関数を使って置換するのも replace-match でやるのも内部な方法としては一緒みたいですね。