3
votes

I'm trying to go through Capture::Tiny to get output of a command upon failure,

#!/usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use Carp 'confess';
use Capture::Tiny 'capture';

sub execute {
    my $cmd = shift;
    my ($stdout, $stderr, $exit) = capture {
      system( $cmd ); # the script dies here
    };
    if ($exit != 0) { # the script should die here
        say "exit = $exit";
        say "STDOUT = $stdout";
        say "STDERR = $stderr";
        confess "$cmd failed";
    }
    say "STDOUT = $stdout";
    say "STDERR = $stderr";
    say "exit code = $exit";
    return 0
}

execute("ls fakedir");

but the problem is that when I execute the script,

con@V:~/Scripts$ perl execute.pl
"ls fakedir" unexpectedly returned exit value 2 at /home/con/perl5/perlbrew/perls/perl-5.32.0/lib/site_perl/5.32.0/Capture/Tiny.pm line 382.

all I get is the exit value, which doesn't give the valuable information that fakedir doesn't exist. That is, I only get STDOUT and STDERR when the script succeeds.

Whether I use die or confess I get the same problem -> the script doesn't print the output $stderr and $stdout

I've tried IO::CaptureOutput as suggested on How do you capture stderr, stdout, and the exit code all at once, in Perl?, which does what I want, but the author https://metacpan.org/pod/IO::CaptureOutput says "This module is no longer recommended by the maintainer - see Capture::Tiny instead." This is strange, IO::CaptureOutput seems to work much better!

How can I get this script to die with $stdout, $stderr, and $exit printed with confess?

1
Works For Me™ with Capture::Tiny 0.48 and Perl 5.32.0. Check Capture::Tiny is up to date.Schwern
What does system $cmd do if you run it outside capture? If it dies you will have to trap that with an eval. It shouldn't die, but that shouldn't be Capture::Tiny's fault.Schwern
I can replicate your problem if I add use autodie "system" which will cause system to die on failure which you'd have to trap with an eval block inside capture (or no autodie in the capture block). Check your environment that you're not shadow loading autodie, that would be Bad.Schwern
@Schwern I also have Capture::Tiny version 0.48. I could've sworn that I wasn't using autodie, but I like autodie because it captures errors in other parts of the scripts. Indeed, that may be the problem. For this reason, I think that IO::CaptureOutput may be superior to Capture::Tiny because it allows autodie to work properlycon
autodie and Capture::Tiny are working properly. Capture::Tiny traps STDOUT and STDERR. It does not trap errors. That's up to you, the programmer who decided to make system raise an error. IO::CaptureOutput silently swallows the error, that's a bug.Schwern

1 Answers

6
votes

Capture::Tiny is working properly. system is raising an error as if use autodie "system" was on. Capture::Tiny only captures STDOUT and STDERR, it does not trap errors.

You must do the error handling. You can trap it with an eval block.

    my ($stdout, $stderr, $exit) = capture {
        eval {
            system( $cmd );
        }
    };
    if (my $e = $@) { # the script should die here
        # Can't get the system exit code from autodie.
        #say "exit = $exit"
        say "STDOUT = $stdout";
        say "STDERR = $stderr";
        confess "$cmd failed";
    }
    else {
        say "STDOUT = $stdout";
        say "STDERR = $stderr";
        say "exit code = $exit";
        return 0;
    }

In this specific case you're already doing what autodie does. It's simpler to just turn off autodie inside the capture block.

    my ($stdout, $stderr, $exit) = capture {
        no autodie "system";
        system( $cmd );
    };
    if ($exit) {
        say "exit = $exit";
        say "STDOUT = $stdout";
        say "STDERR = $stderr";
        # the script should die here
        confess "$cmd failed";
    }
    else {
        say "STDOUT = $stdout";
        say "STDERR = $stderr";
        say "exit code = $exit";
        return 0;
    }

Or, since you're going to error anyway, you could let autodie do its thing.

    my ($stdout, $stderr, $exit) = capture {
        system( $cmd );
    };
    say "STDOUT = $stdout";
    say "STDERR = $stderr";
    say "exit code = $exit";