「初めてのPerl」: 8章正規表現の詳細

段々、内容が難しくなってきました。昨日は8章の「正規表現の詳細」を勉強しました。

イデオム

今まで正規表現grepやviでごく簡単な正規表現しか使ったことがなかったので、この章は勉強になりました。
そこで、新しく覚えたイデオムを1つ紹介。以下の正規表現は、改行文字も含めてあらゆる文字にマッチさせたい場合によく使われる書き方なのだそうです。へぇ〜。正規表現も、どの言語でも同じように、イデオムをできる限り蓄積して、それを適切な場面で使えるかどうかがポイントですね。

[\d\D]

私のgrep

私が普段よく使う正規表現は、本当にシンプルです。例えばPHPで書かれたsetURL()という関数の定義を調べたいと思ったら、以下のようにgrepします。大抵の場合、これで十分です。メタキャラクターは使いません。

$ grep -rn "function setURL(" .
./libs/BLOG.php:765:    function setURL($val) {
./libs/MEMBER.php:403:  function setURL($site) {

むしろ使わない方が、ある関数の定義を探したい場合は良いです。なぜかというと、classで定義されているfunctionを見つけたい場合、大抵「function」の前に空白(インデント)があるからです。こんな感じでキャレットアンカー(^)を使ってしまうと、class内に定義されるお目当てのfunctionは恐らく見つかりません。

$ grep -rn -e "^function setURL(" .

ちなみにFreeBSD系のソースでよく見られる命名規則だと、以下のような感じなので単に関数の定義を探したい場合は、キャレットアンカー(^)が大活躍しそうですね。(以下は、私たちがよく使う「ls」のソースの一部です。)

[ys@freebsd]$ vi /usr/src/bin/ls/ls.c
[...]
static void
traverse(int argc, char *argv[], int options)
{
    FTS *ftsp;
    FTSENT *p, *chp;
    int ch_options;
[...]

1回のgrepでゴミが沢山出てわかりにくい場合は、コマンドをパイプで連結してもう一度grepすれば良いのです。私はよくそうします。

$ grep -rn "function foo(" . | grep "buz"

とまあ、コマンドにおける日常生活ではメタキャラクタを含む正規表現を使わなければ全く歯に立たないといった場面に遭遇することは極まれです。私の場合はそうです。

後方参照とメモリ変数

今までPerlで「$1」といったメモリ変数は使ったことがあったのですが、正規表現に埋め込まれた「\1」のような後方参照は使ったこはありませんでした。勉強になりました。

練習問題: Test::Moreによる些細なHack

4つ目の問題をちょっと頑張ってみました。その問題は以下のような問題です。

同じワードが連続して2回以上出現する行にマッチするパターンを作成しましょう。この問題では、ワードは、aからz、AからZ、数字、下線が連続するものと定義しましょう。ワードの間に置かれる空白文字は違っていてもかまいません。[...]

最初は、ごにょごにょ正規表現を書いてはテスト、といったサイクルを繰り返していたのですが、非常に面倒に感じました。何でこんな単純作業を繰り返さないといけないのだろう、と。そうだ!宮川さんの「PerlStyle」で「Test! Test! Test!−Perlのテスティングモジュール」で勉強したじゃないか!と思い、早速その記事で紹介されていたTest::Moreで、ちょっと書いてみました。まだPerlの文法の一部しか知らないので、ちょっと不細工ですが、十分役に立ちました。書いたコードは以下のようなものです。

vi ~/perl.life/learning.perl/chap8/regex_tester
#!/usr/bin/perl -w
use strict;
use Test::More tests => 5;

sub regex {
    $_ = shift;
    if (/(\b\w+\b)\s+(\b\1\b\s*)+/) {
        return 1;
    } else {
        return 0;
    }
}

my %test_strings = (
    'Paris is the the spring' => 1,
    'I think that that is the problem' => 1,
    'I think that that   that is the problem' => 1,
    'This is a test' => 0,
    'the theory of regular expressions' => 0,
);

while ( my($string, $expected) = each %test_strings ) {
    is regex($string), $expected, "\'$string\'";
}

テストしたい文字列と、それをregexサブルーチンに入力して得られた結果の期待値をハッシュに持たせています。ハッシュに格納すると、記述した順番通りにはeach演算子で取り出せませんが、まあ順番はこの際気にしないことにしました。Test::Harnessで綺麗にレポートを整形しても良かったですが、今回はそこまでやる必要性は感じなかったのでやめました。それと、最初のほうに「use Test::More tests => 5;」のように書いて、テストケースの個数をハードコーディングしていますが、良い方法が思いつかなかったのでとりあえずハードコードしました。

実行すると以下のような結果が得られます。すべてうまくテストに成功していることがわかります。

$ ~/perl.life/learning.perl/chap8/regex_tester
1..5
ok 1 - 'the theory of regular expressions'
ok 2 - 'I think that that   that is the problem'
ok 3 - 'I think that that is the problem'
ok 4 - 'This is a test'
ok 5 - 'Paris is the the spring'

わざと「Paris is the The spring」のように「the」を「The」に変えると、見事に失敗します。失敗してもこんなに親切にレポートしてくれるので、本当に楽チンです。Test::More最高ですね。

$ ./regex_tester
1..5
ok 1 - 'the theory of regular expressions'
ok 2 - 'I think that that   that is the problem'
ok 3 - 'I think that that is the problem'
ok 4 - 'This is a test'
not ok 5 - 'Paris is the The spring'
#     Failed test (./regex_tester at line 25)
#          got: '0'
#     expected: '1'
# Looks like you failed 1 tests of 5.

今までにTest::Moreでテストコードを書かれたことがない方は、一度試してみられることをお勧めします。CPANを覗けば、いっぱいテストコードが見つかります。ご参考までに。

初めてのPerl