Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to replace STDOUT with an IO::Tee object?

Tags:

perl

I have a very large program that writes a lot of things to STDOUT, I'd like to be able to feed all of that output to both STDOUT and to an arbitrary FILE. I've decided to use IO::Tee to create the shared writeable handle:

use IO::Tee;

pipe(my $reader, my $writer);
my $new_stdout = IO::Tee->new(\*STDOUT, $writer);

*{STDOUT} = $new_stdout;

print "foo";

print(do { local $\; <$reader> }); 

However, this causes a deep-recursion and crashes the program. So, instead I can not reference *STDOUT, and it creates it:

use IO::Tee;

pipe(my $reader, my $writer);
my $new_stdout = IO::Tee->new(*STDOUT, $writer);

*{STDOUT} = $new_stdout;

print "foo";

print(do { local $\; <$reader> });

This creates the warning: Undefined value assigned to typeglob at ... line 42, and when I use Data::Printer to describe $new_stdout, it is undef. How can I do this?

like image 884
Rawley Fowler Avatar asked Dec 05 '25 15:12

Rawley Fowler


1 Answers

Almost all code that accepts a file handle accepts it in many forms:

  • A reference to a glob with an IO object (\*STDOUT)
  • A glob with an IO object (*STDOUT)
  • An IO object (*STDOUT{IO})

Here is no exception. Instead of a passing a reference to a glob you later modify, you can solve this problem by passing the IO object originally assigned to STDOUT. This makes the later modification of *STDOUT moot.

Replace

IO::Tee->new( \*STDOUT, $writer )

with

IO::Tee->new( *STDOUT{IO}, $writer )

That leaves you with a number of other problems, though.

  • You'll need to empty the pipe before the end of the program or risk becoming deadlocked.
  • You'll need to close the pipe at some point if you're going to continue reading from the pipe until EOF.
  • You might want to disable buffering for the new handle ($new_stdout->autoflush;).

[just pipe its output to tee] would be ideal... But we're hoping to be able to package this as a library, such that we can e.g use Stdout::Replace

package Stdout::Replace;

use IPC::Open3 qw( open3 );

my $pid = open3( local *CHILD_STDIN, ">&STDOUT", ">&STDERR", "tee", "file.out" );
open( STDOUT, ">&", *CHILD_STDIN );  # dup2
close( CHILD_STDIN );
STDOUT->autoflush();

END {
   close( STDOUT );
   waitpid( $pid, 0 );
}

1;

Demo:

$ cat file.out
cat: file.out: No such file or directory

$ perl -I lib -Mv5.14 -e'use Stdout::Replace; say "foo";'
foo

$ cat file.out
foo

You can dup2 CHILD_STDIN onto STDERR too, if you so desire.

You can save and restore the original STDOUT as follows:

open( my $orig_stdout, ">&", *STDOUT );
...
open( STDOUT, ">&", $orig_stdout );
like image 57
ikegami Avatar answered Dec 08 '25 08:12

ikegami



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!