Consider:
#!/usr/bin/perl
use strict;
use warnings;
my $command = "nosuchcommand"
my $rc = open( my $fh, "-|", "$command" );
if ( ! defined( $rc ) )
{
print "My own error message\n";
}
else
{
system( "ps -p $rc" );
}
Expected output:
My own error message
Observed output:
Can't exec "nosuchcommand": No such file or directory at ./mcre line 8.
My own error message
How can I catch / trap / suppress the error message generated by Perl on a failing open-pipe (in favor of my own error handling)?
Further observations:
If I open( my $fh, "-|", "$command 2>&1" );, I get the expected output. But in case of success of $command, I would get the STDERR of $command mixed into STDOUT, and I don't want that.
If I open( my $fh, "-|", "$command 2>/dev/null" );, I get this:
PID TTY TIME CMD
15143 pts/0 00:00:00 sh <defunct>
So the specific redirecion of 2>/dev/null (instead of 2>&1) changed the return value of open... this was somewhat surprising, but is not the core of my question.
On IPC::Run
This has been suggested in two answers by now. I understand that it might be the "correct" answer in the general case, but would really prefer not to (as IPC::Run is not an option for me on one very significant target platform that comes without it and does not offer an easy way to install it).
That message doesn't come from Perl, but from the shell you launched to execute your shell command.[1] That's because
open( my $fh, "-|", $shell_command )
is equivalent to
open( my $fh, "-|", "/bin/sh", "-c", $shell_command )
As you suggested, you could redirect STDERR using 2>/dev/null, but that will also silence errors from the program (when a valid program is used).
Here's what you could do:
use IPC::Run qw( run );
my @cmd = ( $prog, @args );
eval { run \@cmd };
if ( $@ ) {
die( "Problem creating child: $@" );
}
elsif ( $? & 0x7F ) {
die( "Child died from signal ".( $? & 0x7F )."\n" );
}
elsif ( $? >> 8 ) {
die( "Child exited with error ".( $? >> 8 )."\n" );
}
If you want even more control, you'll need to implement things yourself. The source of IPC::Open3's open3 would be a good start.
As for redirection in the shell, we can redirect STDERR separately to a file
command 2> stderr.out
(as well as any file descriptors)
This leaves STDOUT going out, as you want in a pipe-open, or redirect it as well (cmd > stdout.out 2> stderr.out).
The mechanism can be used in Perl code since with shell metacharacters in the command (like redirection symbols) a shell is used.† Perl uses sh but that is normally linked to another shell so check which shell is used instead of sh in your distribution. (Or run via bash explicitly.)
An example
use warnings;
use strict;
use feature 'say';
my $errfile = 'err.out';
my $pid = open my $fh, '-|', "ls *txt nofile 2> $errfile"
// die "fork failed: $!";
print "stdout: $_" while <$fh>;
close $fh or say "Exit: ", $? >> 8;
if (-z $errfile) {
unlink $errfile or warn "Can't unlink $errfile: $!"
}
else { # there was STDERR stream printed, see it
print "error-file:\n",
my $err = do { local (@ARGV, $/) = $errfile; <> };
# investigate $err more closely?
}
So this does capture the STDERR stream, and on its own, but then there is a file to deal with.‡
The open's return is the pid of the child process (which here is the shell, regardless of whether the command can then be started by it), unless fork itself failed.§ Checking close is the way to see how things went and get a hint of possible problems -- the command couldn't be started, it reports a non-zero status, etc
If the filehandle came from a piped open,
closereturns false if one of the other syscalls involved fails or if its program exits with non-zero status. ...
Here the command (ls) is started successfully and prints the *txt files if any, and the exit code (2 on my system). The message that would come out on the STDERR stream about a non-existent nofile goes instead into the err.out file.
In case when the command itself fails to start only the exit code is printed (127), while the STDERR message again winds up in the file.
We can't directly tell whether the message redirected to a file is about the program (that doesn't exist or failed to start) or from the program itself as it prints to STDERR. But we do keep that error in a file so that it can be investigated more closely, unlike when it's sent to /dev/null.
Finally, I too would recommend IPC::Run instead. Basic uses are trivial, while it is almost a mini-shell if you need it to be.
† External commands are started either by starting a shell and passing it the string with the command and arguments, or by using one of execvp functions to start the command directly. If the command has no shell "metacharacters" (redirections, globbing, quoting, etc) the process is normally optimized by avoiding the shell. This can be ensured by the syntax, see system and exec. Otherwise the shell is started so to be able to perform indicated actions.
This clearly makes a difference: with the shell the command undergoes its syntax rules as well, we may get shell's messages, in pipe-open a process is forked for the shell (fork normally doesn't fail) and then another for the command, etc. With no shell we can get perl's messages about the (failed) command. In either case we may also get messages from the program itself. What went down is not normally easily disentangled without getting into details or using libraries (and getting into details).
‡ I read the file in one statement (and save it into a variable) with builtin tools since an issue with non-core modules was mentioned. If installing software is actually not a problem, there are good libraries for this. For instance
use Path::Tiny; # path, slurp
my $err = path($errfile)->slurp;
§ An example in open docs implies that a command that can't start results in open returning false. That is valid only for commands which don't involve shell metacharacters and so are started directly -- so if they fail to start there is indeed no process.
Also see this topic in perlipc, with a more accurate error message in examples.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With