2

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?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
oldpride
  • 761
  • 7
  • 15
  • 2
    Why do you think you need to escape the double quotes inside a backtick string? There are also lots of different ways to quote strings in Perl. Lastly, when you are trying to change shell from inside Perl to execute richer commands outside Perl, from inside Perl... well, maybe you should just use Perl? What are these commands that you need to use bash to execute? – TLP Feb 08 '22 at 16:08
  • One reason is that try to honor bash's syntax as much as possible. One application is to parse a string like it was a bash command. The script will take some user's bash command because Bash is better known to users than Perl is. From a Perl dev's perspective, you are right. From a user's perspective, bash is easier than Perl. – oldpride Feb 08 '22 at 16:14
  • 1
    If your users are so bad with Perl, why do they use it? Messing around with shells, escaping, quoting, keeping track of interpolation, error handling is potentially a LOT harder than just learning some simple Perl code. For example, you use `echo $BASH_VERSION` as a way to get that string into a variable, when in Perl it is just `$ENV{BASH_VERSION}`. `"abcde" =~ bcd` is almost exactly the same in Perl. None of your examples are easier in bash than Perl, and in fact are just more difficult and slower. It comes down to what exactly you are trying to do, which is why I asked. – TLP Feb 08 '22 at 17:20
  • Sorry, I may have confused you. I am the Perl developer. My users have basic bash knowledge. Therefore, my Perl script take bash-style instructions from users. I don't expect them to write perl script. – oldpride Feb 08 '22 at 20:16
  • Allowing users to execute arbitrary commands on your system? Sounds dangerous. Well, best of luck. – TLP Feb 08 '22 at 22:47

6 Answers6

4

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?

zdim
  • 64,580
  • 5
  • 52
  • 81
  • I saw that you had listed the three modules I suggested only after posting them. (I had only looked at the first paragraph before posting.) +1 – ikegami Feb 08 '22 at 20:41
  • @ikegami Thank you for saying that -- but it's good to show them nicely like you do (+1), instead of one having to wade through this long and chatty post for a mere mention – zdim Feb 09 '22 at 18:39
2

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.

oldpride
  • 761
  • 7
  • 15
  • one problem with this sub is that I am not sure it will pass on the $? in case the command failed. – oldpride Feb 08 '22 at 15:49
  • 1
    it's a common idiom to replace your `my $output = ""; while (<$ifh>) { $output .= $_ }` with `my $output = do { local $/; <$ifh> };` – hobbs Feb 09 '22 at 17:52
  • I would agree with you and gave you thumbs up, but my code is definitely more readable. I remember the day I came back from being a python programmer to being a perl programmer again, how hard it was to recall what those idioms meant. lol – oldpride Feb 09 '22 at 18:59
  • 1
    Here's a variation then (can't resist): `my $output = join '', <$ifh>;` All one need remember is that `<>` is context sensitive (so when feeding `join`, which takes a list, it returns all lines). But I don't have a problem with your way, it is crystal clear to anyone :) – zdim Oct 26 '22 at 16:51
  • this is better. – oldpride Jun 18 '23 at 03:19
2

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.

hobbs
  • 223,387
  • 19
  • 210
  • 288
  • Wow, this seems to be it. And it seems to be part of the core. It took care the stderr and exit code too. I think I saw it during perl installation or somewhere. – oldpride Feb 08 '22 at 20:27
  • I fed the command with array @whatever = ('bash', '-c', q(a=1 b="has spaces" c='also has spaces' d=$HOME e=`date +%H:%M:%S`));. It passed all tests. Thanks a lot – oldpride Feb 08 '22 at 20:42
  • also tested @whatever = ('bash', '-c', q(echo "a'b")); worked too – oldpride Feb 08 '22 at 20:54
  • 2
    @oldpride it's not core, but it's pure-perl, has no non-core dependencies, and works back to 5.6 (though you need 5.8.1 for sanity). – hobbs Feb 08 '22 at 21:17
  • this is important to know as most Linux systems inside company don't have non-core installed. It hits my job site too. So for portability, I have to use "require Capture::Tiny" instead of "use Capture::Tiny", but then "require" triggers error: Can't use string ("0") as a subroutine ref while "strict refs" in use at /usr/local/share/perl/5.26.1/Capture/Tiny.pm line 382 – oldpride Feb 09 '22 at 19:06
  • @oldpride no, it doesn't work that way. You use `use` whether it's core or not. – hobbs Feb 10 '22 at 18:14
2

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;
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • You can also use `open( my $pipe, "-|" )` plus `exec`, but the required error checking becomes extensive. You can look at the source of IPC::System::Simple's `capturex` if you want to see how this is done. – ikegami Feb 08 '22 at 20:37
  • Unlike the first, the last two solutions are portable (for a command that's appropriate to the system). Unlike the first, they also avoid invoking `sh` to launch `bash` on unixy systems. (No idea what happens internally on Windows.) – ikegami Feb 08 '22 at 20:39
0

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'`
}
Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
0

You can change the shell used by perl :

$ENV{PERL5SHELL} = "bash";
my $out = qx{echo "Hello ' world from bash \$BASH_VERSION"};
print($out);
Philippe
  • 20,025
  • 2
  • 23
  • 32
  • 1
    See also: https://stackoverflow.com/q/55010798/2173773. Unfortunately `PERL5SHELL` only works on Windows? – Håkon Hægland Feb 08 '22 at 13:32
  • @hakon, correct, it doesn't seem to work on my ubuntu linux. I tried to set PERL5SHELL both inside and outside the test perl script. The $BASH_VERSION wasn't printed out – oldpride Feb 08 '22 at 14:10