Bashで正規表現をやろうとしたら 否定先読み(Negative Lookahead) が出来なくてハマった話

こんにちは。DI部の大高です。

最近、Bashで正規表現処理をする機会があり、がっつりハマったのでご紹介します。

やりたかったこと

やりたかったのは、^(?![0-9])[0-9A-Za-z_]+$ という正規表現パターンでのエラーチェックです。実際の挙動は以下のオンラインサービスで確認していました。

RegExr: Untitled 4giee

このパターンは「先頭は数字で始まっちゃ駄目だけど、英数字とアンダースコアを許容する」という正規表現パターンです。 01_NGはNGで、OK_01 はOKみたいな感じです。

Bashで正規表現のチェックってどうするの?

BashではVersion 3から ~= というオペレーターで比較できるよ!という情報を得たので、確認してみました。こんな感じです。(サンプルなので冗長に書いてます)

#!/bin/bash

regex='^(?![0-9])[0-9A-Za-z_]+$'
ok_text='OK_01'
ng_text='01_NG'

if [[ ($ok_text =~ $regex) ]]; then
  echo "OKパターンだよ"
else
  echo "NGパターンだよ"
fi

if [[ ($ng_text =~ $regex) ]]; then
  echo "OKパターンだよ"
else
  echo "NGパターンだよ"
fi

で、実行すると両方とも NGパターンだよ と言われてしまいました。そんな馬鹿な…!

何がいけないのか

色々調べたところ、「BashではPCRE(Perl互換の正規表現)である否定先読み(Negative Lookahead)をサポートしていない」ということが分かりました。

Bash double bracket regex comparison using negative lookahead error return 2 - Stack Overflow

The conditional expression [Bash Hackers Wiki]

Perl Compatible Regular Expressions - Wikipedia

どうすればいいの?

色々とワークアラウンドはありそうでしたが、一番シンプルな2つに分けるという方法を取ってみました。

bash - Regex negative matching trouble - Server Fault

先程のコードを書き換えると以下のようになります。否定条件と肯定条件を分ける感じです。

#!/bin/bash

#regex='^(?![0-9])[0-9A-Za-z_]+$'
positive_regex='^[0-9A-Za-z_]+$'
negative_regex='^[0-9].*$'
ok_text='OK_01'
ng_text='01_NG'

if [[ ($ok_text =~ $positive_regex) && !($ok_text =~ $negative_regex) ]]; then
  echo "OKパターンだよ"
else
  echo "NGパターンだよ"
fi

if [[ ($ng_text =~ $positive_regex) && !($ng_text =~ $negative_regex) ]]; then
  echo "OKパターンだよ"
else
  echo "NGパターンだよ"
fi

この場合は、ちゃんと以下のように出力されました。

OKパターンだよ
NGパターンだよ

まとめ

Bashでは否定先読み(Negative Lookahead)できないので、お気を付けください。

どなたかのお役に立てれば幸いです。それでは!