'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 example

  • 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