IO::Scalarで色々

tieでSTDOUTをつかまえるの記事に対するid:vkgtaroさんのブックーマークコメントで、IO::Scalarを使う方法を教えてもらいました。以下、Log::Dispatch::Colorfulテストスクリプトより。

my $err;
tie *STDERR, 'IO::Scalar', \$err;

先の2記事(tieでSTDOUTをつかまえるtieでSTDINを置き換える)ではそれぞれ「printだけ」「readline(<>演算子)だけ」のテストだったので、あのような最小限のクラスでも動いてましたが、様々なI/O操作に対応するのであれば、自分でクラスを作らずにこのIO::Scalarを利用するのがよさそうです。

ちなみにSTDIN/STDOUT を使うテストで使われていたIO::ScalarArrayは、IO::Scalarと同じくIO::stringyに含まれるモジュールでした。よく見ておくべきでした……。

さて、IO::Scalarのperldocを読んでみると「newでファイルハンドルを返すのであれば、tieを使わなくてもSTDOUT等との置き換えはできるのでないか」と思いつき、以下を試してみました。

use strict;
use warnings;
use IO::Scalar;

my $out;
{
    local *STDOUT = IO::Scalar->new(\$out);
    print "foo\n";
    # Can't call method "PRINT" on an undefined value
}
print '$out=', $out;

しかしこれだとうまく動作しません。さらに試してみたところ、以下のように一度スカラー変数への代入を介すると、意図した動作になりました。

use strict;
use warnings;
use IO::Scalar;

my $out;
{
    my $fh = IO::Scalar->new(\$out);
    local *STDOUT = $fh;
    print "foo\n";
}
print '$out=', $out;
# $out=foo

型グロブへの代入とスカラー変数への代入でどう動作が変わってくるのか……自分には分かりませんでした。

以下はSTDINとの置き換えも含めたサンプルです。

use strict;
use warnings;
use IO::Scalar;
use Test::More qw( no_plan );

# catch stdout test
my @src1 = ('a' .. 'z');
my $dist1;
{
    my $fh = IO::Scalar->new(\$dist1);
    local *STDOUT = $fh;
    print for @src1;
}
is $dist1, join q{}, @src1;

# replace stdin test
my @src2 = map { "$_\n" } ('A' .. 'Z');
my @dist2;
{
    my $fh = IO::Scalar->new( \join(q{}, @src2) );
    local *STDIN = $fh;
    @dist2 = <STDIN>;
}
ok eq_array \@dist2, \@src2;

なお、さらに探してみると、IO::StringというIO::Scalarと同じような動作を行うらしいモジュールもありました。