パターンマッチング

パターンマッチングとは、有り体に言えば検索機能のことだ。 特にキーワードとなる文字列を、ファイルのような大量のテキストデータと比較することをいう。 パターンマッチングは Perl の得意分野のひとつで、単なる比較だけでなく、書き換えなどの操作処理もこなすことができる。 手始めに、単純なプログラムを作ってみることにしよう。 grep コマンドを真似た、文字列を含む行を表示するプログラムだ。

Perl では、ファイルから行単位で入力するループを簡単に書くことができる。 それにパターンマッチに必要な命令を追加するだけで、grep モドキは完成だ。 これを「なんちゃって grep」の意味で ngrep と命名しよう。 最初の nperl のプログラムは次のようになる。 perl のパスは適切なものに書き換え、実行ビットを立てるのを忘れないようにしよう。

#!/usr/bin/perl

while (<>) {
	if (index($_, 'keyword') >= 0) {
		print;
	}
}

index は単純な文字列マッチングのための関数だ。 一般的には次のような形式となる。

index(str, substr)

str で示される文字列から、substr を検索し、見つかったらその文字列が str の何文字目から始まるかを返す。 文字列の位置は、最初が 0 となっている。 たとえば「this is the keyword」という文字列から「keyword」を検索すると、index は 13 ではなく 12 を返す。 もし substr が見つからなければ、index は負の数(普通は -1)を返す。 上記のプログラムでは、index が 0 以上の数を返したときだけ print を実行している。 つまり文字列を含む行だけが出力されるわけである。

このバージョンでは、ngrep は「keyword」という文字列を検索することしかできない。 別の文字列を探すのに、いちいちプログラムを書き換えるのは面倒だ。 そこでコマンドラインから検索文字列を指定できるようにしよう。

<> を説明したとき、一瞬だけ <ARGV> という表現を紹介した。 実は ARGV というのは、コマンドラインで指定された文字列を保持しているシステム配列なのだ。 >> は、ARGV を解析してファイルをオープン、行ごとに読み込む処理を自動的に行っている。 このループの前に、コマンドラインの最初の文字列だけをパターンとして取り込んでしまえばよいのだ。

#!/usr/bin/perl

if ($#ARGV < 0) {
	print STDERR "usage: ngrep pattern [file...]\n";
	exit;
}
$pattern = $ARGV[0];
shift @ARGV;

while (<>) {
	if (index($_, $pattern) >= 0) {
		print;
	}
}

まず新しく加わった if から見てみよう。 ここでは配列 @ARGV の要素数を調べている。 $#ARGV で分かるのは、配列の最後の要素の添字の値だ。 もし @ARGV が空っぽなら、その値は -1 になる。 引数が1つだけ指定されていたら、その値は 0 だ。 つまり引数の数より1少ない値になっている。 そこで、$#ARGV が 0 より小さいことをチェックし、そのときはエラーメッセージを出力するのだ。 print 文に、STDERR という余分な指定がついているのに注目してほしい。 これは print の出力を、通常の標準出力ではなく、エラー出力へ向けなおしているのだ。 さもないと、nperl の出力がパイプやリダイレクトで画面以外に切り替えられてしまうと、エラーメッセージも一緒に切り替えられた方へ出力されてしまい、画面に表示されなくなってしまうからだ。 パイプやリダイレクトは、標準出力を変更するだけで、エラー出力は変更しない。 ユーザの目にさらすために、エラーメッセージは STDERR へ向ける必要があるのだ。 このとき注意すべきなのは、STDERR を指定するときコンマ「,」で区切ってはいけないことである。 奇妙な文法規則だが、仕方がないので覚えてしまおう。

次に @ARGV からパターンを取り出す部分だ。 最初の「$pattern = $ARGV[0];」は、通常の配列アクセスなので問題はないだろう。 次の「shift @ARGV;」は、今読み取ったパターンを ARGV から削除している。 こうしておかないと、パターンのつもりで指定した文字列が、次の >< でファイル名として解釈されてしまう。 shift は最初の要素、つまり添字が 0 の要素を配列から削除し、残りを詰めてくれる。 これで >< が実行されるときには、パターンを取り除いた残りのコマンドラインが処理されることになるのだ。

while ループの中では、index に与える引数が変更されている。 これまで固定の文字列だった部分が、$pattern という変数に置き換わった。 これで次のようにして、コマンドを使うことができる。

