ファイルテスト演算子と優先順位

先日作ったソースコードビューアのコード中で「ファイルテスト演算子」というものを使いました。
ファイルテスト演算子とは、文字通り「“ファイル”を“テスト”する為の“演算子”」です。ファイル演算子を使う事で、「ファイルが存在するか」「ファイルに書き込めるか」「ファイルを所持しているか(UNIX系OSの場合)」などを確かめたり、「ファイルの最終更新日時」を取得できたり、ファイルの色々な情報を取得する事ができます。
ファイル演算子は「ファイル名」(ファイルへのパス)もしくは「ファイルハンドル」を受け取る単項演算子で、「-」(ハイフン)+「英文字1字」で構成されます。例えば、以下のように使います。

if (-e "hello.pl") {
  print "hello.pl exists.";
} else {
  print "hello.pl does not exist.";
}

「-e」は、「ファイルが存在するかどうか」を確かめる為のファイルテスト演算子です。ファイルが存在すれば真(1)を、存在しなければ偽(空文字列 "")を返します。つまり、この場合は "hello.pl" がカレントディレクトリに存在するなら "hello.pl exists." と表示されます。存在しなければ else のメッセージが表示されます。「演算子」と呼んではいるものの、「-e」という名前の組み込み関数のようですね。(^_^)
他にも、ファイルテスト演算子には色々なものがあります。以下の表に、よく使うと思われる演算子をまとめてみました。ファイルテスト演算子はこれだけではないので、必要があればその都度勉強したいと思います。

演算子 意味 返り値
-e ファイルが存在するかどうか 存在すれば真
-s ファイルのサイズを取得 バイト単位でのサイズ
-z ファイルのサイズが 0 かどうか サイズが 0 ならば真
-f 普通のファイルのパスかどうか ファイルのパスならば真
-d ディレクトリ(フォルダ)のパスかどうか ディレクトリのパスならば真
-r 現在のユーザーで読み込めるかどうか 読み込めるならば真
-w 現在のユーザーで書き込めるかどうか 書き込めるならば真
-o 現在のユーザーが所有しているかどうか 所有しているならば真
-M ファイルの最終更新日を取得 更新されてからの日数*1
-A ファイルへの最終アクセス日を取得 アクセスされてからの日数

色々あって便利ですね。(^_^)


演算子としての優先順位をラクダ本で調べてみたところ、比較演算子(==, < など)よりも高く、加算減算などの算術演算子よりは低い、という位置にあるようです。(普通の組み込み関数と同じかな?)
例えば、この場合は

my $fn = "hello";
if (-e $fn . ".pl") {
  print "$fn!";
}

ファイルテスト演算子 -e よりも、文字列結合演算子「.」が優先されるので(加算減算と同じ優先順位)、思った通りに動きます。"hello.pl" が存在すれば、"hello!" と表示されます。


では、ファイル名を指定する為に「三項演算子」(?:)を使ってみるとどうでしょうか。

my $afternoon = 0;
if (-e $afternoon ? "hello.pl" : "goodnight.pl") {
  print "Exists!";
}

$afternoon が 0 (偽)なので、「$afternoon ? "hello.pl" : "goodnight.pl"」という三項演算子は「goodnight.pl」を返すはずです。「goodnight.pl」は存在しません。それにもかかわらず、このプログラムは "Exists!" と表示してしまいます。$afternoon を 1 にしてみても、常に "Exists!" と表示されてしまうので、条件文が常に「真」として判定されている事になりますね。
これは、三項演算子がファイルテスト演算子よりも「低い優先順位を持っている」のが原因です。つまり、三項演算子よりも先に「-e $afternoon」が評価されてしまうのです。「"0" という名前のファイルは存在しない」ので、結果として「偽」(空文字)が返ります。そして、その結果が三項演算子の条件として使われて

"" ? "hello.pl" : "goodnight.pl"

が評価されます。「""」(空文字)は偽なので、当然この式は "goodnight.pl" を返します。文字列を論理値として評価する場合(ブール値コンテキスト)、空文字("")は「偽」、それ以外は「真」として評価されます。"goodnight.pl" は「真」、つまり、式全体の評価結果は常に「真」という事になってしまうわけです。
これを解消するには、三項演算子が優先して評価されるようにカッコで括ればいいですね。

my $afternoon = 0;
if ( -e ($afternoon ? "hello.pl" : "goodnight.pl") ) {
  print "Exists!";
}

実行してみると、$afternoon が 0 の時は何も表示されず、1 の時は "Exists!" と表示されるようになりました。(^_^)


Perl には演算子がたくさんあるので、それぞれ優先順位を覚えるのはとても大変ですね。直感的な優先順位になってはいますが、それでも思った通りの順番で評価されない事もあります。特に Perl では「カッコをなるべく省略する」という慣習があるので、こういった間違いが起こりやすいと思います。
優先順位の間違いは、文法上の誤りではないので Perl は「エラー」だと気づいてくれません。その結果、他の部分でエラーになってしまい、エラーの出る位置と原因の位置が離れている「原因がなかなかわからないバグ」を作り出してしまいます。
あまり自信がなければ、多少読みづらくなるとしても、カッコで括るようにしたほうがいいですね。(^_^)

*1:その日付から、プログラムの開始日時までの経過日数を小数で表したものが返ってきます