正規表現

IIJ技術研究所 山本和彦
作成:2002/08/30
更新:2002/08/31

正規表現は難しい。 でも、Emacs Lisp の正規表現はもっと難しい。 ここでは Emacs Lisp で正規表現を書くプログラマを混乱させる落とし穴についてまとめる。


バックスラッシュ

Perl では、バックスラッシュに合致する正規表現を次のように書く。

'\\'

しかし、Emacs Lisp では以下のように書かなければならない。

"\\\\"

Emacs Lisp の正規表現が嫌いになる理由はこれだけで十分だ。 しかし、なぜこんなにもバックスラッシュが必要なのだろう? それは、Perl が「リテラル」として正規表現を記述するのに対し、 Emacs Lisp では「文字列」として表記するからである。

Emacs Lisp の関数呼び出しでは、 各引数が「評価」されてから関数に利用される。 もちろん、引数として与えられた文字列も利用前に評価される。

(message "a\tb")

文字列 "a\tb" が評価されるからこそ、 \t がタブ文字に変換されるのだ。

正規表現でも事情は変らない。次のバックスラッシュを検索する例を見て欲しい。

(re-search-forward "\\\\")

"\\\\" はまず評価されて '\\' になり、 その後 re-search-forward に利用される。

もし、バックスラッシュが多すぎてよく分らないのなら、 正規表現を評価した後の形について考えればよい。 文字列を評価する簡単な方法としては、 *scratch* バッファに insert することが挙げられる。

(insert "\\\\\\*")
→ \\\*

"\\\\\\*" という謎の正規表現も、'\\\*' となる。 こればバックスラッシュの後にアスタリスクが続く文字列に合致する。 バックスラッシュもアスタリスクもメタ文字なので、 バックスラッシュが前についているだけのことだ。

初心者は insert による方法を思い付かず、 逆にリテラルを正規表現に直そうとする傾向にあるようだ。 そして、regexp-quote という関数を発見してぬか喜びをする。 この関数の役割は、 リテラルを正規表現へ直すことではないから、 忘れる方がいい。

正規表現エンジンは、 \t や \n といった文字列の表現は理解できないことに注意しよう。 タブを探す正規表現は以下のように書く。 この場合、正規表現エンジンは、'\t' ではなく、タブ文字を受け取るのである。

(re-search-forward "\t")

次の例では、正規表現エンジンは '\t' を受け取る。 バックスラッシュはメタ文字であるが、 正規表現には \t というメタ表現はない。 だから、このバックスラッシュは無視され、't' だけに合致することになる。 (定義されてないメタ表現は、 バックスラッシュの後の文字そのものを意味する。)

(re-search-forward "\\t")

よって、バックスラッシュの後に 't' が続くパターンを表わす正規表現は、 以下のように書く必要がある。 正規表現エンジンは、'\\t' を受け取り、 '\\' がバックスラッシュに合致するメタ表現として解釈されるのである。

(re-search-forward "\\\\t")

問題をややこしくしているのは、 クラス("[" と "]" で囲まれたパターン)を使うと、 バックスラッシュに合致する正規表現は以下のように書けることだ。

(re-search-forward "[\\]")

混乱ついでに、 バックスラッシュが 2 つ続くパターンに合致する正規表現も紹介しておこう。

(re-search-forward "[\\]\\\\")

こんな奇怪な正規表現になる理由は、 Emacs Lisp ではクラスの中のバックスラッシュがメタ文字として扱われないからである。 クラス中でメタ文字として扱われるのは、 ']' と '-' と '^' のみである。

"[\\]\\\\" を文字列として評価すれば、 '[\]\\' となる。 バックスラッシュが ']'に掛っているように見えるかもしれないが、 これはもはや文字列ではなくリテラルだから、そんなことはない。 クラスの中のバックスラッシュは、メタ文字ではないから、 1 つでバックスラッシュに合致する。 クラスの外のバックスラッシュは、メタ文字なので、 2 つでバックスラッシュに合致する。 というわけだ。

では、以下の正規表現は何に合致するか分るだろうか?

(re-search-forward "[\\t]")

"[\\t]" は評価されて '[\t]' となる。 これをタブに合致する正規表現だと思った人は、 ここまでの話がまったく理解できないない。 クラスの中でバックスラッシュは、1 つでバックスラッシュを表わす。 だから、これはバックスラッシュか 't' を表わすクラスになる。 (そもそも正規表現エンジンには '\t' をタブだとは解釈しないし、 '\t' というメタ表現もない。)

Perl なら '(' や ')' がメタ文字なので、 前方参照をする正規表現は以下のようにすっきり書ける。

'([1-9][0-9]*)'

Emacs Lisp では '(' や ')' がメタ文字でななく、 正規表現エンジンに '\('、'\)' というメタ表現を渡す必要がある。 これもバックスラッシュを増やしている理由である。 まず文字列として評価されることを考えれば、 Emacs Lisp では同じ正規表現を以下のように書かなければならない。

'\\([1-9][0-9]*\\)'

蛇足ついでに、正規表現による検索コマンドへ一言。 C-uC-s や C-uC-r で利用できる正規表現による検索コマンドでは、 正規表現をリテラルで入力する。 文字列ではないこと注意しよう。


否定クラス

通常 Emacs Lisp の正規表現は、「行」内の文字列を対象とし、 行をまたがって合致することはない。 しかしながら、否定クラスを使うと突然、行を越えて合致するようになる。

たとえば、以下のようなバッファがあるとしよう。

This
is a pen

カーソルを "T" の前に置いて、 以下のプログラムをM-: などで実行してみて欲しい。

(save-excursion
  (when (re-search-forward "[^ \t]+" nil t)
    (buffer-substring (match-beginning 0) (match-end 0))))

すると、"This\Jis" に合致することが分る。 「なぜだ!」と Emacs に向かって叫ばずにはいられない。

理由は、「クラス」では '\n' (当然、正規表現エンジンが受け取るのは改行文字そのもの)が言外に否定されているからだ。 「否定クラス」を利用すると、言外の部分も否定を受け、 否定の否定で '\n' そのものが含まれるようになる。

だから、否定クラスには '\n' も含める習慣を付けよう。 上記の例は、以下のようにすればよい。

(save-excursion
  (when (re-search-forward "[^ \t\n]+" nil t)
    (buffer-substring (match-beginning 0) (match-end 0))))

これでめでたく、"This" に合致するようになる。


looking-at と string-match

'^' は、行頭に合致するメタ文字である。 re-search-forward などでは、このメタ文字の意味は明らかである。 では、カーソルが行頭にない場合に、 looking-at と組み合わせるとどうなるだろう?

たとえば、次のようなバッファがあるとしよう。

This is a pen

'p' の前にカーソルを置き、以下のプログラムを M-: などで実行してほしい。

(looking-at "^pen")

合致せずに、nil が戻ってくるのが分るだろう。 つまり、looking-at に渡される正規表現中の '^' は、 カーソルの位置に関係なく行頭に合致するのである。

同じことが、string-match にも言える。

(string-match "pen" "This is a pen")
→ 10
(string-match "^pen" "This is a pen" 10)
→ nil

シンタックス表

ホワイトスペースを表現するには、 '[ \t]' よりも '\W' の方がかっこよく見えるかもしれない。 しかし、私は '\W' を使わないことにしている。

その理由は、Emacs Lisp の正規表現にはシンタックス表という概念があり、 それを操作すれば '\W' の意味を変更できるからである。 シンタックス表の設定によっては、 '\W' は ' ' だけに合致するようになるかもしれないのだ。