2
votes

I have a perl script that is run with a command like this:

/path/to/binary/executable | /path/to/perl/script.pl

The script does useful things to the output for the binary file, then exits once STDIN runs out (<> returns undef). This is all well and good, except if the binary exits with a non-zero code. From the script's POV, it thinks the script just ended cleanly, and so it cleans up, and exits, with a code of 0.

Is there a way for the perl script to see what the exit code was? Ideally, I'd want something like this to work:

# close STDIN, and if there was an error, exit with that same error.
unless (close STDIN) {
   print "error closing STDIN: $! ($?)\n";
   exit $?;
}

But unfortunately, this doesn't seem to work:

$ (date; sleep 3; date; exit 1) | /path/to/perl/script.pl /tmp/test.out
Mon Jun  7 14:43:49 PDT 2010
Mon Jun  7 14:43:52 PDT 2010
$ echo $?
0

Is there a way to have it Do What I Mean?

Edited to add:

The perl script is manipulating the output of the binary command in real-time, so buffering it all into a file is not a feasible solution. It doesn't, however, need to know the exit code until the end of the script.

5

5 Answers

8
votes

The bash environment variable $PIPESTATUS is an array that contains the statuses of each part of the last command's pipeline. For example:

$ false | true; echo "PIPESTATUS: ${PIPESTATUS[@]};  ?: $?"
PIPESTATUS: 1 0;  ?: 0

So it sounds like rather than refactoring your perl script, you just need the script running that piped command to check $PIPESTATUS. Using $PIPESTATUS without the [@] gives you the value of the array's first element.

If you need to check the status of both the initial executable and the perl script, you want to assign the $PIPESTATUS over to another variable first:

status=(${PIPESTATUS[@]})

Then you can check them individually, like

if (( ${status[0]} )); then echo "main reactor core breach!"; exit 1;
elif (( ${status[1]} )); then echo "perls poisoned by toxic spill!"; exit 2;
fi;

You have to do this via a temp variable because the next statement, even if it's an if statement, will reset ${PIPESTATUS[@]} before the following statement, even if it's an elif statement, can check it.

Note that this stuff only works with bash and not the original bourne shell (usually sh, though many systems link /bin/sh to /bin/bash due to its backwards compatibility). So if you put this in a shell script, the first line should be

#!/bin/bash

rather than #!/bin/sh.

4
votes

To elaborate on Ether's proposal, this is the shell workaround approach:

bash-2.03$ TF=/tmp/rc_$$; (/bin/false; echo $?>$TF) | 
           perl5.8 -e 'print "OUT\n"'; test `cat $TF|tr -d "\012"` -eq 0 
OUT
bash-2.03$ echo $?
1
bash-2.03$ TF=/tmp/rc_$$; (/bin/true; echo $?>$TF) | 
           perl5.8 -e 'print "OUT\n"'; test `cat $TF|tr -d "\012"` -eq 0     
OUT
bash-2.03$ echo $?
0
bash-2.03$ 

The downsides:

  • General ugliness

  • Leaves a mess of /tmp/rc_* files around

  • Loses the exact value of non-zero exit code (which in the example above was 255)

You can deal with all these downsides by minorly editing your Perl script to:

  • Read in contents of the file named $ENV{TF} (using File::Slurp::read_file()), say into my $rc
  • chomp $rc;
  • unlinking $ENV{TF}
  • exit $rc
  • Then your command line becomes: TF=/tmp/rc_$$; (/bin/true; echo $?>$TF) | /your/perl/script

This is a bit less invasive change compared to Ether's change of script to use system() call - you just add 4 lines to the very end of the script (including exit); but once you're changing the script anyway I'd probably recommend going all out and doing Ether's suggested change in the first place.

3
votes

I see two options:

  • you could rewrite the script so that it invokes the command itself, so that it can detect its exit status and take different action if it did not exit successfully
  • you could wrap the invocation of the command in a shell script, which checked the exit value and then invoked the Perl script differently (essentially the same as option 1, except it doesn't require changing the Perl script).

However, since you're reading input in your Perl script from the command before it has exited, you obviously don't have a return code yet. You would only get access to that once the command is finished, so you would need to buffer its output somewhere else in the meantime, such as a file:

use IPC::System::Simple qw(system $EXITVAL);
use File::Temp;

my $tempfile = File::Temp->new->filename;
system("/path/to/binary/executable > $tempfile");
if ($EXITVAL == 0)
{
     system("/path/to/perl/script.pl < $tempfile");
}
else
{
     die "oh noes!";
}
2
votes

You only get the exit status for your own child processes. The thing connected to your STDIN isn't perl's child process; it's the shell's. So sadly, what you want is not possible.

1
votes

Unfortunately, bash throws away the exit status on a pipe it seems. Running "sleep 3 | echo hi" initiates the echo before the sleep has even completed, so it has absolutely no chance to capture the exit status of the first command.

You could (in theory) run this by altering the bash command to a list of commands-- bash will save the value within $? (just like Perl), but then you've got to pass it to Perl script somehow, meaning that your Perl script will need to accept the exit status of the previous program on (say) the command line.

Alternatively, you could just rewrite the Perl script to run the command, and capture the exit status, or wrap the whole thing in yet-another script.