とあるソフトウェア製品での出来事。
あるバグフィックス・アップデートを適用して以来、それまでは問題なかった入力値でValidation Errorが出るようになった。丸一日調べ回った結果、以前は
^[a-z0-9_\-][a-z0-9_\.\-]*$
だった入力チェックの正規表現が、
^[a-z0-9_\-][a-z0-9_\.\-]*[^x][^y][^z]$
に変わったため、と判明した(xyzは架空)。どうやら「末尾がxyzで終わってはいけない」という条件を追加したかったようだが、この正規表現は完全に誤りだといえる。これだと最低でも4文字以上ないとマッチしないし、’boxes’のような正当なはずの文字列にもマッチしないし、逆に’a+:^’のような不正であるべき文字列にマッチしてしまう。
では「末尾がxyzで終わってはいけない」を表す正しい正規表現とはどんなものだろうか。戻り読みが使える環境なら、否定戻り読みを使うのが簡単だと思う。
irb> my_answer = /^[-a-z0-9_][-a-z0-9_.]*(?<!xyz)$/ => /^[-a-z0-9_][-a-z0-9_.]*(?<!xyz)$/ irb> my_answer =~ "a_xyz" => nil irb> my_answer =~ "xyz" => nil irb> my_answer =~ "a" => 0 irb> my_answer =~ "abcdef" => 0 irb> my_answer =~ "abc_xyz_def" => 0
戻り読みが使えないとなると……ちょっと思いつかない。条件式を2つに分けるしかないだろうか。
ひっくり返して「xyzで始まってはいけない」にすると、否定先読みを使うことになる。例えば xyz で始まらない単語文字列なら
^(?!xyz)\w+$
さらに厳しくして「xyzではない」にするとどうだろう。…これは先読み/戻り読みだけでは無理かな?長さの条件も加えて
^\w{4,}$|^(?!xyz)\w+$
といったところか。まあいずれにせよ、実際に使う機会は稀だろうが。
ちなみに件の製品は戻り読みが使えない glibc の regex を使っていたので、諦めてアップデート前の形に戻した。