preg_replace()で特定の文字列を行ごと削除する

正規表現についてのすっごい小ネタです。要約すると「()の中に ^$ も含むことができる」ってだけの話です。

複数行に渡るテキストがあったとして、その中に含まれる、 ## DELETE LINE ## から始まる行は行ごと削除したいとします。ちなみに話を単純化するため、改行コードは \n (LF) 固定とします。ざっくり PHP スクリプトを書くとこんな感じです。

<?php
$subject = <<<TXT
1.
2.
## DELETE LINE ##
3.
TXT;

$replaced = preg_replace( '/## DELETE LINE ##\n/', '', $subject );

var_dump( $replaced );

すると結果は以下のように出力されます。

string(8) "1.
2.
3."

本題はここからなんですが、んじゃ、テキストが文末だったらどうでしょう? 具体的には以下のケースですが、

<?php
$subject = <<<TXT
1.
2.
3.
## DELETE LINE ##
TXT;

$replaced = preg_replace( '/## DELETE LINE ##\n/', '', $subject );

var_dump( $replaced );

\n は飽くまでも改行コード(LF)なので、EOL にはヒットしません。よって、このケースでは ## DELETE LINE ## は残ったままとなります。で、どうするかというと、冒頭で要約したとおり、 pattern の正規表現を \n 固定じゃなくて文末($)も条件にすればヒットするってわけです。

<?php
$subject = <<<TXT
1.
2.
3.
## DELETE LINE ##
TXT;

$replaced = preg_replace( '/## DELETE LINE ##(?:\n|$)/', '', $subject );

var_dump( $replaced );

これだと結果は以下のように出力されます。

string(9) "1.
2.
3.
"

これで ## DELETE LINE ## は削除できました。

あれ? 最初の結果と違うことない? それは 3. の末尾に改行コードを含むから、ですね。これはまた別のお話…だと思うんですけど、結果を同じようにしたい場合は、一行でなんとかするよりもう一度 preg_replace() を実行するのが簡単でしょうね。例えばこんな感じになります。

<?php
$subject = <<<TXT
1.
2.
3.
## DELETE LINE ##
TXT;

$replaced01 = preg_replace( '/## DELETE LINE ##(?:\n|\z)/', "", $subject );
$replaced02 = preg_replace( '/\n$/', "", $replaced01 );

var_dump( $replaced02 );

結果はこうなります。

string(8) "1.
2.
3."

言われてみればそうなんですが、$\z( ) で括るって発想がなかったので、しばらくハマってました……っていう小話でした。