tieを利用してSTDOUTの出力を弄る

print文でのSTDOUTの出力先を変更する方法 - すぎゃーんメモの続き。
id:mattnさんから「tieを使うのが一般的かと思いますよ」とコメントをいただきました。ありがとうございます。
tieって名前は聞いたことがあったけど、まったく使ったことがなかった。。
ドキュメント読んだりしながら勉強してみた。


まだよく理解できてないところはあるけど、とにかく「オブジェクトクラスを変数にゴニョゴニョすることができる」らしい。
とりあえず書いてみた。

#!/opt/local/bin/perl
use strict;
use warnings;

my $hoge;
{
    $hoge = tie local *STDOUT, 'Hoge';
    print "hoge";
    print "fuga";
    print "piyo";
}

for my $line ($hoge->lines) {
    print "$line\n";
}


package Hoge;

sub TIEHANDLE {
    my $class = shift;
    my @lines;
    bless \@lines, $class;
}

sub PRINT {
    my $self = shift;
    for my $arg (@_) {
        push(@$self, qq['$arg'が書かれたよ]);
    }
}

sub lines {
    my $self = shift;
    return @$self;
}

実行結果:

$ ./tie.pl 
'hoge'が書かれたよ
'fuga'が書かれたよ
'piyo'が書かれたよ
  1. 使用するメソッド(TIEHANDLE, PRINTなど)を実装したpackageを作成する
  2. tieすると引数に指定したHANDLEがCLASSに結びつけられる
    • この場合local *STDOUTがHogeクラスに結びつけられる
  3. この後、このスコープ内ではSTDOUTへの操作はすべてHogeに結びつけられる
    • ので、print文はHoge::PRINTメソッドが呼ばれることになる
  4. Hoge内では引数をもらって'書かれたよ'と内部変数に保存
  5. スコープから抜けるとprint文は普通にコンソールに出力される

…というカンジかな?


わかったようなわかってないような。。


まぁ、おそらく実際にこういうのを書くことは少なくて、内部でtieを使っているモジュールを使うことが多いのではないかと。

Tie::STDOUT

use宣言時に、STDOUTに対するそれぞれの操作(print, printfなど)で何を行うかを指定するらしい。

#!/opt/local/bin/perl
use strict;
use warnings;

use Tie::STDOUT print => sub {
    print join " ", map { $_ x 2 } @_;
    print "\n";
};

print 'ほげ', 'ふが', 'ぴよ';

syswrite STDOUT, "大事なことなので2回ずつ言いました\n";
$ ./tie_stdout.pl
ほげほげ ふがふが ぴよぴよ
大事なことなので2回ずつ言いました

どうすれば元に戻せるのかよく分からなかった。。

IO::Capture::Stdout

これは名前の通り、STDOUTへの出力をキャプチャしてくれる。

#!/opt/local/bin/perl
use strict;
use warnings;

use IO::Capture::Stdout;

my $capture = IO::Capture::Stdout->new;

$capture->start;
print "hoge";
print "fuga";
print "piyo";
$capture->stop;

for my $line (reverse $capture->read) {
    print $line, "\n";
}
$ ./capture_stdout.pl 
piyo
fuga
hoge

これは便利だ。readはスカラーコンテキストだと1行ずつ読んでくれるし、リストコンテキストだとまとめて返してくれる。
ソースをみると、start時にtie, stop時にuntieしているのが分かる。

追記

ブコメIO::Scalarを使った方法もありますと教えていただきました。id:kitsさん、ありがとうございます。
IO::Scalarで色々 - 徒書
標準出力のキャプチャリングはこれを使うのも良さそうだ。

#!/opt/local/bin/perl
use strict;
use warnings;

use IO::Scalar;

my $str;
{
    my $fh = new IO::Scalar(\$str);
    local *STDOUT = $fh;
    print 'hoge';
    print 'fuga';
    print 'piyo';
}

print "output: $str\n";
$ ./io_scalar.pl 
output: hogefugapiyo

このようにして、STDOUTへの出力を変数に格納しておくことができる。


もしくは、明示的にtieを書くとこうかな?

#!/opt/local/bin/perl
use strict;
use warnings;

use IO::Scalar;

my $str;
{
    tie local *STDOUT, 'IO::Scalar', \$str;
    print 'hoge';
    print 'fuga';
    print 'piyo';
}

print "output: $str\n";