可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答2:
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
.
回答3:
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:
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.
回答4:
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!";
}
回答5:
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.