Perl のローレベルエラー処理 (もしくは Errno.pm のススメ)B!

例えば、「ディレクトリが存在しなければ作成」という処理を書くことを考えてみます。

if (! -d $dir) {
    mkdir $dir
        or die "failed to create dir:$dir:$!";
}

でいいしょうか? いえ、違います。これだと、-d 演算子でディレクトリの不存在を確認した後、mkdir を呼ぶまでの間に他のプロセスがディレクトリを作成してしまう可能性があります。なので、たとえば C ならば、

if (mkdir(dir) != 0 && errno != EEXIST) {
  fprintf(stderr, "failed to create directory:%s:%s\n", dir, strerror(errno));
  exit(1);
}

のように、mkdir を呼び出してエラーが返ってきた場合には原因を確認する、というのが「正しい」コードになります。では、同様のコードを Perl で書く場合は、どうすればいいのでしょうか。

unless (mkdir $dir or $! =~ /exists/) {
  die "failed to create dir:$dir:$!";
}

でしょうか。やっぱり違います。エラー文字列はロケールによって変化するので、例えば LANG が ja_JP.utf8 の場合は「同名のファイルが既に存在」のような文字列になります。ですから、$! を正規表現で確認する、という手段は使えません。

正解は、

use Errno ();

unless (mkdir $dir or $! == Errno::EEXIST) {
    die "failed to create dir:$dir:$!";
}

です。このコードを理解する上で必要になる知識は、まず、$! が dualvar、つまり、文字列として評価した場合は文字列を、数値として評価した場合は数値を返す変数であるということです。

上のコードでは、!= 演算子を適用しているので、$! は数値として評価されます。Linux や OSX では、既に同名のディレクトリが存在したために mkdir が失敗すると、17 という値が返ってくることになります。数値であれば、ロケールによって変化することはありませんから、エラーの判定に適しています。

ただ、この場合に返されるエラー番号が 17 という値かどうかは OS によって異なります。そこで、Errno モジュールに定義されている定数「Errno::EEXIST」と比較することで、OS に依存しないエラー処理が完成することになります。

各関数がどのようなエラーを返すかは、OS の man ページに書かれています。例えば mkdir の返すエラーの一覧を知りたければ、コンソールから man mkdir と入力して調べることになります。OS に依存しないプログラムを書く場合は、各種 Unix の共通仕様を定めた Single Unix Specification のドキュメントを参照するといいでしょう (The Open Group Base Specifications Issue 6)。

まとめ

ディレクトリ作成に限らず、OS が返すエラーの原因を確認して動作を変えるという処理はしばしば必要になります。Errno の使い方と、どのようなエラーが返ってくるかの調べ方は、覚えておくべきでしょう。

PS. 明日は ZIGOROu さんです。お楽しみに!

  • 2010/02/18 サンプルコードが間違ってたので修正