警告のハンドリング
Perl の警告は、warnings
プラグマを有効にしておいて怪しげなことをやってのけるか、またはwarn
関数にメッセージを渡せば、発生させることができます。
警告メッセージングをトラップして好きに料理するために、Perl では特殊変数%SIG
1を使います。
warn 'test1'; #=> test1 at ...
{
my $handler = sub {
my $msg = shift;
print "hello! $msg";
};
$SIG{__WARN__} = $handler;
}
warn 'test2'; #=> hello! test2 at ...
print STDERR "test3\n"; #=> test3
$SIG{__WARN__}
エントリにハンドラとしてサブルーチンリファレンスをセットしておくと、警告メッセージは通常のようにSTDERR
に出力される代わりに、引数$_[0]
としてハンドラに渡ります。ハンドラは、メッセージを加工して情報を追加するなり、警告をdie
で致命的エラーに昇格させるなり、出力せずにどこかの配列に溜めてゆくなり、何もしない (ゆえに警告も表示されない) なり、好きなことができます。
warn
は文字列のリストを取り、$SIG{__WARN__}
フックのかかっていない状態では、print
のように標準エラー出力に逐次それらを出力してゆくだけです (しかし警告シグナルが発生していることは忘れないように!)。出力が改行で終わっていない場合には、さらに警告が発生した位置の情報と、それから改行を追加してくれます。上掲のwarn 'test1'
の箇所では、そのように振舞っています。
ハンドラがセットされていると、ハンドラにメッセージが渡りますが、その@_
の中味はwarn
に渡されたリストそのものではありません。warn
が出力するように連結され加工された文字列が$_[0]
に渡り、$_[1]
以下は未定義になります。従ってwarn 'test2'
のところでは、"test2 at ..."
という加工済みの文字列が、&my_handler
の$msg
に渡っています。&my_handler
はそれを"hello! test2 at ..."
という文字列にさらに加工してから、標準出力に2出力します (だってタダのprint
ですから)。
最後に、print STDERR "test3\n"
では、警告ではないので、単にSTDERR
に出力されるだけです。標準エラー出力に出力したからといって、perl 内部で警告シグナルが発生するわけではありません。
$SIG{__WARN__} の仕様
$SIG{__WARN__}
の細かい仕様は、warn のドキュメント 及び perlvar を参照してください。以下には軽く列挙します。
- すべての警告を強引に無視するには、
$SIG{__WARN__} = sub {};
として no-op. を設定します。 - 初期状態に戻すには、
$SIG{__WARN__} = 'DEFAULT';
として、特殊な文字列値'DEFAULT'
を格納します。 - コンパイルフェーズでの警告も捕捉する必要があるのなら、
BEGIN { $SIG{__WARN__} = sub { ... } }
などと、BEGIN
ブロックの中で操作します。 -
local $SIG{__WARN__} = sub { ... };
として、レキシカルスコープの中だけで有効なハンドラを設定できます。 - ハンドラ内で
warn
を安全に呼べます。このwarn
はデフォルトの振舞いをし、それが再びハンドラを呼出して無限ループに陥ることはありません。
警告ハンドリングのカスケーディング
Perl の警告メッセージング機構は、このように非常に動的言語としての特性が前面に出たものであり、$SIG{__WARN__}
によって処理系のグローバルな状態を操作するものです。つまり一面、$SIG{__WARN__}
にハンドラをセットするのは強力すぎる道具立てだ、ということになります。
空サブルーチンを設置して警告を黙らせれば、どんなモジュールのどんな警告も出なくなります。レキシカルスコープに局所化することはできますが、それは上書きなので、そのブロック内では他の一切のハンドラが機能しなくなります。またメッセージのタイプによって何かをする場合など、予想もしないすべての警告にハンドラが対処することは難しいので、ハンドラ内で再びwarn
を呼んで、とりあえず表示させておくのが通例です:
BEGIN {
$SIG{__WARN__} = sub {
my $msg = shift;
warn $msg; # 文字通り警告しておく
...; # それから何かをする
};
}
それでは、複数の警告ハンドリング機構を同時に使い、それらをカスケードしたい場合には、どうすればよいのでしょうか? これは例えば、あるモジュール X では警告に情報を添加する、別のモジュール Y では警告に色を付ける、そしてそれらのモジュールが読込まれているかどうか、またどんな順序で読込まれているかについて、モジュール設計者としてはいかなる仮定も置けない、というような場合です。
まず、以前のハンドラを退避することが必要です。そして、自分のしたいことをしてから、以前のハンドラも呼ぶことにしましょう。しかし、「以前のハンドラ」というものはそもそもなく、$SIG{__WARN__}
が初期状態である可能性もあります。従って、場合分けが必要です。これは例えば、次のようなコードで実現することができます。
use 5.28.0;
use warnings;
warn 'Message!';
# => Message! at cascade_warn.pl line 4.
package Fuga {
my $prev_warn = $SIG{__WARN__};
$SIG{__WARN__} = sub {
my $str = "Fuga: $_[0]"; # したいことをする
defined $prev_warn
? $prev_warn->($str)
: warn($str)
;
};
}
warn 'Message!';
# => Fuga: Message! at cascade_warn.pl line 20.
package Piyo {
my $prev_warn = $SIG{__WARN__};
$SIG{__WARN__} = sub {
my $str = "Piyo: $_[0]"; # したいことをする
defined $prev_warn
? $prev_warn->($str)
: warn($str)
;
};
}
warn 'Message!';
# => Fuga: Piyo: Message! at cascade_warn.pl line 36.
Fuga
→Piyo
の順でパッケージが読込まれ、それぞれが$SIG{__WARN__}
を操作して、したいことをしています。しかしその前に、以前に設置されていたハンドラを$prev_warn
に退避しています。ところで、「以前のハンドラ」が存在しない場合には? Fuga
が、ちょうどそうですね。この場合、$SIG{__WARN__}
は未定義値を返します。だから、$prev_warn
がundef
かどうかによって、定義されているならサブルーチンリファレンスだからそれを呼び、定義されていないなら単にwarn
を呼ぶ、と挙動を変えてやればよいのです (Fuga
→Piyo
の順でハンドラがアペンドされたので、最後のwarn
では、メッセージはまずPiyo
化され、次にFuga
化されます)。
$SIG{__WARN__}
はグローバルにどこからでも弄れるので、こうした仕組みをさらにサブルーチンにまとめてやることもできます。このような手続きを、どこかで何らかの API で提供しておけば、他のモジュールはそれを利用するだけで、簡単にカスケーディングを実現できるようになるでしょう:
use 5.28.0;
use warnings;
sub cascade_warn :prototype(&) {
my $coderef = shift;
my $prev_warn = $SIG{__WARN__};
$SIG{__WARN__} = sub {
my $str = $coderef->($_[0]);
defined $prev_warn
? $prev_warn->($str)
: warn($str)
;
};
}
package Hoge {
warn 'Message!';
#=> Message! at cascade_warn2.pl line 19.
}
package Fuga {
main::cascade_warn { return "Fuga: $_[0]" }; # したいことをする
warn 'Message!';
#=> Fuga: Message! at cascade_warn2.pl line 25.
}
package Piyo {
main::cascade_warn { return "Piyo: $_[0]" }; # したいことをする
warn 'Message!';
#=> Fuga: Piyo: Message! at cascade_warn2.pl line 31.
}
以上は、警告ハンドリングのカスケーディングについて、私の考えついた一つの方法に過ぎません。何かもっといいトリックをご存知だったり、思いついた方がいらっしゃれば、ぜひともお知らせください。