xah-replace-pairs 20170713.628(in MELPA)
Multi-pair find/replace in strings and region.

概要

elispで置換処理を書くのはけっこう面倒です。

xah-replace-pairs.el は、バッファ内・文字列の置換を
一発で記述できるようにするライブラリです。

インストール

パッケージシステムを初めて使う人は
以下の設定を ~/.emacs.d/init.el の
先頭に加えてください。

(package-initialize)
(setq package-archives
      '(("gnu" . "http://elpa.gnu.org/packages/")
        ("melpa" . "http://melpa.org/packages/")
        ("org" . "http://orgmode.org/elpa/")))

初めてxah-replace-pairsを使う方は
以下のコマンドを実行します。

M-x package-install xah-replace-pairs

アップグレードする方は、
以下のコマンドでアップグレードしてください。
そのためにはpackage-utilsパッケージが必要です。

M-x package-install package-utils (初めてアップグレードする場合のみ)
M-x package-utils-upgrade-by-name xah-replace-pairs

replace-stringやreplace-regexpをelispで使ってはいけない

バッファ内の文字列や正規表現を置換するならば、
素直に replace-stringreplace-regexp を使えばいいじゃないか
という声が聞こえてきそうですが、Lispプログラミングではだめな方法です。

replace-stringのdocstringにはこう書いてあります。

This function is usually the wrong thing to use in a Lisp program.
What you probably want is a loop like this:
  (while (search-forward FROM-STRING nil t)
    (replace-match TO-STRING nil t))
which will run faster and will not set the mark or print anything.
(You may need a more complex loop if FROM-STRING can match the null string
and TO-STRING is also null.)

replace-regexpには同様にこう書けと言われています。

(while (re-search-forward REGEXP nil t)
  (replace-match TO-STRING nil nil))

elispでは頻繁にテキスト処理が行われるのだから、
置換くらい標準関数で用意されてしかるべきなんですが、
何年たってもなぜか用意されていないんですよね。

文字列内の置換ならば replace-regexp-in-string が使えますが、
複数の置換を行うには何度もネストする必要があります。

この関数もEmacs21になってやっと導入されたのだから
これまで無数の自前置換関数が定義されたことでしょう。

HTMLで実体参照に変換する例

HTMLでは「&」を表すには「&」と実体参照で書かなければなりませんが、
region内で置換するコマンドを考えます。

作者のサイトでの例です。

(defun replace-html-chars-region (begin end)
  "Replace “<” to “&lt;” etc in region."
  (interactive "r")
  (save-restriction
    (narrow-to-region begin end)

    (goto-char (point-min))
    (while (search-forward "&" nil t) (replace-match "&amp;" nil t))

    (goto-char (point-min))
    (while (search-forward "<" nil t) (replace-match "&lt;" nil t))

    (goto-char (point-min))
    (while (search-forward ">" nil t) (replace-match "&gt;" nil t))))

うざってぇ…置換ごときでなんでこんなに行数喰うんだよ
と思われるでしょう。

でも、 xah-replace-pairs-region を使うと一発です!

(require 'xah-replace-pairs)
(defun replace-html-chars-region (begin end)
  (interactive "r")
  (xah-replace-pairs-region begin end
                            '(
                              ["&" "&amp;"]
                              ["<" "&lt;"]
                              [">" "&gt;"]
                              )))

作者はベクタ好きのようですが、リストでもいいです。

(defun replace-html-chars-region (begin end)
  (interactive "r")
  (xah-replace-pairs-region begin end
                            '(
                              ("&" "&amp;")
                              ("<" "&lt;")
                              (">" "&gt;")
                              )))

pairsと名乗っていますが、単純に1回の置換も
すんなりと記述できます。

(with-temp-buffer
  (insert "foobarbaz")
  (xah-replace-pairs-region (point-min) (point-max) '(("bar" "BAR")))
  (buffer-string))                      ; => "fooBARbaz"

正規表現置換バージョンの xah-replace-regexp-pairs-region もあります。

文字列置換も楽々

文字列置換も replace-regexp-in-string を連発するより簡単です。

xah-replace-pairs-in-string を使います。

この関数は xah-replace-pairs-region
with-temp-bufferinsertbuffer-string
でくるんでいるだけです。

正規表現置換バージョンの xah-replace-regexp-pairs-in-string
もあります。

;;; BEFORE
(replace-regexp-in-string
 ">" "&gt;"
 (replace-regexp-in-string
  "<" "&lt;"
  (replace-regexp-in-string
   "&" "&amp;" "<<<A&B>>>")))
;; => "&lt;&lt;&lt;A&amp;B&gt;&gt;&gt;"
;;; AFTER
(xah-replace-pairs-in-string
 "<<<A&B>>>"
 '(("&" "&amp;") ("<" "&lt;") (">" "&gt;")))
;; => "&lt;&lt;&lt;A&amp;B&gt;&gt;&gt;"

再帰置換a→c、c→dもできる

実はxah-replace-pairs-(region|in-string)は
置換先を一旦一時的な別な文字に置換し、
その文字を置換先に置換しています。

それをせずに単純に置換するだけの
xah-replace-pairs-region-recursive
xah-replace-pairs-in-string-recursive
も用意されています。

当然、こちらの方が動作は高速です。

;;; 置換先のcを一時的に別な文字に置換しているのでa→cになる
(xah-replace-pairs-in-string "abcd" '(("a" "c") ("c" "d")))
;; => "cbdd"
;;; a→cの後にc→dが適用されるのでa→dになる
(xah-replace-pairs-in-string-recursive "abcd" '(("a" "c") ("c" "d")))
;; => "dbdd"
;;; regexpの方はrecursiveな挙動をする
(xah-replace-regexp-pairs-in-string "abcd" '(("a" "c") ("c" "d")))
;; => "dbdd"

まとめ

このライブラリには6つの関数が定義されています。

統制が取れた名前で正規表現で書くと
xah-replace(-regexp)?-pairs-(region|in-string)(-recursive)?
となります。

  • xah-replace-pairs-region
  • xah-replace-pairs-in-string
  • xah-replace-pairs-region-recursive
  • xah-replace-pairs-in-string-recursive
  • xah-replace-regexp-pairs-region
  • xah-replace-regexp-pairs-in-string

が定義されています。

  • xah-replace-regexp-pairs-region-recursive
  • xah-replace-regexp-pairs-in-string-recursive

は定義されていませんが、regexp版はrecursiveな挙動をします。

elispプログラマならば、ぜひ導入したいところです。


本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。