% ./ngrep index ngrep
	if (index($_, $pattern) >= 0) {
% ./ngrep login名 /etc/passwd
login名:1000:1000:Your Name:/home/login名:/bin/csh

さて、このプログラムを元にどんどん機能を追加していくのも面白いが、とりあえずパターンマッチング機能を強化して行こう。 次は正規表現だ。

正規表現とは、シェルのワイルドカードのように特殊な意味を持つメタ文字を使って、パターンを表わす手段だ。 正規表現は大変強力で、慣れれば複雑なパターンを一発でマッチさせることができるようになる。 まず、一般的な正規表現パターンを紹介しよう。

文字 メタ文字以外の文字は、その文字そのものにマッチする。
. 任意の1文字。
[...] カッコ内の文字のうちのどれか1文字。
[^...] カッコ内の文字以外の1文字。[...] の逆。
* 直前のパターンの0回以上の繰り返し。
+ 直前のパターンの1回以上の繰り返し。
^ 行頭。
$ 行末。

他にもたくさんあるのだが、多すぎると覚えるのが大変なので、基本的なものだけにとどめておく。 一番分かりづらいのは * と + だろう。 直前のパターンの繰り返しとは、たとえば a* は「a」「aaa」「aaaaaaaa」にマッチする。 ポイントなのは、* は「0回以上の繰り返し」なので、ゼロ文字「」にもマッチするということだ。 また直前のパターンだけを繰り返すから、abc*def というパターンは「abcdef」「abccccdef」にマッチするが、「ababacdef」にはマッチしない。 また、c は0文字でもよいので「abdef」にはマッチしてしまう。 もし「ababacdef」にマッチさせたいなら、[abc]*def というパターンが使える。 このパターンの意味を日本語で書けば、「aまたはbまたはc が続き、最後にdefで終わる文字列」ということになる。 + は、* とちがって 1回以上の繰り返しだから、[abc]+def は「def」だけにはマッチしない。

このような正規表現を使えるようにするには、次のようにプログラムを変更する。

#!/usr/bin/perl

if ($#ARGV < 0) {
	print STDERR "usage: ngrep pattern [file...]\n";
	exit;
}
$pattern = $ARGV[0];
shift @ARGV;

while (<>) {
	if ($_ =~ /$pattern/) {
		print;
	}
}

index の代わりが =~ 演算子だ。 これは正規表現を使ったマッチングを、左辺に対して適用するという意味の演算子だ (比較演算子とはちょっと違うので注意すること)。 マッチングパターンを表わすのが // だ。 この間に指定された文字列が、正規表現で表わされたパターンと見なされ、$_ に対して適用される。 その結果、パターンが一致すれば真となるわけだ。 ちなみに // は、ダブルクォートと同じで変数名を展開してくれるので、「$pattern」という文字列がそのままパターンと見なされることはない。 もしそういう文字列をパターンとして指定したいなら「\$pattern」と表記しなければならない。

% ./ngrep print ngrep
	print STDERR "usage: ngrep pattern [file...]\n";
		print;
% ./ngrep 'p.*t' ngrep
	print STDERR "usage: ngrep pattern [file...]\n";
$pattern = $ARGV[0];
	if ($_ =~ /$pattern/) {
		print;
% ./ngrep ^if ngrep
if ($#ARGV < 0) {

最初の例は、通常の文字列と同じ print を検索している。 2番目の例は、p.*t というパターンを検索している。 意味は「p に続いて任意の文字列があり、最後が t で終わっている文字列」だ。 だから print ももちろんマッチするが、 「pattern」という文字列の中の「patt」の部分もマッチしている。 * は最長一致という性質があるので、上記の例では「print STDERR "usage: ngrep patt」とう長い文字列が一致している。 ngrep は一致したパターンを含む行を表示しているので、その区別はつかないが。 3番目の例は、if で始まる行を検索している。 ^ のせいで行頭から始まる if しか表示していないのが分かる。

そうそう、忘れるところだったがポイントがいくつか。 まず shift は、引数を省略すると @ARGV を対象としてくれるのでいちいち指定しなくてもよい。 また // も、この文脈ならば $_ が対象なのは明らかなので省略できる。 というわけで最終的なプログラムはこうだ。

#!/usr/bin/perl

if ($#ARGV < 0) {
	print STDERR "usage: ngrep pattern [file...]\n";
	exit;
}
$pattern = $ARGV[0];
shift;

while (<>) {
	if (/$pattern/) {
		print;
	}
}

[back to index]