readline() on closed filehandle FILE at test.pl line 13.
あるファイルを読みつつ、その内容を処理し、結果を別なファイルに書き出すというプログラムはよくあるのだけど、そういうプログラムを書いていて思いっきりハマりました。
何にどうハマったのか、私が実際に作っていたプログラムを紹介するのは面倒なので、いろいろ抽象化、捨象して説明。まずは簡単な例から。
簡単な例
こんなファイルがあるとする。ファイル名を data.txt とする。
japanese:konnichiwa english:hello french:bonjour
このファイルの各行は、二つの単語がコロンで区切られている。コロンより前には「言語名」が書かれていて、コロンの後ろにはその言語での「あいさつ」が書いてある。
このファイルを読み込んで、それぞれの行を次のように処理したい。
- 言語名を取り出し
- その後ろに ".txt" の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 }
もう一度書きますがこの例は正しく動きません。
きちんと動くバージョンのプログラムと違うのは、ファイルハンドル名がもともと IN
、OUT
だったのをどちらも FILE
に変更した点のみ。
これを動かすと、japanese.txt
は作成されるけれど english.txt
と french.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.txt
や french.txt
は作られなかった。english.txt
や french.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 でも、同じ指摘をするのだろうか。