正規表現は短い表現でたくさんのパターンを記述できるため適切に使えばとても便利な機能です。しかし、雑に正規表現を使ってしまうと思わぬバグになったり、わかりにくいプログラムになってしまいます。「動く」正規表現は書けるけど、「適切な」正規表現はまだ書けない、という初級者向けの話です。
例: 拡張子を変更する
正規表現を使って実現することが多い処理として「拡張子を変更する」という処理があります。次のような処理です。
pdf_file_name = ps_file_name.sub(/.ps$/i, ".pdf")
この処理では「XXX.ps」や「XXX.PS」といったファイル名を「XXX.pdf」というファイル名に変更します。
これで問題がないと思いますよね?でも、問題があるんです。問題があるのは例えば次のケースです。
eps_file_name = "XXX.eps"
pdf_file_name = eps_file_name.sub(/.ps$/i, ".pdf")
p pdf_file_name # => "XXX..pdf"
この場合は拡張子が「.eps」なので何もしないで欲しいところですが「..pdf」になっています。
これは正規表現が「マッチしすぎている」からです。ちなみに、マッチしすぎる正規表現は初心者の人がやりがちな雑な正規表現の使い方です。丁寧に正規表現を使えるようになると、「この人はしっかりしているな…!」と一目置かれることでしょう。
マッチしすぎる正規表現
まず、どこがマッチしすぎていたかを確認しましょう。
/.ps$/i
この正規表現の最初にある「.
」が注目するポイントです。「.
」は任意の1文字にマッチするので「.」そのものにもマッチしますが、「e」など他の文字にもマッチします。
初級者の人は「マッチするパターンだけを確認して大丈夫」と判断する傾向があるようです1。初級者からステップアップするために、「マッチしすぎていないか」も確認するようにしましょう。
マッチしすぎる正規表現の問題
では、「マッチしすぎる」となにが問題なのでしょうか。1つは、正常なデータを壊れたデータにしてしまうことです。もう1つは、コードを読むときに理解しにくくなることです。
マッチしすぎる正規表現はエラー処理を省略することよりも深刻な問題になることがあります。エラー処理を書いてない場合は(例外機能がある言語の場合は)エラーでプログラムが止まりますが、マッチしすぎる正規表現の場合は正常なデータを壊れたデータにし2、そのまま処理が継続します。問題が発生したときに止めていれば被害が最小限にとどまったり、原因を見つけやすくなりますが、問題が発生したまま処理を継続すると被害が拡大したり、原因究明を難しくしたりしてしまいます。
コードを読んだときに理解しにくくなることもマッチしすぎる正規表現の問題です。「あれ、この処理だとこの正規表現はマッチしすぎる気がするんだけど。。。なにか特別な意図があるんだろうか。。。」と勘ぐってしまい、理解に手間取ってしまいます。
マッチしすぎる正規表現を防ぐポイント
マッチしすぎる正規表現を防ぐ方法はいくつかあります。たとえば次の方法です。
- 他の人にも確認してもらう。
- メタ文字を意識的に注意する。
チームのみんながそれぞれのコードを読む文化になっていると「他の人にも確認してもらう」は実現しやすいでしょう。そうでない場合は、まわりの人にお願いすることになります。
メタ文字は特別な意味のある文字で「.
」もメタ文字の1つです。これらを意識的に注意することでマッチしすぎないかを見つけやすくなります。中級者以上の人はこれをやっているはずです。
なお、fluent/fluentd-ui@a28b710のケース(↓)のように式展開後の値にメタ文字が含まれるケースもあるので注意してください。これは中級者でも気づきにくいケースです。
FILE_EXTENSION = ".conf".freeze
# ...
@note = note || Note.new(file_path.sub(/#{FILE_EXTENSION}$/, Note::FILE_EXTENSION))
ちなみに、今回の例ではファイル名の終端を表すために「$
」を使っていましたが、Rubyの場合は「\z
」の方が適切です。違いは「$
」は行末、「\z
」は文字列の最後を示すという点です。しかし、通常はファイル名に改行は入らないので、「拡張子を変更する」という機能であれば「妥当なファイル名を渡すのは呼び出し側の責任」と設計することで「$
」でも問題なくすることもできます。
まとめ
初級者がついやってしまいがちなマッチしすぎる正規表現とその問題、およびその対策について説明しました。中級者を目指している人は意識してみてください。