6

I've written some code in Perl which executes some bash command within its execution. My problem was when bash command attributes contained white space inside which failed bash command execution. But I've managed to work with those argument simply adding quotes around argument. Unfortunately during tests I've found that my script fails when \ character is placed at the end of variable which obviously escapes bash execution quotes and fails bash command. For example:

my $arg1 = 'This is sample text which can be put into variable. And a random sign\\';
`echo "$arg1"`

Is there any chance to automatically escape special characters in variable?

J33nn
  • 3,034
  • 5
  • 30
  • 46

5 Answers5

12

It's much easier to use single quotes in bash; then the only character you need to worry about is a single quote itself.

($arbitrary_string) =~ s/'/'"'"'/g;

`echo '$arbitrary_string'`
ysth
  • 96,171
  • 6
  • 121
  • 214
4

CPAN to the rescue.

String::ShellQuote should do what you need, although I concur with @glennjackman that system (or Capture::Tiny) is a better approach:

use String::ShellQuote 'shell_quote';

my $cmd = shell_quote( 'echo', 'This is sample text ending in a slash \\' );

`$cmd`;
Diab Jerius
  • 2,310
  • 13
  • 18
2

Don't use backticks to keep your string away from the shell, and hence all the quoting issues:

system 'echo', $var;

You write:

I need stdout from this command unfortunately. Other thing is that variables are used in a lil bit complicated command which uses many pipes and stuff: echo $var | sed | grep | awk | and so on... and so on...

You might want to investigate with something like this (untested, largely cribbed from the IPC::Open3 perldoc)

use IPC::Open3;
use Symbol 'gensym';

my ($wtr, $rdr, $err);
$err = gensym;
my $pid = open3($wtr, $rdr, $err, 'sed|grep|awk|and so on');
print $wtr $var;
close $wtr;
my $output = <$rdr>;
waitpid $pid, 0;

If your pipeline is very messy, store it in a shell script file, and invoke that script from perl.

glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • ``system`` is cool, but it returns command's exit status and not stdout, so it might make no sense in case of ``echo`` or similar commands where you need to get stdout. – Aleks-Daniel Jakimenko-A. Jul 21 '14 at 19:04
  • That's a good point. The code in this question does not capture stdout, so it depends on what the asker is doing. – glenn jackman Jul 21 '14 at 19:05
  • I've just read [this thread](http://www.perlmonks.org/?node_id=380670) and it seems like there is one more problem with this approach: some programs will detect whether they're running in terminal or not. – Aleks-Daniel Jakimenko-A. Jul 21 '14 at 19:09
  • If you don't need to worry about whether something is attached to a terminal (most programs don't care), [Capture::Tiny](https://metacpan.org/pod/Capture::Tiny) can help. – Diab Jerius Jul 21 '14 at 20:47
  • I need stdout from this command unfortunately. Other thing is that variables are used in a lil bit complicated command which uses many pipes and stuff: `echo $var | sed | grep | awk | and so on... and so on...` – J33nn Jul 22 '14 at 14:47
  • @J33nn are you sure that perl cannot do that job natively? – Aleks-Daniel Jakimenko-A. Jul 22 '14 at 21:26
  • @Aleks-DanielJakimenko of course I can do it natively parsing each line in perl execution but this approach is faster. Why sedding, grepping, awking in perl if I can do it via bash stream. – J33nn Jul 23 '14 at 10:39
  • @J33nn because it [creates multiple processes](http://web.cse.ohio-state.edu/~mamrak/CIS762/pipes_lab_notes.html). Piping any of the mentioned tools is already considered a bad style in bash, doing it from perl is even worse. – Aleks-Daniel Jakimenko-A. Jul 24 '14 at 22:37
  • Please elaborate why piping in bash is bad style? Any articles I can look at? I've never heard this. – J33nn Jul 25 '14 at 09:35
  • _"Piping any of the mentioned tools is already considered a bad style in bash"_ Well, that's just WRONG, because that is what sh is all about. See also: [Pipes, Redirection, and Filters](http://www.catb.org/esr/writings/taoup/html/ch07s02.html#plumbing). Creating multiple processes is not a bad thing either, it's not expensive, it's safe and it's why multiprocessing systems were invented. In the 60s. – David Tonhofer May 22 '16 at 11:00
0

Use quotemeta:

my $arg1 = quotemeta('string to be escaped');
`echo $arg1`

Or \Q\E (which is exactly what quotemeta is);

my $arg1 = 'string to be escaped';
`echo \Q$arg1\E`

And also please note that using echo is a bad way to print arbitrary strings.

And do NOT place any quotes around parameters if you're using quotemeta.

Aleks-Daniel Jakimenko-A.
  • 10,335
  • 3
  • 41
  • 39
  • 3
    quotemeta is designed to quote strings so that Perl doesn't get confused. It doesn't know how to escape things for shells, which have different rules. – Diab Jerius Jul 21 '14 at 20:43
  • 1
    @DiabJerius can you provide any example when it can break? – Aleks-Daniel Jakimenko-A. Jul 22 '14 at 01:07
  • 2
    That's a fair question. bash's quoting/escaping semantics are much more regular than say, csh/tcsh, and bash's documentation does say: "A non-quoted backslash (\) is the escape character. It preserves the literal value of the next character that follows, with the exception of ." – Diab Jerius Jul 22 '14 at 14:38
  • 2
    To continue my last comment, quotemeta only guarantees that ASCII non-"word" characters are escaped. Its Unicode behavior seems complex to me, and I don't know how it maps onto bash's. It's possible that bash's escaping requirements are a subset of quotemeta's, but I prefer to avoid making assumptions about whether or not all of their corner cases match. – Diab Jerius Jul 22 '14 at 14:50
0

When calling directly from perl, as Glenn Jackman says, I would use system or exec to avoid quoting trouble and to capture the output, for example to compute the md5sum of a file:

my $pid, $safe_kid; 
die "cannot fork: $!" unless defined ($pid = open($safe_kid, "-|"));
if ($pid == 0) {
   # This is the child process, exec md5sum
   exec('/usr/bin/md5sum', '--binary', $filename) or die "can't exec md5sum: $!";
} else {
   # This is the parent process, read data (we do not need to wait for
   # child process termination)
   @sum = <$safe_kid>;
   close $safe_kid; # $? contains status 
}
if ($?!=0) {
   die "Problem computing hashsums on '$filename': $!\n";
}

On a related note, I was looking for a way to print command line arguments to the shell for the user to copy and paste. I hit upon the idea of single-quoting everything, and using echo to recompose the string if a single-quote was already present, although using String::ShellQuote seems to be a better idea, really:

#!/usr/bin/perl

use strict;

testEscapeForBash("Hello World");
testEscapeForBash("Hello World!");
testEscapeForBash("Nothing_special_here.txt");
testEscapeForBash("With'One Single Quote");
testEscapeForBash("With 'Two Single' Quotes");
testEscapeForBash("'With Surrounding Single Quotes'");
testEscapeForBash("With \$ Dollar Sign");
testEscapeForBash("With * Fileglob");
testEscapeForBash("With ! History Expansion Sign");
testEscapeForBash("   ");
testEscapeForBash(" Some surrounding spaces ");

sub testEscapeForBash {
   my ($in) = @_;
   my $out = escapeForBash($in);
   print "[$in] gives\n       $out\n";
}

sub escapeForBash {
   my ($name) = @_;
   if (!$name) {
      die "Empty name passed to 'escapeForBash'"
   }
   my @parts = split(/'/,$name,-1); # limit is negative to catch trailing quote
   my $res;
   if (@parts == 1) {
      $res = "'$name'"
   }
   elsif (@parts > 1) {
      $res = '$(echo ';
      my $first = 1;
      for my $part (@parts) {
         $res .= "\"'\"" unless $first;
         $first = 0;
         $res .= "'";
         $res .= $part;
         $res .= "'";
      }
      $res .= ')';
   }
   else {
      die "Weird number of parts: @parts"
   }
   return $res
}

Let's run this:

[Hello World] gives
       'Hello World'
[Hello World!] gives
       'Hello World!'
[Nothing_special_here.txt] gives
       'Nothing_special_here.txt'
[With'One Single Quote] gives
       $(echo 'With'"'"'One Single Quote')
[With 'Two Single' Quotes] gives
       $(echo 'With '"'"'Two Single'"'"' Quotes')
['With Surrounding Single Quotes'] gives
       $(echo ''"'"'With Surrounding Single Quotes'"'"'')
[With $ Dollar Sign] gives
       'With $ Dollar Sign'
[With * Fileglob] gives
       'With * Fileglob'
[With ! History Expansion Sign] gives
       'With ! History Expansion Sign'
[   ] gives
       '   '
[ Some surrounding spaces ] gives
       ' Some surrounding spaces '
David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
  • 1
    See also [How can I safely pass a filename with spaces to an external command in Perl?](http://stackoverflow.com/q/1267669/2173773) and in particular the [answer given by `mklement0`](http://stackoverflow.com/a/32161361/2173773). I also wrote some comments regarding `String::ShellQuote` below that answer. – Håkon Hægland May 22 '16 at 14:14