Perlの正規表現のモードに「一行モード」「複数行モード」というものがあります。
$str =~ /^--/m
and print "複数行モードでマッチ\n";
$str =~ /<script.*?<\/script>/s
and print "一行モードでマッチ\n";
定義が分からない方は、後述で定義を解説しています。
一般に Perl では マッチ m//
や置換 s///
の右側(左側ではないことに注意)に伴う上記の m
や s
のような英文字のことを修飾子 (modifier) と言います (see: perlre)。
私はこれをよく間違えたり忘れたりするので、こんな覚え方を考えてみました。
#そもそも名前がややこしい
- 一行モード(Single line mode) m//s は、ドット(.)が 「複数行」 にわたってマッチするようになる
- 複数行モード(Multiple line mode) m//m は、"^" "$" が 「一行」 ごとにマッチするようになる
つまり 一行モードではない ドット(".")は、改行にはマッチしない、つまり [^\n] とほぼ同じ意味です。
また 複数行モードではない "^" や "$" は、検査するテキストが改行文字を含んだ論理的に複数行の場合、改行の前後にはマッチせず、検査するテキストの冒頭("^")と末尾("$")にのみマッチします。
誇張してややこしく説明していると思う方もいるかもしれませんが、私はいつもこれを混同して混乱していました。
それならばmとsを「複数行モード」「一行モード」とは違う覚え方で覚えればいいと発想の転換をしてみました。
#一行モード
一行モード /s の s は 「Super dot mode」 と覚えます。
今までのドットは万能ではなく 、唯一改行文字にマッチしないという特性(欠点かどうかは別として)を持っていましたが、一行モード /s を指定するとドットは万能になり、あらゆる文字にマッチするようになります。
#複数行モード
複数行モード /m の m は 「Multiple Match Mountain」 と覚えます。
今までの山"^"は変数の冒頭にしかマッチしなかったのが、改行の直後にもマッチするようになります。詳しく書けば戻り読みゼロ幅マッチ (?<=\n|) とほぼ同じ意味となります。
"$" も同様に、先読みゼロ幅マッチ (?=\n) と ほぼ 同じ意味になります。
"$" は "^" とは違う癖があるので、後述で詳細を解説します。
#複数行モードの活用
複数行モードで ^ を使うと、メールの冒頭引用のようなことが簡単にできます。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
# /g はグローバルマッチ。何回でもマッチする
$body =~ s/^/> /mg;
print $body;
結果
> Today is fine.
>
> This is a pen.
>
引用符が入りました。
ただ、 $ については ^ からの想像とは違う結果が複数行モードでは発生します。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/$/<<</mg;
print $body;print "\n";
結果
Today is fine.<<<
<<<
This is a pen.<<<
<<<
<<<
余計な (?) <<<
が最後に入っています。
これは複数行モードの $ が、改行の直前にゼロ幅マッチし、かつ文字列変数の末尾の直前にもゼロ幅マッチするからです。
なお、最後に入れた print "\\n";
はコマンドライン出力を読みやすくするために入れただけで、今回の正規表現マッチの話とは直接関係はありません。
改行を可視化してみると分かりやすいです。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/$/<<</mg;
$body =~ s/\n/{LF}/g;
print $body;print "\n";
結果
Today is fine.<<<{LF}<<<{LF}This is a pen.<<<{LF}<<<{LF}<<<
複数行モードの ^ は直感的ですが、$ は少々癖があることに注意するとよいでしょう。本当に改行の直前のみにマッチさせたい場合には明示的に先読みゼロ幅マッチ (?=\n)
を使うと良いでしょう。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/(?=\n)/<<</mg;
print $body;print "\n";
結果
Today is fine.<<<
<<<
This is a pen.<<<
<<<
#複数行モードで文字列の冒頭と末尾を表す \A \Z \z
\A \Z \z という3つの正規表現について解説します。
複数行モードで ^ や $ は文字列の冒頭末尾以外にも、改行の前後にもマッチするようになりました。「複数行モード」で以前の ^ や $ の意味を表したい場合に使われるのが \A \Z そして \z です。
この正規表現トークンの名前の由来は、たぶんアルファベットの一番最初 (\A)が冒頭、アルファベットの一番最後(\Z および \z)が末尾、ということなのだと思います。
\A は ^ 同様、直感的です。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/^/---/mg;
$body =~ s/\A/===/mg;
print $body;
print "\n";
結果
===---Today is fine.
---
---This is a pen.
---
大文字の \A に対応しているように見える大文字の \Z ですが、とりあえず使ってみましょう。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/$/<<</mg;
$body =~ s/\Z/|||/mg;
print $body;
print "\n";
結果
Today is fine.<<<
<<<
This is a pen.<<<
<<<
<<<|||
直感通りのような気がします。改行を可視化してみると、以下のようになります。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
$body =~ s/$/<<</mg;
$body =~ s/\Z/|||/mg;
$body =~ s/\n/{LF}/g;
print $body;
print "\n";
結果
Today is fine.<<<{LF}<<<{LF}This is a pen.<<<{LF}<<<{LF}<<<|||
大丈夫そうです。でも前段の $ の複数行モードのゼロ幅マッチの置換を取り除くと
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
#$body =~ s/$/<<</mg;
$body =~ s/\Z/|||/mg;
$body =~ s/\n/{LF}/g;
print $body;print "\n";
結果
Today is fine.{LF}{LF}This is a pen.{LF}|||{LF}|||
なんか変なことになりましたね。\Z は 文字列変数の末尾が改行の場合 、文字列変数の終端だけでなく文字列変数の末尾にある改行の直前にもゼロ幅マッチしてしまう ようです。これは文字列をバリデートするときにハマる可能性があります。これは $ の挙動を模しているかのようです(後述します)。
そこで厳密に文字列変数の末尾にゼロ幅マッチする \z という正規表現があります。
これを使うと、我々がより意図した結果になります。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
#$body =~ s/$/<<</mg;
$body =~ s/\z/|||/mg;
$body =~ s/\n/{LF}/g;
print $body;
print "\n";
結果
Today is fine.{LF}{LF}This is a pen.{LF}{LF}|||
\Z の挙動はあまり望ましい挙動ではないケースが多いでしょう。「複数行モード」では \A と \z と変数の冒頭と末尾に使うと覚えましょう。
注意しなければならないのは、複数行モード ではない $ も \Z のようなマッチをしてしまうことがあるということ。
#!/usr/bin/perl
use strict;
use warnings;
my $body = <<END_BODY;
Today is fine.
This is a pen.
END_BODY
#$body =~ s/$/<<</mg;
$body =~ s/$/|||/g; # 複数行モードではない (mなし)
$body =~ s/\n/{LF}/g;
print $body;print "\n";
結果
Today is fine.{LF}{LF}This is a pen.{LF}|||{LF}|||
手元のPerlのバージョンは5.16.3でした。
改行が入ったテキストのバリデーションに失敗してしまうと、HTTPヘッダインジェクションなどの火種になる可能性があります。改行が入る可能性がある外部入力を厳密に検査する必要がある場合は、$ や \Z は避けて、複数行モードを明示した上で \A と \z を使うようにしたほうがよいでしょう。
#Perl以外での注意点
PHPのpreg*関数等、Perl互換正規表現(PCRE)ではほぼ同様の振る舞いを実現していると考えてよいでしょう。Javaや.NETの正規表現もこれとほぼ同様の挙動をするようです(書籍からの情報であって実際に試したわけではありませんし、バージョンや正規表現エンジンの違いによって結果は異なることも想定されます)。特に末尾マッチ系 $ \Z \z については実装依存の部分も多いようです。それぞれの言語のそれぞれの実装系で検証してみることをおすすめします。
Ruby での注意点
Perlと記号的に近いRubyの正規表現修飾子(Ruby のマニュアルでは「オプション」と表現されている)に関しては注意が必要で、これと同等の挙動にはならないことに注意してください。
Perlでいうモード | Perlでは | Rubyでは |
---|---|---|
一行モード (single line mode) |
/pat/s | /pat/m |
複数行モード (multiple line mode) |
/pat/m | 最初からそう |
「一行モード」の挙動。
$ perl -E '$str = "abc\ndef\nghi\n"; $str =~ /(.*)/; say "match: $1\n";'
match: abc
$ perl -E '$str = "abc\ndef\nghi\n"; $str =~ /(.*)/s; say "match: $1\n";'
match: abc
def
ghi
$ ruby -e 'str = "abc\ndef\nghi\n"; str.match(/(.*)/); puts "match: #{$1}\n";'
match: abc
$ ruby -e 'str = "abc\ndef\nghi\n"; str.match(/(.*)/s); puts "match: #{$1}\n";'
match: abc
$ ruby -e 'str = "abc\ndef\nghi\n"; str.match(/(.*)/m); puts "match: #{$1}\n";'
match: abc
def
ghi
「複数行モード」についての Perl と Ruby の差異。
$ perl -E '$str = "abc\ndef\nghi\n"; $str =~ s/^/>/g; say $str;'
>abc
def
ghi
$ perl -E '$str = "abc\ndef\nghi\n"; $str =~ s/^/>/gm; say $str;'
>abc
>def
>ghi
$ ruby -e 'str = "abc\ndef\nghi\n"; puts str.gsub(/^/, ">");'
>abc
>def
>ghi
(Ruby の String#gsub!
ではない非破壊版 String#gsub
と同等のことは、Perl 5.14 以降であれば s///r
と r 修飾子を使うことで実現できます。 say $str =~ s/^/>/gmr;
)
ドットを改行にもマッチさせる、Perl でいう「一行モード」を Ruby で実現したい場合には /pat/s
(super dot mode) ではなく /pat/m
と書く必要があります。この m
が何を由来としているか、multiple であるとすれば、Ruby にとっての「複数行」は、「ドットが改行を飲み込んで複数行に渡ってマッチする」ことなのでしょう。それもまた自然な命名のように感じます。
また ^
を冒頭だけでなく改行の直後にマッチさせる、Perl でいう「複数行モード」については、Ruby の正規表現は最初からそういう挙動になっています。Perl でいう「複数行モード」ではない ^
を Ruby で実現させるためには、明示的に \A
を使います。
上記のワンライナーでの実験で、Ruby の正規表現にて /pat/s
を指定してもエラーにならかなかったのは、それに別の意味が割り当てられているからです。それは「正規表現はいわゆる Shift_JIS で書かれている」という意味です。
また1.8、1.9、2.0でそれぞれRubyの正規表現エンジンは微妙に違う(1.9系は Oniguruma、2.0以降は Onigmo)ことも、古いバージョンのRubyインタープリタを視野に入れたプログラムで込み入った正規表現を使う場合に少々注意が必要かもしれません。
JavaScript での注意点
元来 JavaScript では正規表現中の .
は改行文字にはマッチさせる方法が無かったのですが、ES2018での変更で Perl と同じ /s
修飾子(JavaScriptではフラグ(flag)と呼ばれている)が導入されました。
Perlでいうモード | Perlでは | JavaScriptでは |
---|---|---|
一行モード (single line mode) |
/pat/s | /pat/s (※ES2018以降) |
複数行モード (multiple line mode) |
/pat/m | /pat/m |
もし Perl でいう /s
修飾子での .
の振る舞いを /s
が使えない ES2018 より古い JavaScript で使いたい場合は、改行にマッチしない .
の代わりに、以下のような記法で対応することになります。
-
[^]
という特殊な文字クラス表記を使う- ただし古いブラウザでは使えないこともある
- Perl や PCRE 的には
[^]
という表記はエラー(正規表現コンパイルエラー)になる
-
[\s\S]
といった、「文字クラスとその補集合の文字クラスのどちらか」というような2つのバックスラッシュシーケンスの文字クラスを、大括弧で囲む文字クラス形式で列挙する- こちらは古いブラウザとも互換性がある
- Perl や PCRE 的にも正しい正規表現
#参考文献
- Ecma-262.pdf の21.2.2.8 Atom
- RegExp - JavaScript | MDN