perl

Perl: IPC::Run escaping problems with special characters, commands not working


I am having problems running commands using IPC::run with special characters, in my current case ' and *

The used commands:

apt-get -y --allow-unauthenticated -o Dpkg::lock::timeout=0 install /tmp/archive-keyring*.deb

(the file /tmp/archive-keyring_2022.04.01~tux_all.deb exists)

and

dpkg-query -f '${db:Status-Abbrev} ${Package} ${Version}\n' -W 'archive-keyring'

work without problems when used on a CLI with sudo. Of course this script has to be started as sudo as well. If it helps, I am using an Ubuntu system.

The module I am using is libipc-run-perl version 20231003.0-1

Here the example script:

#!/usr/bin/perl -w                                                               
use strict qw(vars subs);
use warnings;
use IPC::Run qw( run timeout );
use Data::Dumper;

# for debugging
$ENV{IPCRUNDEBUG} = 'data';

sub startProgram {
    my ($subOutput, $subErrors, $subTimeout, $subReturnValue, @subCommand) = @_;
    # for debug
    print Dumper(@subCommand);
    my $runExitCode = q{};
    eval {
        # undef has to be \undef !!
        $runExitCode = run \@subCommand, \undef, $subOutput, $subErrors, timeout ( $subTimeout, exception=>'timeout' );
    };

    if ($@ =~ /timeout/) {
        $subReturnValue = exitCode();
        print "Timed out...\n";
    } else {
        $subReturnValue = exitCode();
        print "Completed task without timeout\n";
    }
    print   "output: >$$subOutput<\n".
            "errors: >$$subErrors<\n".
            "return value: >$subReturnValue<\n".
            "run exit code: >$runExitCode<\n\n";
    return ($runExitCode);
}

sub exitCode {
    return ($?>>8);
}


my $output = q{};
my $errors = q{};
my $timeout = 5;
my $retVal = q{};

# does not work
# problem is: *
# actual command on cmdline:
# apt-get -y --allow-unauthenticated -o Dpkg::lock::timeout=0 install /tmp/archive-keyring*.deb
my @command1 = (
    'apt-get',
    '-y',
    '--allow-unauthenticated',
    '-o',
    'Dpkg::lock::timeout=0',
    'install',
    '/tmp/archive-keyring*.deb');
startProgram(\$output, \$errors, $timeout, \$retVal, @command1);

# does not work
my @command2 = (
    'apt-get',
    '-y',
    '--allow-unauthenticated',
    '-o',
    'Dpkg::lock::timeout=0',
    'install',
    '/tmp/archive-keyring\*.deb');
startProgram(\$output, \$errors, $timeout, \$retVal, @command2);

# does not work
my $fileName = q{/tmp/archive-keyring*.deb};
my @command3 = (
    'apt-get',
    '-y',
    '--allow-unauthenticated',
    '-o',
    'Dpkg::lock::timeout=0',
    'install',
    $fileName);
startProgram(\$output, \$errors, $timeout, \$retVal, @command3);

# this works !! but I have to use * for different reasons
# I am using here the full filename of the package
my @command4 = (
    'apt-get',
    '-y',
    '--allow-unauthenticated',
    '-o',
    'Dpkg::lock::timeout=0',
    'install',
    '/tmp/archive-keyring_2022.04.01~tux_all.deb');
startProgram(\$output, \$errors, $timeout, \$retVal, @command4);


# Another command I need and is not working:
# dpkg-query -f '${db:Status-Abbrev} ${Package} ${Version}\n' -W 'archive-keyring'

# here the problem seems to be: '
# IPC::run seems to convert ' into ''
# does not work
my @command5 = (
    'dpkg-query',
    '-f',
    '\'${db:Status-Abbrev} ${Package} ${Version}\n\'',
    '-W',
    '\'archive-keyring\'');
startProgram(\$output, \$errors, $timeout, \$retVal, @command5);

# does not work
my @command6 = (
    "dpkg-query",
    "-f",
    "'\${db:Status-Abbrev} \${Package} \${Version}\n'",
    "-W",
    "'archive-keyring'");
startProgram(\$output, \$errors, $timeout, \$retVal, @command6);

# does not work
my $package = 'archive-keyring';
my @command7 = (
    "dpkg-query",
    "-f",
    "'\${db:Status-Abbrev} \${Package} \${Version}\n'",
    "-W",
    $package);
