perl

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


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?


Solution

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

    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.


    [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 );