Zed Lopez

Escape From the Shell

Perl’s exec, system, and fork functions all let one execute system commands, and they all do it by driving a POSIX exec system call so they don’t invoke a shell. But Perl’s backtick operator (aka the qx quotelike operator), the easiest way to capture output from a command, invokes a shell, so the arguments to the command need to be shell-escaped.

Sysadm::Install has quote and qquote functions that do a good job of this, but it’s easy enough to avoid needing shell escaping.
sub command {
  my ($command, $dontchomp) = @_;
  $dontchomp //= 0;
  open(my $ph, qq{$command|}) or die "Can't fork $command: $!";
  my $raw = do { local $/ = <$ph> };
  close($ph) or die "$command returned error: $! $?";
  chomp $raw unless $dontchomp;
  return wantarray ? split "\
", $raw : $raw;
}

Note that you pass the whole command line as one string, arguments and all. The default behavior is to chomp the command output, which differs from the native backtick behavior; you can pass a true value for the second arg, $dontchomp, if you don’t want it.

Reading all the output into a scalar is suitable for things with modest amounts of output (like the situtations in which you would have been using a backtick.) If you were processing a lot of output, you’d want to open the pipehandle and iterate on <$ph> yourself.

The reason I went down this rabbit-hole is wanting to get info from ratpoison so I could write some window management scripts. When you pass ratpoison a command with its -c option, it expects the whole command as one string. So we do have to worry about quote-escaping that string, like so:

sub rp {
  local $_ = "@_";
  s/"/\\\\"/g;
  return command(qq{ratpoison -c "$_"});
}

The way this is written, you can pass your arguments as a string or as a list, to taste.

rp("windows %c %t %n");
rp("windows", "%c", "%t", "%n");

Ratpoison lets you pass it multiple commands at once, and this rp routine doesn’t, but that’s usually what you want when you’re getting info out of it.