roger
roger

Reputation: 9893

how to execute multiline shell command in perl?

I want to run shell command as follow in perl:

tar --exclude="*/node_modules" \
--exclude="*/vendor" \
--exclude='.git' \
-zvcf /tmp/robot.tgz .

But it seems perl can not excute this:

`tar --exclude="cv/node_modules" \
--exclude="*/vendor" \
--exclude='.git' \
-zvcf /tmp/robot.tgz .`;

Here is the error:

tar: Must specify one of -c, -r, -t, -u, -x
sh: line 1: --exclude=*/vendor: No such file or directory
sh: line 2: --exclude=.git: command not found
sh: line 3: -zvcf: command not found

it seems perl treat each line as one command.

Upvotes: 3

Views: 4445

Answers (1)

Borodin
Borodin

Reputation: 126722

Update

I apologise. My original diagnosis was wrong

It is hard to clearly express in a post like this the contents of strings that contain Perl escape characters. If anything below is unclear to you then please write a comment to say so. I hope I haven't made things unnecessarily complicated

My original solution below is still valid, and will give you better control over the contents of the command, but my reasons for why the OP's code doesn't work for them were wrong, and the truth offers other resolutions

The problem is that the contents of backticks (or qx/.../) are evaluated as a double-quoted string, which means that Perl variables and escape sequences like \t and \x20 are expanded before the string is executed. One of the consequences of this is that a backslash is deleted if it is followed by a literal newline, leaving just the newline

That means that a statement like this

my $output = `ls \
-l`;

will be preprocessed to "ls \n-l" and will no longer contain the backslash that is needed to signal to the shell that the newline should be removed (or indeed to get the command passed to the shell in the first place)

Apart from manipulating the command string directly as I described in my original post below, there are two solutions to this. The first is to escape the backslash itself by doubling it up, like this

my $output = `ls \\
-l`;

which will prevent it from being removed by Perl. That will pass the backslash-newline sequence to the shell, which will remove it as normal

The other is to use qx'...' instead of backticks together with single-quote delimiters, which will prevent the contents from being processed as a double-quoted string

my $output = qx'ls \
-l';

This will work fine unless you have used Perl variables in the string that you want to be interpolated



Original post

The problem is that the shell removes newlines preceded by backslashes from the command string before executing it. Without that step the command is invalid

So you must do the same thing yourself in Perl, and to do that you must put the command in a temporary variable

use strict;
use warnings 'all';

my $cmd = <<'END';
tar --exclude="*/node_modules" \
--exclude="*/vendor" \
--exclude='.git' \
-zvcf /tmp/robot.tgz .
END

$cmd =~ s/\\\n//g;

my $output = `$cmd`;

There is no need for the backslashes of course; you can simply use newlines and remove those before executing the command

Or you may prefer to wrap the operations in a subroutine, like this

use strict;
use warnings 'all';

my $output = do_command(<<'END');
tar --exclude="*/node_modules" \
--exclude="*/vendor" \
--exclude='.git' \
-zvcf /tmp/robot.tgz .
END

sub do_command {
    my ($cmd) = @_;
    $cmd =~ s/\\\n//g;
    `$cmd`;
}

Upvotes: 14

Related Questions