startProgram(\$output, \$errors, $timeout, \$retVal, @command7);

# does not work
# same as before but completely separated as array
my @command8 = (
    'dpkg-query',
    '-f',
    '\'${db:Status-Abbrev}',
    '${Package}',
    '${Version}\n\'',
    '-W',
    '\'archive-keyring\'');
startProgram(\$output, \$errors, $timeout, \$retVal, @command8);

# does not work
my @command9 = (q{dpkg-query}, q{-f}, q{'${db:Status-Abbrev} ${Package} ${Version}\n'}, q{-W}, q{'archive-keyring'});
startProgram(\$output, \$errors, $timeout, \$retVal, @command9);

exit (0);

Somehow I am having problems escaping the characters * and '. It seems like IPC::run itself is escaping the characters as well rendering the commands useless.

What am I doing wrong?

How do I escape correctly?

Some of the debug info I get are:

execing /usr/bin/apt-get -y --allow-unauthenticated -o Dpkg::lock::timeout=0 install /tmp/archive-keyring*.deb

errors: >E: Unsupported file /tmp/archive-keyring*.deb given on commandline


execing /usr/bin/dpkg-query -f ''${db:Status-Abbrev} ${Package} ${Version}\n'' -W 'archive-keyring'

errors: >dpkg-query: no packages found matching 'archive-keyring'


IPC::Run 0001 [#6(14085) dpkg-query]: execing /usr/bin/dpkg-query -f ''${db:Status-Abbrev} ${Package} ${Version}
IPC::Run 0001 [#6(14085) dpkg-query]: '' -W 'archive-keyring'

errors: >dpkg-query: no packages found matching 'archive-keyring'


IPC::Run 0001 [#7(14086) dpkg-query]: execing /usr/bin/dpkg-query -f ''${db:Status-Abbrev} ${Package} ${Version}
IPC::Run 0001 [#7(14086) dpkg-query]: '' -W archive-keyring

errors: >dpkg-query: no packages found matching archive-keyring


execing /usr/bin/dpkg-query -f '${db:Status-Abbrev} ${Package} ${Version}\n' -W 'archive-keyring'

errors: >dpkg-query: error: need an action option

As I said, the commands work perfectly when entered into a terminal.


Solution

  • The following is a shell command:

    dpkg-query -f '${db:Status-Abbrev} ${Package} ${Version}\n' -W 'archive-keyring'
    

    To evaluate it, the shell executes dpkg-query with the following arguments:

    Note the lack of quotes.

    To pass these arguments using run, one could use the following:

    my @cmd = (
       "dpkg-query" => (
          "-f" => '${db:Status-Abbrev} ${Package} ${Version}\n',
          "-W" => "archive-keyring",
       )
    );
    

    The following is a shell command:

    apt-get -y --allow-unauthenticated -o Dpkg::lock::timeout=0 install /tmp/archive-keyring*.deb
    

    To evaluate it, the shell executes apt-get with the following arguments:

    Note that /tmp/archive-keyring*.deb was expanded by the shell.

    If you're going to avoid the shell, you're going to need to replicate the behaviour of the shell.

    my @cmd = (
       "apt-get" => (
          "-y",
          "--allow-unauthenticated",
          "-o" => "Dpkg::lock::timeout=0",
          "install" => glob( "/tmp/archive-keyring*.deb" ),
       )
    );
    

    The following function shows the arguments the shell passes to it:

    show-args() { perl -Mv5.14 -e'say 0+@ARGV; say ">$_<" for @ARGV' -- "$@"; }
    

    We can examine your commands using it.

    show-args dpkg-query -f '${db:Status-Abbrev} ${Package} ${Version}\n' -W 'archive-keyring'
    
    5
    >dpkg-query<
    >-f<
    >${db:Status-Abbrev} ${Package} ${Version}\n<
    >-W<
    >archive-keyring<
    
    show-args apt-get -y --allow-unauthenticated -o Dpkg::lock::timeout=0 install /tmp/archive-keyring*.deb
    
    7
    >apt-get<
    >-y<
    >--allow-unauthenticated<
    >-o<
    >Dpkg::lock::timeout=0<
    >install<
    >/tmp/archive-keyring_2022.04.01~tux_all.deb<