PerlIOで混乱した

perlでちょっとしたテキスト処理なんかをする時に

while (<>) {
    # code for each line
}

みたいなコードをよく書くんだけど、ちょっと格好つけて(?)use utf8とかuse encodingとかuse openとかつけてみたら*1途端に文字化けしまくって、しかもどうやったら正しく処理できるのかわからなくなった。
ぺちぱーに成り下がった結果がこれだよ!


なんかね、

perl script.pl <target.txt

だと上手く動くのに

perl script.pl target.txt

だと文字化けする、みたいなよくあるトラブル?
つーか、過去にも何度かこれではまって、結局どうするのがいいのかよくわからないままだった。
どうせ使い捨てだし今は急いでるからどっちかで動けばいいやってね。

試行錯誤の末、出来上がったコード

実行環境はWindowsコマンドプロンプト、対象ファイルはShift JISを想定。

use utf8;
use strict;
use warnings;
use open ':encoding(cp932)';
use open ':std';

while (<>) {
    # code for each line
}

とりあえずこれでどっちの実行方法でも文字化けせずに動くようになった。

整理する

何がどういう影響をもたらすのか整理してみる。

use utf8
use encoding
use open
  • 入出力のデフォルトPerlIOレイヤを設定する。
  • 追加で:stdを指定すると、設定がSTDIN、STDOUT、STDERRにも反映される。
  • プラグマ宣言時に既に開かれているハンドルには影響しない。
<>(ヌルファイルハンドル)

以下の擬似コードと同じ(perlopより)。

unshift(@ARGV, '-') unless @ARGV;
while ($ARGV = shift) {
    open(ARGV, $ARGV);
    while (<ARGV>) {
        ...     # code for each line
    }
}

ちなみに'-'を開くと標準入力、'>-'を開くと標準出力。


えーと、つまり渡し方によって標準入力から読むか普通にファイルとして読むか違ってくるから、両方に対して適切にPerlIOレイヤの設定を行わないとダメだったってことでいいのかな。

use encodingは使わない方がいいの?

環境もWindowsだしターゲットもWindows用ファイルだしってことで、use encoding 'cp932';とかやったところから始まって、結局上記の仕組みがわかったので動くようにはなった。
だけど、どうも調べているとuse encodingは副作用やら何やらに悩まされるので使わない方がいい、という情報がぽつぽつ出てきたので、結局UTF-8で書いてuse openで指定することにした。

STDERRに対するencoding layer

上の整理で少し気になったのが、use encodingだとSTDERRは含まれないのに、use open ':std'だとSTDERRも含まれるという点。
もしかしてこういう理由からそうなってるのかな?


binmode()ルーチンは:encodingレイヤのインスタンスを作成し,*STDERRレイヤスタックにその未初期化の:encodingレイヤインスタンスを追加する。その後:encodingのイニシャライザが呼び出されるが,"foo" というエンコーディングは存在しないため,:encodingのイニシャライザは警告を発しようとする。そこで警告メッセージを*STDERRに出力しようとするが,*STDERRには初期化が終わっていない:encodingレイヤインスタンスが乗ったままなので,:encodingを介した警告メッセージの出力は失敗に終わる。

参考

*1:さすがに全部いっぺんにつけたわけじゃない。