readline() on closed filehandle FILE at test.pl line 13.

あるファイルを読みつつ、その内容を処理し、結果を別なファイルに書き出すというプログラムはよくあるのだけど、そういうプログラムを書いていて思いっきりハマりました。


何にどうハマったのか、私が実際に作っていたプログラムを紹介するのは面倒なので、いろいろ抽象化、捨象して説明。まずは簡単な例から。

簡単な例

こんなファイルがあるとする。ファイル名を data.txt とする。

japanese:konnichiwa
english:hello
french:bonjour


このファイルの各行は、二つの単語がコロンで区切られている。コロンより前には「言語名」が書かれていて、コロンの後ろにはその言語での「あいさつ」が書いてある。


このファイルを読み込んで、それぞれの行を次のように処理したい。

  1. 言語名を取り出し
  2. その後ろに ".txt" の4文字を付加し
  3. それをファイル名として、新規にファイルを作り
  4. そのファイルの中に、コロンの後ろ側にある「あいさつ」を書き込む。
#! /usr/bin/perl 

open IN, "<data.txt" or die;

while ($_ = <IN>) {
    ($language, $greeting) = split /:/, $_;
    open OUT, ">$language.txt" or die;
    print OUT $greeting;
    close OUT;
}

close IN;


これを実行すると、japanese.txt, english.txt, french.txt という3つのファイルが新たに作られる。それぞれのファイルにはそれぞれの言語で、あいさつが書き込まれる。


ここで理解しておいて欲しいことは、プログラムが起動している間に、合計4つのファイルが open され、close されたということ。時系列で見ていくと、つまりこんなイメージ。

open IN data.txt
 |
 | open  OUT japanese.txt
 |  |
 |  | konnichiwa
 |  |
 | close OUT japanese.txt
 |
 | open  OUT english.txt
 |  |
 |  | hello
 |  |
 | close OUT english.txt
 |
 | open  OUT french.txt
 |  |
 |  | bonjour
 |  |
 | close OUT french.txt
 |
close IN data.txt

もうちょっと、ちゃんと書く

上のプログラムでは、読み込むファイルのファイル名が決め打ちになっている。


data.txt のようなファイルが複数あった場合に、それらを連続して取り扱えるようにするためにこの処理自体を関数化する。(「サブルーチン化」と言ったほうが正しいのかな?) もちろんその関数では、引数としてファイル名を受け取るようにする。


また、ファイルを書き込む部分もくくり出して、別関数とした。

#! /usr/bin/perl
use strict;

read_data_file("data.txt");

sub read_data_file {
    my $filename = shift;
    my ($language, $greeting);

    open IN, "<$filename" or die;
    while ($_ = <IN>) {
        ($language, $greeting) = split /:/, $_;
        write_greeting_file("$language.txt", $greeting);
    }
    close IN;
}

sub write_greeting_file {
    my $filename = shift;
    my $content = shift;

    open OUT, ">$filename" or die;
    print OUT $content;
    close OUT;
}


これで、私がハマった時にかなり近い状態になった。このプログラムはちゃんと動くはず。

ハマったバージョン

私がハマったときは、こんな風に書いてました。


間違った例。正しく動きません。都合により行番号を付加。

  1 #! /usr/bin/perl
  2 use strict;
  3
  4 read_data_file("data.txt");
  5
  6 sub read_data_file {
  7     my $filename = shift;
  8     my ($language, $greeting);
  9
 10     open FILE, "<$filename" or die;
 11     while ($_ = <FILE>) {
 12         ($language, $greeting) = split /:/, $_;
 13         write_greeting_file("$language.txt", $greeting);
 14     }
 15     close FILE;
 16 }
 17
 18 sub write_greeting_file {
 19     my $filename = shift;
 20     my $content = shift;
 21
 22     open FILE, ">$filename" or die;
 23     print FILE $content;
 24     close FILE;
 25 }


もう一度書きますがこの例は正しく動きません。


きちんと動くバージョンのプログラムと違うのは、ファイルハンドル名がもともと INOUT だったのをどちらも FILE に変更した点のみ。


これを動かすと、japanese.txt は作成されるけれど english.txtfrench.txt は作成されない。エラーは出ない。


なぜそうなるのかというと、読み込みのために使っているファイルハンドル FILE が、関数 write_greeting_file の中で(別なファイルの書き出し用に利用された後) close されちゃうから。


ファイルハンドル FILE って、確かに my とかやってないものなので、グローバルなんだ。グローバルというのは正しくないか……。正しくは、カレントパッケージにグローバル。

Since filehandles are global to the current package, two subroutines trying to open "INFILE" will clash.

(perlopentut)


これを知らなかったのがハマった理由の一つ。


もう一つの理由は、こいつを -w 付きで動かしたときの Perl インタプリタの吐くメッセージ。妙だ。

