From 3f8354f58fe6e6ae9101e1d21e324dba5595596e Mon Sep 17 00:00:00 2001 From: Amrouche Hamza Date: Tue, 31 Oct 2017 07:00:43 +0100 Subject: [PATCH] [Process] Allow writing portable "prepared" command lines --- src/Symfony/Component/Process/Process.php | 24 +++++++--- .../Component/Process/Tests/ProcessTest.php | 44 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 5e3993d7882c..755a574de6b4 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -291,6 +291,12 @@ public function start(callable $callback = null, array $env = []) $this->hasCallback = null !== $callback; $descriptors = $this->getDescriptors(); + if ($this->env) { + $env += $this->env; + } + + $env += $this->getDefaultEnv(); + if (\is_array($commandline = $this->commandline)) { $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); @@ -298,13 +304,10 @@ public function start(callable $callback = null, array $env = []) // exec is mandatory to deal with sending a signal to the process $commandline = 'exec '.$commandline; } + } else { + $commandline = $this->replacePlaceholders($commandline, $env); } - if ($this->env) { - $env += $this->env; - } - $env += $this->getDefaultEnv(); - $options = ['suppress_errors' => true]; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -1632,6 +1635,17 @@ private function escapeArgument(?string $argument): string return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; } + private function replacePlaceholders(string $commandline, array $env) + { + return preg_replace_callback('/"\$([_a-zA-Z]++[_a-zA-Z0-9]*+)"/', function ($matches) use ($commandline, $env) { + if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { + throw new InvalidArgumentException(sprintf('Command line is missing a value for key %s: %s.', $matches[0], $commandline)); + } + + return '\\' === \DIRECTORY_SEPARATOR ? $this->escapeArgument($env[$matches[1]]) : $matches[0]; + }, $commandline); + } + private function getDefaultEnv() { $env = []; diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 8ae8d4ca94f0..1ee9b3c43dcb 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1500,6 +1500,50 @@ public function provideEscapeArgument() yield [1.1]; } + public function testPreparedCommand() + { + $p = Process::fromShellCommandline('echo "$abc"DEF'); + $p->run(null, ['abc' => 'ABC']); + + $this->assertSame('ABCDEF', rtrim($p->getOutput())); + } + + public function testPreparedCommandMulti() + { + $p = Process::fromShellCommandline('echo "$abc""$def"'); + $p->run(null, ['abc' => 'ABC', 'def' => 'DEF']); + + $this->assertSame('ABCDEF', rtrim($p->getOutput())); + } + + public function testPreparedCommandWithQuoteInIt() + { + $p = Process::fromShellCommandline('php -r "$code" "$def"'); + $p->run(null, ['code' => 'echo $argv[1];', 'def' => '"DEF"']); + + $this->assertSame('"DEF"', rtrim($p->getOutput())); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Command line is missing a value for key "$abc": echo "$abc". + */ + public function testPreparedCommandWithMissingValue() + { + $p = Process::fromShellCommandline('echo "$abc"'); + $p->run(null, ['bcd' => 'BCD']); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Command line is missing a value for key "$abc": echo "$abc". + */ + public function testPreparedCommandWithNoValues() + { + $p = Process::fromShellCommandline('echo "$abc"'); + $p->run(null, []); + } + public function testEnvArgument() { $env = ['FOO' => 'Foo', 'BAR' => 'Bar'];