Java正規表現のフラグ指定と埋込みフラグ表現

バグ修正をしていたところ、 java.util.regex.Pattern の埋込みフラグ表現が原因だった。

Java正規表現のフラグ指定や埋込みフラグ表現について、周囲に知っている人がいなかったのでメモ。

フラグ指定

Pattern.compile(String regex, int flags) で、指定したフラグを有効にして正規表現コンパイルできる。

古いAPIなので、フラグはPatternにstaticなintフィールドとして設定されている。複数のフラグを指定する場合、それぞれ加算して渡す。

よく使われるものだと、US-ASCII文字セット内の文字について大文字と小文字を区別しないマッチングを行う場合、 Pattern.CASE_INSENSITIVE を指定する。

埋込みフラグ

文字列内に埋込みフラグ(embedded flag expression)を指定することでも、前述のフラグ指定と同じことが行える。

Pattern.CASE_INSENSITIVE を埋込みフラグで行うには、コンパイルする正規表現文字列の先頭に (?i) を記述する。

コンパイル時のフラグ指定と埋込みフラグの差異

フラグを指定してコンパイルした場合、その正規表現全体でフラグが有効となる。

一方、埋込みフラグを使用した場合、記述位置以降の文字に対して有効となる。

記述位置以降で、フラグがONになる、といった動きの模様。

また、フラグをOFFにすることもできる。 (?i) をOFFにする場合、 (?-i) を記述する。

埋込みフラグでON/OFFを切り替えることにより、文字列の一部のみ大文字小文字を無視してマッチング、といった記述が行える。

今回発生したバグについて

先頭に (?i) が付与された正規表現パターンが使用されていたが、意図していない文字列までマッチさせていた。

だいぶ昔に書かれており、なんとも判断がつかないが、おそらくそのコードを記述した人は、埋込フラグの直後の1文字のみ対象となると考えていた模様。

ざっくりテスト

Groovyでサクッと確認。

[/(?i)test/, /t(?i)e(?-i)st/].each { p ->
  println "pattern: /${p}/"
  println 'test' ==~ p
  println 'Test' ==~ p
  println 'tEst' ==~ p
  println 'teSt' ==~ p
  println 'tesT' ==~ p
  println 'TEST' ==~ p
}

動かすと、以下の出力となった。

pattern: /(?i)test/
true
true
true
true
true
true
pattern: /t(?i)e(?-i)st/
true
false
true
false
false
false

振り返り

ちょっと難しいことをやろうと思ったら、Javadocは読んでおいたほうがいいね。

docs.oracle.com