読者です 読者をやめる 読者になる 読者になる

正規表現のキャプチャ

正規表現のキャプチャの結果を取得したいときには、次のようなコードを書く:

if (my @capture = '10/17' =~ m!([0-9]{2})/([0-9]{2})!) {
    # @capture = (10, 17)
}

$1や$2を使っても取得はできるが、キャプチャが増えたときに$1, $2, $3, $4, ...と増やしていくのは苦行である。
このようにすると、@captureにキャプチャの結果が入る。しかし、キャプチャの括弧がない正規表現の場合、@captureの値はどうなるのか。

if (my @capture = '10/17' =~ m![0-9]{2}/[0-9]{2}!) {
    # @capture = (1)
}

キャプチャされていないのに、@captureには(1)が代入されている。空リストはfalse扱いなので、こういったケースのために(1)を返すみたいだ。
しかし、@captureは空リストであるべきなので、どうすればうまく解決できるか、Hokkaido.pm Casual#6で質問したところ、@jamadamさんが教えてくれた。@jamadam++
Marquee/lib/Marquee/Plugin/Router.pm at master · jamadam/Marquee · GitHub

if (my @captures = ($path =~ $regex)) {
    $cb->(defined $1 ? @captures : ());
    last;
}

$1に何も入っていない場合は、キャプチャしていないと判断して、空リストにしているみたいだ。でもちょっと待った。キャプチャされる場合とされない場合がある正規表現だと、どうなる?:

if (my @capture = $filename =~ /.+?(?:\.(.+))?$/) {
    @capture = () unless defined $1;
    # @capture is?
}

この正規表現は以下の通りにマッチする。

  • 'file.jpg'
    • @capture = ('jpg');
  • 'file.tar.gz'
    • @capture = ('tar.gz');

ここまでは、問題なくキャプチャできる。しかし、この正規表現はキャプチャしない場合も考えられる:

  • file
    • @capture = ();

この場合、@captureが(undef)になるべきだと思うので、$1を使った方法ではうまく解決できないことが分かる。なぜ(undef)であるべきか:

if (my @capture = '10:12 rainy' =~ /(\d+):(\d+)(?::(\d+))?\s+(.+)/) {
    # @capture = (10, 12, undef, 'rainy')
    # この場合では、undefが代入されている!
}


perldoc perlvarあたりが怪しいよね、と@aloelightさんが言っていたので、読みあさってみたところ特殊変数@+を発見した。

You can use $#+ to determine how many subgroups were in the last successful match.

$#+でキャプチャの数が分かるみたいだ。キャプチャの数が分かれば、あとは簡単である。
また、@-という特殊変数もあり、同じように$#-とするとキャプチャの数が分かる。$#-は、*マッチした*キャプチャの数を返し、$#+はマッチして(?:いる|いない)キャプチャの数を返すといった違いがあるみたいだ。
最終的に、今回の問題は$#+を使って、キャプチャの数を見て判別することにした。Perlの特殊変数は奥が深すぎて、大半の特殊変数は知らないものばかりですね :)

if (my @capture = $filename =~ /.+(?:\.(jpe?g|png|gif))?/) {
    @capture = () unless $#+;
    # @capture is?
}
  • file.jpeg
    • @capture = ("jpeg");
  • file.png
    • @capture = ("png");
  • file
    • @capture = (undef);

追記

最後の例ですが、/.+?(?:\.(.+))?$/に書き直そうとして忘れてました。もし、そのままの形で修正するとなると、/.+?(?:\.(jpe?g|png|gif))?$/となりますね。(thanks kitsさん: /.+(?:\.(jpe?g|png|gif))?/.+ で文字列の末尾までマッチするので、()部分では何もキャプチャされない。)
()?を使わないほうがいい、というのはおっしゃる通りです。実際、使ったことないです :)