This is probably going overboard, but I have an idea for a comprehensive method for capturing STDIN/STDOUT. The goal is to capture all traffic, but be method agnostic. Also, I want to allow nested captures. The two general methods I know of for doing this involve dup'ing file handles and tieing file handles. Dup'ing is most useful when you want to capture all the output of a exec'd process. Tieing is generally useful when you need to capture output within Perl (e.g., IO::String, IO::Scalar, etc.). Oh, and the final requirement: it must work in Perl 5.6, so PerlIO is right out.

I should say that part of this solution is based upon knowledge primarily gained from the discussion in tieing STDIN & STDOUT using IO::Scalar for use inside an eval, Redirecting STDOUT from internal function with 5.6.1 restrictions, and Section 7.10 of Perl Cookbook, 2nd Ed by Christiansen and Torkington. I've also taken note of some modules designed to do something like this, such as IO::Capture, and of course IO::String and IO::Scalar. However, each of these falls short of my goals.

Read on for my solution...

I've tested this code in a very limited fashion (i.e., this script), but I want to know if anyone sees any caveats to this solution. Is it sound? Do you like it? What would you change?

The capture_streams() subroutine is the interesting part. The rest is the test script.

#!/usr/bin/perl use strict; use warnings; use File::Temp 'tempfile'; use IO::String; use Symbol; sub capture_streams { my $in = shift; my $out = shift; my $code = shift; my $tie_in = UNIVERSAL::can($in, 'TIEHANDLE'); my $tie_out = UNIVERSAL::can($out, 'TIEHANDLE'); my ($save_in, $save_out); my ($save_in_fd, $save_out_fd); # Save/capture STDIN if ($tie_in) { $save_in = tied *STDIN; tie *STDIN, $in; } else { if (tied *STDIN) { $save_in = tied *STDIN; no warnings 'untie'; untie *STDIN; } $save_in_fd = gensym; open($save_in_fd, '<&STDIN'); open(STDIN, '<&='.fileno($in)); } # Save/capture STDOUT if ($tie_out) { $save_out = tied *STDOUT; tie *STDOUT, $out; } else { if (tied *STDOUT) { $save_out = tied *STDOUT; no warnings 'untie'; untie *STDOUT; } $save_out_fd = gensym; open($save_out_fd, '>&STDOUT'); open(STDOUT, '>&='.fileno($out)); } # Run code within captured handles my $result; if (wantarray) { my @array = $code->(@_); $result = \@array; } else { $result = $code->(@_); } # Restore STDOUT if ($tie_out) { if (defined $save_out) { tie *STDOUT, $save_out; } else { no warnings 'untie'; untie *STDOUT; } } else { open(STDOUT, '>&='.fileno($save_out_fd)); close($save_out_fd); if (defined $save_out) { tie *STDOUT, $save_out; } } # Restore STDIN if ($tie_in) { if (defined $save_in) { tie *STDIN, $save_in; } else { no warnings 'untie'; untie *STDIN; } } else { open(STDIN, '<&='.fileno($save_in_fd)); close($save_in_fd); if (defined $save_in) { tie *STDIN, $save_in; } } return wantarray ? @$result : $result; } sub test_it { my ($in, $out, $data) = @_; print $in $data; seek $in, 0, 0; capture_streams($in, $out, sub { while (<STDIN>) { tr/abcdefghijklmnopqrstuvwxyz/zyxwvutsrqponmlkjihgfedcba/; print $_; } }); seek $out, 0, 0; while (<$out>) { print; } } sub nested_test_it { my ($in, $in2, $out, $out2, $data) = @_; print $in $data; seek $in, 0, 0; capture_streams($in, $out, sub { while (<STDIN>) { print $in2 $_; } seek $in2, 0, 0; capture_streams($in2, $out2, sub { while (<STDIN>) { tr/abcdefghijklmnopqrstuvwxyz/zyxwvutsrqponmlkjihgfedc +ba/; print $_; } }); seek $out2, 0, 0; while (<$out2>) { print; } }); seek $out, 0, 0; while (<$out>) { print; } } my $data = join '', <STDIN>; eval { print "Test 0\n"; print '=' x 70, "\n"; test_it(IO::String->new, IO::String->new, $data); }; print STDERR $@ if $@; eval { print "\nTest 1\n"; print '=' x 70, "\n"; test_it(scalar(tempfile), scalar(tempfile), $data); }; print STDERR $@ if $@; eval { print "\nTest 2\n"; print '=' x 70, "\n"; nested_test_it( IO::String->new, IO::String->new, IO::String->new, IO::String->new, $data); }; print STDERR $@ if $@; eval { print "\nTest 3\n"; print '=' x 70, "\n"; nested_test_it( scalar(tempfile), IO::String->new, IO::String->new, scalar(tempfile), $data); }; print STDERR $@ if $@; eval { print "\nTest 4\n"; print '=' x 70, "\n"; nested_test_it( IO::String->new, scalar(tempfile), scalar(tempfile), IO::String->new, $data); }; print STDERR $@ if $@; eval { print "\nTest 5\n"; print '=' x 70, "\n"; nested_test_it( scalar(tempfile), scalar(tempfile), scalar(tempfile), scalar(tempfile), $data); }; print STDERR $@ if $@;

Thanks,
Sterling


In reply to Very generic/nested capture of STDIN/STDOUT by hanenkamp

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post, it's "PerlMonks-approved HTML":



  • Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
  • Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
  • Read Where should I post X? if you're not absolutely sure you're posting in the right place.
  • Please read these before you post! —
  • Posts may use any of the Perl Monks Approved HTML tags:
    a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
  • You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Link using PerlMonks shortcuts! What shortcuts can I use for linking?
  • See Writeup Formatting Tips and other pages linked from there for more info.