mod_rewrite,RewriteCondのバックリファレンス

先週の13日に書いたエントリで、Ifブロック中ではmod_rewriteのRewriteRuleバックリファレンス($1とか$2とか)が使えないと書きました。
そこで、RewriteCondのバックリファレンス(%1とか%2とかに書き換え中。これがなかなか難敵ですね。今日遭遇した現象はこんなの。

だいいち、正規表現が複雑なんだよ。

えーとですね。具体的にはかけないのでちょっと文字列を変えながら説明します。URIをリライトするんですが、そのルールが条件で書くと結構単純なんですが正規表現にするとまぁなんとも複雑怪奇。
書き換えて欲しいURIはこんな。

  • /hoge/fuga/xxxxxxx
  • /hoge/fuga/xxxx_yyyy

これを

  • /hoge/fuga/foo/xxxxxxx
  • /hoge/fuga/foo/xxxx_yyyy

に書き換える。

でも単純に書き換えるわけじゃなくて、書き換えてほしくないURIもある。
書き換えてほしくないURIはこんな。

xxxxやyyyは任意なんですが、XXXとYYYは特定の文字列。

暗号だな

でもって、 最初にRewriteCondに書いた文字列はこんな。

RewriteCond    %{REQUEST_URI}    ^/hoge/fuga(?!(.html|/($|foo|(XXX|YYY)(.html|/)?)))
RewriteRule    ^/hoge/fuga/(.*)    /hoge/fuga/foo/$1

なんだこりゃ。/hoge/fugaに続いて、.htmlがない、/で終わらない、XXX.htmlとかでも終わらないって表現。
自分で書いてなんだけど一週間後には読めなくなってる自信がある。
これをRewriteCondのバックリファレンスで置き換える。/hoge/fugaに続く文字列をキャプチャすればいいので、否定先読み部分を括弧でキャプチャする。

RewriteCond    %{REQUEST_URI}    ^/hoge/fuga((?!(.html|/($|foo|(XXX|YYY)(.html|/)?))))
RewriteRule    ^/hoge/fuga/(.*)    /hoge/fuga/foo%1

動かない

残念ながらこれ動きません。RewriteCond部分はちゃんと引っ掛けてくれてRewriteRuleへ誘導してくれますが、意図した部分がキャプチャされない。
"/hoge/fuga/foo/" が 404だぞって言われます。%1が空っぽなんですね。

よく考えるとそりゃそうだ

というのは、"先読み否定"は正規表現でも文字列リテラルではなくて場所を表す表現。位置はキャプチャできませんね。なので%1にはなにも入ってこない。
このようにして解決。

RewriteCond    %{REQUEST_URI}    ^/hoge/fuga((?!(.html|/($|foo|(XXX|YYY)(.html|/)?))).*)
RewriteRule    ^/hoge/fuga/(.*)    /hoge/fuga/foo%1

キャプチャ部分に任意の文字列表現を追加。これで正常にキャプチャしてくれるようになりました。

ところで

いつもmod_rewriteの表現を書くときには"grep -P"をつかって試験しています。それとバックリファレンスを使う場合はperlでも試験するんですが、perlの文字列置換だと上記の"動きません"と書いた表現でもちゃんとキャプチャしてくれるんですね。
適当にURIを書いたファイルを /tmp/mm へいれて以下のワンライナー

$ perl -ne "s#/hoge/fuga((?!(.html|/($|foo|(XXX|YYY)(.html|/)?))))#$1#' /tmp/mm

同じPCELでもインプリによって動作に違いがあるのはよくあることですが。