%perl -w test.pl
readline() on closed filehandle FILE at test.pl line 13.


まず、私は readline なんて使ってないっ(断言)。それに、13 行目は関係なくて、どっちかというと readline と関係あるのは 11 行目のほうだと思う。たぶん、$_ = <FILE> とかを実行する時に、Perl が内部で readline() を使っている気がする。

教訓

次から気を付けなきゃならないことや、勉強になったこと。

Indirect Filehandle を使え

まず第一は、裸のワードのファイルハンドルをやめて、ちゃんと my した変数をファイルハンドルとして使うこと。例えばこんな風に。

my $fn;

open $fn, "<$filename" or die;
# do something here
close $fn;


こういうやりかたのことを、Indirect Filehandle と言うらしい。詳しくは、perldoc perlopentut を実行して、Indirect Filehandles の部分を御覧ください。

既に open されているファイルハンドルを再度 open しても怒られない

上のほうで、ファイルのオープン、クローズを時系列であらわした図を書いたけど、それを、ちゃんと動かないほうのプログラムに基づいて書き直すと、気がつくことがある。


まず、プログラムの最初のほうはこんな感じになると思う。

open FILE data.txt
 |
open FILE japanese
 |
(略)


ここで気がつくことは、既に open されているファイルハンドルを使ってもう一度 open しようとしても、Perl は何も言わない、ということ。


このことについても、ドキュメントにちゃんと書いてあった。

If the filehandle was previously opened, it will be implicitly closed first.

(perlopentut)

close の失敗も捕捉せよ

時系列の図を、もうちょっと書き進めてみると。

open  FILE data.txt
 |
open  FILE japanese.txt
 |
 | konnichiwa
 |
close FILE japanese.txt
 |
close FILE data.txt


english.txtfrench.txt は作られなかった。english.txtfrench.txt を作ろうにも、そもそも data.txt を読むためのファイルハンドルが閉じられており、english:hello という行を読むことができなかったのだ。


なので、処理の流れは while を抜ける(出る)。


最後に待ち構えているのは close FILE; という行なのだけど、既に閉じられているファイルハンドルに対してもう一度 close しようとしても、Perl はまた文句を言わないのか?と、なんでも Perl のせいにしようかと思ったら、そうではなくて……


こんな風に、close にも or die を付けておけばよかったのだ。

    open FILE, "<$filename" or die;
    while ($_ = <FILE>) {
        ($language, $greeting) = split /:/, $_;
        write_greeting_file("$language.txt", $greeting);
    }
    close FILE or die;


このようにして実行すると、最後の最後で close がコケて、die が実行される。実際には、close はコケていたのだ。close の失敗を捕捉しようとしてなかっただけの話。


これで、少なくともプログラムが正常終了するということは避けることができる。最初からこれをやってれば、かなり見当も付けやすくなってただろうなぁと、今にして思う。

"readline() on closed filehandle" は妙だ
readline() on closed filehandle FILE at test.pl line 13.


最初にこのメッセージを見た時には、なにがイケナイのか本当にわからなくて困った。readline なんて使っていないという点もそうだけれど、特に at test.pl line 13 という指摘の行番号が、変なところを指しているように思える。


このエントリで使った「間違った例」のプログラムの 13 行目には write_greeting_file というサブルーチンの呼び出しがあったけれど、どうやらこのサブルーチン呼び出し自体は重要ではないらしい。


次のプログラムは、上にある「間違った例」に少し手を加えたもの。

  1 #! /usr/bin/perl
  2 use strict;
  3
  4 read_data_file("data.txt");
  5
  6 sub read_data_file {
  7     my $filename = shift;
  8     my ($language, $greeting);
  9
 10     open FILE, "<$filename" or die;
 11     while ($_ = <FILE>) {
 12         ($language, $greeting) = split /:/, $_;
 13         write_greeting_file("$language.txt", $greeting);
 14         do_something();
 15         do_something();
 16         do_something();
 17     }
 18     close FILE;
 19 }
 20
 21 sub write_greeting_file {
 22     my $filename = shift;
 23     my $content = shift;
 24
 25     open FILE, ">$filename" or die;
 26     print FILE $content;
 27     close FILE;
 28 }
 29
 30 sub do_something {
 31     # サブルーチン名に反して、なにもしない
 32 }


手を加えたのは、do_something というサブルーチン呼び出しを追加した点のみ。
これを -w オプション付きで実行すると、次のようになる。

%perl -w test.pl
readline() on closed filehandle FILE at test.pl line 16.


こんどは 16 行目を指摘してくる。きっとたぶん、while ($_ = ) { で始まる while 文の、おしまいのブレース (カーリーブラケット、'}' ) の直前にあるものを指しているだけなんだろうと思う。


このメッセージが指摘する行番号はすごく妙に感じる。
他のバージョンの Perl でも、同じ指摘をするのだろうか。