本当にキモい Vim script - 正規表現編

Vim Advent Calendar 2012 の 339 日目の記事です。

先日、Lingr で :s コマンドの引数をパースする方法についての話になりました。
:s はご存知の通り、置換コマンドです。

:[range]s[ubstitute]/{pattern}/{string}/[flags] [count]

今回 [range] と [count] は無視するとして、それ以外の {pattern} と {string} と [flags]、あとはパターン内でエスケープされているかもしれない区切り文字(多くの場合は /) が何になるか知りたい。
結果的にできたのは以下のような正規表現です。

\v^s%[ubstitute]([\x00-\xff]&[^\\"|[:alnum:][:blank:]])(%(\\.|.){-})%(\1(%(\\.|.){-})%(\1([&cegiInp#lr]*))?)?$

以下のように matchlist() で使うと各部位が取得できます。

let pattern = '\v^s%[ubstitute]([\x00-\xff]&[^\\"|[:alnum:][:blank:]])(%(\\.|.){-})%(\1(%(\\.|.){-})%(\1([&cegiInp#lr]*))?)?$'
echo matchlist('s/fo\/ge\\/hu\/ga/gi', pattern)
" => ['s/fo\/ge\\/hu\/ga/gi', '/', 'fo\/ge\\', 'hu\/ga', 'gi', '', '', '', '', '']
echo matchlist('s@fo/ge\@@hu/ga@gi', pattern)
" => ['s@fo/ge\@@hu/ga@gi', '@', 'fo/ge\@', 'hu/ga', 'gi', '', '', '', '', '']

返ってくるリストの、

  • 0 番目がマッチした全体(きちんとマッチした場合は渡した文字列と一致するので基本的に不要)
  • 1 番目が区切り文字
  • 2 番目が {pattern}
  • 3 番目が {string}
  • 4 番目が [flags]

となります。マッチしなかった場合は matchlist() の仕様で空のリストが返ってきます。

で、これを見せたところ、

「どこのアトムが何をパースするために割り当てられているかとか,ぱっと見ると全然分からないです.」

「メンテできる気がしない…」

などの声が上がったので、ちょっと分解して解説してみます。

改めまして

\v^s%[ubstitute]([\x00-\xff]&[^\\"|[:alnum:][:blank:]])(%(\\.|.){-})%(\1(%(\\.|.){-})%(\1([&cegiInp#lr]*))?)?$

先頭から見ていきます。



\v

これは very magic を使う、という意味です。記号に \ が溢れ返るのを防ぎます。

:help /\v



s%[ubstitute]

これはコマンド名の部分ですね。%[] は、コマンドを表現するのに便利です。別の書き方をすると、

s%(u%(b%(s%(t%(i%(t%(u%(te?)?)?)?)?)?)?)?)?

これの省略形です。

:help /\%[]



([\x00-\xff]&[^\\"|[:alnum:][:blank:]])

これは区切り文字となる部分です。:s/ の / です。
大抵の場合は / を使うと思いますが、実は一部の文字を除く任意の1バイト文字が使えます。
そこで、

任意の1バイト文字

[\x00-\xff]

一部の記号を除く文字(今回の場合は、\"| と英数字です)

[^\\"|[:alnum:][:blank:]]

これらを & で繋ぐことで、どちらも満たした場合、という意味になります。この & は very magic でない場合は \& なので注意。
後者だけだと、マルチバイト文字にもマッチしてしまうため、それを防ぐために前者のものが必要になります。
また、今回、全体を () で囲っています。これは区切り文字を後で参照するためです。

:help /[]
:help /branch



(%(\\.|.){-})

これは {pattern} の部分です。キャプチャするために () で囲ってあります。
{-} は最短一致です。Perl などで言うところの *? です。全体のパターンを見ると、次は \1 になっており、つまり区切り文字が来るまで、という意味になります。最短一致にしないと、末尾にある区切り文字まで見に行ってしまいます。
ここのキモなのですが、単純に .{-} とすると、\ でエスケープされた区切り文字で止まってしまいます。そこで、

%(\\.|.)

これです。これは \ を見付けた場合、次の任意の1文字もマッチに含めます。こうすることで、\ の直後の区切り文字は {pattern} に含まれるようになります。
これを逆に、%(.|\\.) と書くと、先に任意の 1 文字である . がマッチしてしまうので、うまく行きません。| で区切る順番はとても重要です。
%() は後方参照を伴わないグループです。

:help /\{
:help /\|
:help /\%(\)



%(\1(%(\\.|.){-})%(\1([&cegiInp#lr]*))?)?$

残りの部分を見ていきます。まず、全体が %()? で囲まれています。これは、 :s コマンドの入力中、{string} を入力していない段階でも各部分を取得したいという目的があったためです。ない場合はこの %()? は必要ありません。
更に内側を見ていきます。



\1

これは上で行っていた、([\x00-\xff]&[^\\"|[:alnum:][:blank:] ]) の部分にマッチした文字列と同じ文字列、つまり / などになります。

:help /\1



(%(\\.|.){-})

{string} の部分です。このパターンは {pattern} に使ったものと同じものです。



%(\1([&cegiInp#lr]*))?

フラグの部分です。
まず、フラグの部分は終わりの区切り文字も含めて省略可能なので、全体を %()? で囲っています。
次に、区切り文字が来た後、任意のフラグ列がマッチするように以下のようにしています。

([&cegiInp#lr]*)

これらの文字は単純にフラグとして有効な文字のリストです。[flags] として取得したいので、全体を () で囲っています。


最後に、全体を ^ と $ で囲っています。これにより、正規表現が文字列全体にマッチするように強制して、中途半端に誤爆してマッチするのを防ぐようにしています。


以上、正規表現の解説でした。
こうして見ると、暗号のように見える正規表現でも、きちんと意味が…あるにはあるのだけど、暗号には違いないと思います。正規表現の可読性の低さは異常ですね。キモい。

明日は @manga_osyo さんです。