ファイル入出力関連

2020/04/20更新

目次

モダンなopen関数の書き方

かつて、Perlでファイルを開くときのopen関数は、以下のように書いていた。

open(FH, ">$file");

このような書き方は現在ではレガシーとなっている。ファイルハンドルに使われている裸の文字列(Bareword)は変数としての取り扱いが難しく、また、読み書きモードとファイル名を一体化させて渡しているため、安全性の問題もあった。

上記の問題があったため、現在(Perl5.6以降)では以下のように3引数のopen関数を使って書くのが一般的となっている。

open(my $fh, '>', $file);

まず、ファイルハンドルには、定義したばかりの(中身がundefな)ローカル変数を使う。そして、3引数にすることで、<>>>などの読み書きモード指定とファイル名とを分ける。なお、外部コマンドへ出力を渡す場合は|-、外部コマンドから入力を受ける場合は-|とする。

ローカル変数のファイルハンドルは、従来のファイルハンドルとほぼ同じように使用できる。

print $fh "hoge\n";

また、サブルーチンなどにそのまま渡すことができる。

sub my_output {
    my($fh, $data) = shift;
    print $fh $data;
}
open(my $fh, '>', $file);
my_output($fh, "hoge\n");

ファイルハンドルの謎

前述の通り、現在ではファイルハンドルを指定する部分には、通常のスカラー変数を渡すのが普通であるが、それではかつてファイルハンドルとして指定していたFHなどの文字は一体何だったのだろうか?Perlを学び始めた頃、これは大きな謎だった。$が付いていないのでスカラー変数ではないし、クォートされていないので文字列でもない。

Perlにおいて、パッケージ内でグローバルに使われるスカラー変数やサブルーチンなどの「名前」は、シンボルテーブルというもので管理されている。シンボルテーブルでは、例えばFHという名前(シンボル)に対して、スカラー変数(SCALAR)、配列(ARRAY)、ハッシュ(HASH)、サブルーチン(CODE)などのいろいろなスロットがあり、それぞれ、$FH@FH%FH&FHという変数やサブルーチンへのリファレンスを格納している。

ところが、ファイルハンドルというのは、上記のいずれのスロットでもなく「IO」という専用のスロットに格納される。このスロットは、$@のようなそれを指し示す専用のシジルが無いため、open関数などに渡す際は、FHのようにシジル無しの裸の文字列(bareword)でシンボル自体を書くか、*FHのように*を付けて全てのスロットを総称する型グロブ(またはそのリファレンス\*FH)として記述するしかなかったのである。

このようにbarewordや型グロブでは取り扱いに難があるし、そもそもグローバルであることも問題であった。そこで、Perl 5.6.0以降では、open関数などにファイルハンドルとして未定義のスカラー変数が渡されたときは、内部で自動的に新しいファイルハンドルを生成し、その型グロブのリファレンスをスカラー変数に割り当ててくれるようになった。これにより、通常の変数として取り扱うことができるようになった上、グローバルに影響を及ぼすファイルハンドルそのものについて考える必要が無くなったのである。

このように、ファイルハンドルの型グロブがスカラー変数に入っていることを理解すると、例えば標準入出力のファイルハンドルSTDINSTDOUTSTDERRなどを変数に代入したいときは以下のようにすればよいことが分かる。

# $fileがあれば開き、無ければ標準出力を$fhに設定
my $fh;
if ($file) {
    open($fh, '>', $file);
} else {
    $fh = *STDOUT;
}
print $fh "Hello, world!\n";

ファイルの内容を一度に読み込む方法

Perlにおいて、テキストファイルを読み込むには、行入力演算子<~>を使うが、これは通常、改行ごとに区切られて入力されるため、ファイルの内容を一度に読み込みたい場合は使い勝手が悪い。

open(my $fh, '<', $file);
my @lines = <$fh>;
my $content = join('', @lines);
close($fh);

そのようなときは、特殊変数$/によって行の区切り文字を一時的に変更すればよい。

open(my $fh, '<', $file);
my $content = do { local $/; <$fh> };
close($fh);

このように、local$/の値を一時的にundefにすることで、区切り文字無しとなり、全体が1行という扱いになって、結果的に一度の<~>で全てを読み込むことができる。

なお、do { ~ }は、localのブロックスコープを作りつつ、<$fh>の値を返すために使っているものである。もし、スクリプト全体で$/を無効にしたいのであれば、スクリプトの冒頭でundef $/;などとすることもできる。

外部コマンド実行時のエラー処理

openで外部コマンドを起動した場合、そのコマンドが成功したか失敗したかは、openの時点ではなく、closeの時点で判明する。

# 通常のファイル入出力の場合
open(my $fh, '<', $file) or die "ファイルを開けません。";
  :
close($fh);

# コマンド実行の場合
open(my $cmd, '-|', "ls $dir");
  :
close($cmd) or die "コマンドを実行できません。";

外部リンク