'Perl backticks using bash
In Perl, the default shell to execute backticks is sh. I'd like to switch to use bash for its richer syntax. So far I found that the suggested solution is
`bash -c \"echo a b\"`
The apparent drawback is that the escaped double quotes, which means I will have difficulty to use double quotes in my bash args. For example, if I wanted to run commands requiring double quotes in bash
echo "a'b"
The above method will be very awkward.
Perl's system() call has a solution for this problem: to use ARRAY args,
system("bash", "-c", qq(echo "a'b"));
This keeps my original bash command unmodified, and almost always.
I'd like to use ARRAY args in backticks too. Is it possible?
Solution 1:[1]
Capture::Tiny is a very nice option: as the SYNOPSIS shows, you can do
use Capture::Tiny 'capture';
my ($output, $error_output, $exit_code) = capture {
system(@whatever);
};
as well as using system inside capture_stdout
if you want the simpler behavior of backticks.
Plus it's very general-purpose, working on Perl code (even Perl code that does weird stuff) as well as external programs, so it's a good thing to have in your toolbox.
Solution 2:[2]
For one, one can submit a list to qx
; it gets interpolated into a string and then passed to either execvp
or a shell (see qx, and the second part of this post and comments). And if you need a shell then presumably that string contains shell metacharacters so it goes via shell.
my @cmd = qw(ls -l "dir with spaces");
#my @cmd = qw(ls -l "dir with spaces" > outfile);
my @out = qx(@cmd);
print for @out;
I make a "dir with spaces"
directory with a file in it to test. (For a command with quotes in it a shell does get used.)
Next, I would in principle recommend a module to compose those shell commands, instead of going through a nail-biter to correctly escape and pass it all, like String::ShellQuote
use String::ShellQuote qw(shell_quote);
my @cmd = ('ls', '-l', q(dir with spaces));
my $quoted = shell_quote(@cmd);;
my @out = qx($quoted);
#my @out = qx($quoted > outfile);
print for @out;
I use the q(...)
operator form of single quotes to demonstrate another way (also useful for including single quotes); it is not necessary for this simple example. One still need be careful with details; that's in the nature of using complex external commands and cannot be fully avoided by any approach or tool.
As for running bash
, note that normally sh
delegates to a default-of-sorts shell on your system, and on many systems it is bash
that is used. But if it isn't on yours, one way to use bash -c
in the command would be to first prepare the command and then add that to the qx
string
my @cmd = ('ls', '-l', q(dir with spaces));
my $quoted = shell_quote(@cmd);
my @out = qx(bash -c "$quoted");
#my @out = qx(bash -c "$quoted" > outfile);
print for @out;
A couple more notes I'd like to offer:
That
qx
is an ancient demon. How about using modern tools/modules for running external commands? There may be a little more to do in order to prepare your involved bash strings but then everything else will be better. Options abound. For exampleIPC::System::Simple with its few utility functions
Use Capture::Tiny to wrap a
system
call with syntax you preferThe IPC::Run can do any and all of this and then way way more
Why use external commands, with Perl's (far) superior richness? It's a whole, very complete, programming language, vs. the command-interpreter with some programming capabilities. If you need shell's capabilities why not run just those things via the shell and do all else in Perl?
Solution 3:[3]
Given
my @cmd = ( "bash", "-c", qq(echo "a'b") );
You can use any of the following:
use Shell::Quote qw( shell_quote );
my $cmd = shell_quote( @cmd );
my $output = `$cmd`;
die "Can't spawn child: $!\n" if $? == -1;
die "Child killed by signal ".( $? & 0x7F )."\n" if $? & 0x7F;
die "Child exited with error ".( $? >> 8 )."\n" if $? >> 8;
or
use IPC::System::Simple qw( capturex );
my $output = capturex( @cmd );
or
use IPC::Run qw( run );
run \@cmd, '>', \my $output;
die "Child killed by signal ".( $? & 0x7F )."\n" if $? & 0x7F;
die "Child exited with error ".( $? >> 8 )."\n" if $? >> 8;
Solution 4:[4]
I have the following sub that works
sub bash_output {
my ($cmd) = @_;
open my $ifh, "-|", "bash", "-c", $cmd or die "cannot open file handler: $!";
my $output = "";
while (<$ifh>) {
$output .= $_;
}
close $ifh;
return $output;
}
print "test bash_output()\n";
my @strings = (
qq(echo "a'b"),
'echo $BASH_VERSION',
'[[ "abcde" =~ bcd ]] && echo matched',
'i=1; ((i++)); echo $i',
);
for my $s (@strings) {
print "bash_output($s) = ", bash_output($s), "\n";
}
The output is
bash_output(echo "a'b") = a'b
bash_output(echo $BASH_VERSION) = 4.4.20(1)-release
bash_output([[ "abcde" =~ bcd ]] && echo matched) = matched
bash_output(i=1; ((i++)); echo $i) = 2
My answer is long-winded but it fills my need. I was hoping Perl has a built-in solution just like how it handles system() call and I am still hoping.
Solution 5:[5]
You can change the shell used by perl :
$ENV{PERL5SHELL} = "bash";
my $out = qx{echo "Hello ' world from bash \$BASH_VERSION"};
print($out);
Solution 6:[6]
You can use single quotes as delimiter with qx
like this:
my $out = qx'bash -c "echo a b"';
this will according to perlop protect the command from Perl's double-quote interpolation.
Unfortunately, this does not work for single quotes. If you want to do echo "'"
for example, you need the following:
my $out = `bash -c \"echo \\\"'\\\"\"`;
Edit:
To help you managing the escaping of quotes you could use a helper function like this:
use experimental qw(signatures);
sub bash_backticks($code) {
$code =~ s/'/'"'"'/g;
`bash -c '$code'`
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | hobbs |
Solution 2 | |
Solution 3 | |
Solution 4 | oldpride |
Solution 5 | Philippe |
Solution 6 |