おでーぶでおでーぶ

いろいろ書く。いろいろ。

3つの正規表現を知らないとハマるヨ

古来の正規表現 = 標準正規表現、basic regular expression、BRE

ダン正規表現 = 拡張正規表現、extended regular expression、ERE

独自にさらに拡張された拡張正規表現 = 長いので、超拡張正規表現とします (ちなみにもう正規言語の域を超えた)

の3種類が存在するのだけれど、みんなあまり気にしていない気がする。

BRE と ERE の違い

便宜上、左を拡張正規表現であるEREにしており、違う部分だけを抜粋(したつもり

ERE BRE 用途 代替表現
| unsupported OR 表現 I don’t know
+ unsupported 1文字以上の繰り返し \{1,\}
? unsupported 0 or 1文字 \{,1\}
() \(\) グループ化
{n,m} \{n,m\} n文字以上、m文字以下の繰り返し

であれば、詳しくは以下のコマンドを叩いて、読んでみると面白いです。(Macでしか確認していません)

man re_format

超拡張表現との比較が非常に多くなり、色々と面倒なので割愛するけれど、例として POSIX クラスを挙げておく。
e.g. 超拡張表現では POSIXクラスを [:space:] として表せるが、BRE、ERE共に [[:space:]] としなければならない。

それぞれの違いを例示してみる

他に詳しく違いを並べる時間もないので、例を出して「色々知っておかないとどはまりするかもよ」ということを以下で、備忘録という意図も含めて書いておきたい。
拡張正規表現は大体のプログラミング言語で扱われていて便利な分、尚更違いを知っておくべきである。

2つの例を用意する。

1つ、Mac で private ipを引くために、sed を使って ifconfig からぶっこ抜くことを考える。 2つ、axyzaopqalmn という文字列から aから始まり、aで終わる文字列 にマッチさせることを考える。

Mac で private ip を引く時の表現の違い

まずは1つ目。

一応 ifconfig の usage を超簡潔に示す。

ifconfig [interface name] inet

これで対応する interface の inet が出て来る。以下が出力例。

en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        inet xxx.xx.x.xxx netmask 0xfffffxxx broadcast xxx.xx.x.255

ここからぶっこ抜くために、全体マッチさせてipの部分で置換することを考える。 (ifconfig en0 inet|tail -1 を流し込むとする。)
たくさん思いつきますね?

# BSD sed
sed 's/[[:blank:]][[:blank:]]*inet \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/g'

# BSD sed
sed 's/[[:blank:]][[:blank:]]*inet \([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*/\1/g'

# BSD sed
sed 's/[[:blank:]]\{1,\}inet \(\([0-9]\{1,3\}\.\)\{3\}[0-9]\{1,3\}\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet (([0-9]{1,3}\.){3}[0-9]{1,3}).*/\1/g'

( :digit: は長すぎるので使っていません )

BRE vs ERE で、グループ記法 ()、繰り返し記法 {n,m}、1以上繰り返し + の有無辺りに違いが見て取れる。

拡張正規表現を使ってみる with ジャバ

String parttern = "\s+inet ((\d{1,3}\.){3}\d{1,3}).*";
// replace the matched string with 1st group

メタ文字が使えてスッキリ。

axyzaopqalmn という文字列から aから始まり、aで終わる文字列 にマッチさせる時の表現の違い

(面倒くさいのでEREを使います)

特に条件を指定していないので、2つ出来るはず。(最長マッチ、最短マッチ)

echo 'axyzaopqalmn' | sed -E 's/a.+a/ここだよ/' # 最長
echo 'axyzaopqalmn' | sed -E 's/a[^a]+a/ここだよ/' # 最短

拡張正規表現ならば最長マッチは変わらないのだけれど、最短マッチは

a.+?a

で表せる。

結論

拡張正規表現しか知らないと sed とかで組むときにその人の発想が柔軟かどうかみたいな話になってしまうので 拡張正規表現の記法がどんなもののエイリアス(正確な表現ではないのだけれど)になっているか を知っておくべきだと思う。
勿論表現限界のせいで実現できないものも存在するので、知っておいた方がいい。シェル芸人になりたいなら尚更である。

例えば否定後読みなどは JavaScript (Chromium上) ですら1年前に実装されたばかりである。

https://codereview.chromium.org/1418963009

Tips

  • シェルスクリプト内ではBREの方が安全な場合もあるので、BREのみで表現可能であればBREでいいのかもしれない。 e.g.) 変数展開の文字をエスケープし忘れるなどが減る
  • 大体のコマンドは -E オプションでEREを使えるので、EREで表現可能であれば移植性も考えてEREでよい。
  • どうしてもEREでもだめだというなら perlruby にコマンドレベルで委譲するべき。問題ないのであればスクリプト全体をその言語にする必要はない
  • ある言語で定義した正規表現が別の言語だとそのまま使えない場合、バリデーションの統一に支障が出ることを念頭に置く。e.g.) サーバーサイドで定義したバリデーション用正規表現をクライアントで用いるケース

ちなみに正規表現エンジンの勉強がしたいなら以下の本がとてもオススメ。読み物としてもなかなかおもしろいです。