3

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?

con
  • 5,767
  • 8
  • 33
  • 62
  • Works For Me™ with Capture::Tiny 0.48 and Perl 5.32.0. Check Capture::Tiny is up to date. – Schwern Feb 02 '21 at 20:17
  • 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 Feb 02 '21 at 20:20
  • 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 Feb 02 '21 at 20:24
  • @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 properly – con Feb 02 '21 at 20:30
  • 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 Feb 02 '21 at 20:40

1 Answers1

7

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";
Schwern
  • 153,029
  • 25
  • 195
  • 336