views:

351

answers:

5

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.

+2  A: 

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.

hobbs
+3  A: 

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!";
}
Ether
+1 for Perl fix, and I also added an answer elaborating pure shell solution and a combination of a shell solution with a slightly-less-"drastic" change to the script :)
DVK
+1  A: 

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.

DaveE
"bash throws away the exit status on a pipe": this is not true — see my answer.
intuited
+4  A: 

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.

DVK
Thanks - additional downsides (and a crucial one in this case) is that the output wouldn't be available during the command's run, only once it's complete.
zigdon
@zigdon - sorry, didn't catch your last edit. You're correct - the child shell call (parenthesis) will indeed buffer the output of the commmand as far as I know.
DVK
+6  A: 

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.

intuited
technically you should send those `echo` es to stderr, using `echo "whatever" >`
intuited
Unfortunately I'm trying to avoid BASHisms (trying to get this to work on dash, for reasons out of my control). On bash I'm currently using 'set -o pipefail' which makes the entire pipeline return the RC of the first non-successful command.
zigdon
Hmm. I guess you need to use a temp file, then, similarly to @DVK's answer, but just read in the temp file's contents from within perl after you get EOF from stdin. I thought about using a fifo instead, but that won't work because perl won't see EOF from stdin until the fifo's been read, and presumably wouldn't check the fifo until input was done, ie *deadlock*. Though I guess you could loop timed-out reads from stdin and the fifo until you consumed the fifo (and then got EOF from stdin). It's a bit complicated, and you'd still have to make the fifo anyway, but it's less messy than temp files.
intuited