8000 [WIP][Process] Add Process\Command by romainneutron · Pull Request #11972 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[WIP][Process] Add Process\Command #11972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions src/Symfony/Component/Process/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Process;

/**
* @author Romain Neutron <imprec@gmail.com>
*/
class Command
{
private $parts = array();
private $appended = array();
private $piped;
private $redirects = array();

public function __construct($parts = null, $escape = true)
{
if (null !== $parts) {
$this->add($parts, $escape);
}
}

public function add($parts, $escape = true, $prepend = false)
{
if (!is_array($parts)) {
$parts = array($parts);
}

if ($prepend) {
$this->parts = array_merge(($escape ? array_map(array($this, 'escape'), $parts) : $parts), $this->parts);
} else {
$this->parts = array_merge($this->parts, $escape ? array_map(array($this, 'escape'), $parts) : $parts);
}

return $this;
}

public function append(Command $command)
{
$this->appended[] = $command;

return $this;
}

public function pipe(Command $command)
{
$this->piped = $command;

return $command;
}

public function redirect($fd, $target = null, $append = false)
{
$this->redirects[$fd] = array('target' => $target, 'append' => $append);

return $this;
}

public function prepareForexecution()
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
return 'cmd /V:ON /E:ON /C "('.$this.')"';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO call to explicit method $this->__toString() will be more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8000

I prefer to keep the __toString method to keep the commandline, and use prepareForExecution to wrap this commandline with OS dependant parameters

}

return (string) $this;
}

public function __toString()
{
$command = implode(' ', $this->parts).(count($this->appended) > 0 ? '; ' : ' ').implode('; ', $this->appended);

if ($this->hasRedirects()) {
$command = '('.$command.$this->getRedirects().')';
}

$command .= ($this->piped ? '| '.$this->piped : '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if piped should be applied to the original command or the last appended ?
For instance, in the following code, should wc applied to ls or exit?

$c = new Command('ls -al');
$c->pipped(new Command('wc -l'))
  ->append(new Command('exit $code', false));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, appended commands should be appended to the line after piped and redirects.
Because, if y want to execute cmd1 ; cmd2 | cmd3 I can use

new Command('cmd1')
  ->append(new Command('cmd2')->pipe(new Command('cmd3')));

But if I want execute cmd1 | cmd3; cmd2 and run

new Command('cmd1')
  ->append(new Command('cmd2'))
  ->pipe(new Command('cmd3'));

It will not works

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for your feedback @jeremy-derusse, this is indeed something we need to pay attention, I'm gonna work on this during the week, let's take some time to see what are the best options for us


return trim($command);
}

public static function fromString($commandline)
{
$command = new self();

$command->add($commandline, false);

return $command;
}

private function hasRedirects()
{
return 0 < count($this->redirects);
}

private function getRedirects()
{
$redirects = '';

foreach ($this->redirects as $fd => $props) {
if (null === $props['target']) {
$props['target'] = defined('PHP_WINDOWS_VERSION_BUILD') ? 'NUL' : '/dev/null';
}
$redirects .= ' '.$fd.'>'.($props['append'] ? '>' : '').self::escape($props['target']);
}

return $redirects ? $redirects : '';
}

/**
* Escapes a string to be used as a shell argument.
*
* @param string $argument The argument that will be escaped
*
* @return string The escaped argument
*
* @internal Method is a static public to provide BC to ProcessUtils until Symfony 3.0
* This method will be a private non-static as of Symfony 3.0
*/
public static function escape($argument)
{
//Fix for PHP bug #43784 escapeshellarg removes % from given string
//Fix for PHP bug #49446 escapeshellarg doesn't work on Windows
//@see https://bugs.php.net/bug.php?id=43784
//@see https://bugs.php.net/bug.php?id=49446
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
if ('' === $argument) {
return escapeshellarg($argument);
}

$escapedArgument = '';
$quote = false;
foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
if ('"' === $part) {
$escapedArgument .= '\\"';
} elseif (self::isSurroundedBy($part, '%')) {
// Avoid environment variable expansion
$escapedArgument .= '^%"'.substr($part, 1, -1).'"^%';
} else {
// escape trailing backslash
if ('\\' === substr($part, -1)) {
$part .= '\\';
}
$quote = true;
$escapedArgument .= $part;
}
}
if ($quote) {
$escapedArgument = '"'.$escapedArgument.'"';
}

return $escapedArgument;
}

return escapeshellarg($argument);
}

private static function isSurroundedBy($arg, $char)
{
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
}
}
6 changes: 3 additions & 3 deletions src/Symfony/Component/Process/PhpProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,19 @@ public function __construct($script, $cwd = null, array $env = array(), $timeout
*/
public function setPhpBinary($php)
{
$this->setCommandLine($php);
$this->setCommand(Command::fromString($php));
}

/**
* {@inheritdoc}
*/
public function start($callback = null)
{
if (null === $this->getCommandLine()) {
if ('' === (string) $this->getCommand()) {
if (false === $php = $this->executableFinder->find()) {
throw new RuntimeException('Unable to find the PHP executable.');
}
$this->setCommandLine($php);
$this->setCommand(Command::fromString($php));
}

parent::start($callback);
Expand Down
61 changes: 46 additions & 15 deletions src/Symfony/Component/Process/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public function __construct($commandline, $cwd = null, array $env = null, $input
throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
}

if (!$commandline instanceof Command) {
$commandline = Command::fromString($commandline);
}

$this->commandline = $commandline;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should rename $this->commandLine to $this->command to have a more logical name given that it stores a command

$this->cwd = $cwd;

Expand Down Expand Up @@ -274,21 +278,29 @@ public function start($callback = null)
$this->callback = $this->buildCallback($callback);
$descriptors = $this->getDescriptors();

$commandline = $this->commandline;
$commandline = clone $this->commandline;

if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
$descriptors = array_merge($descriptors, array(array('pipe', 'w')));

$commandline
->redirect(3, '/dev/null')
->append(new Command('code=$?', false))
->append(new Command('echo $code >&3', false))
->append(new Command('exit $code', false));
}

if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) {
$commandline = 'cmd /V:ON /E:ON /C "('.$commandline.')';
foreach ($this->processPipes->getFiles() as $offset => $filename) {
$commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
$commandline->redirect($offset, $filename);
}
$commandline .= '"';

if (!isset($this->options['bypass_shell'])) {
$this->options['bypass_shell'] = true;
}
}

$this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
$this->process = proc_open($commandline->prepareForexecution(), $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);

if (!is_resource($this->process)) {
throw new RuntimeException('Unable to launch a new process.');
Expand Down Expand Up @@ -840,14 +852,38 @@ public function addErrorOutput($line)
$this->stderr .= $line;
}

/**
* Gets the command to be executed.
*
* @return Command The command to execute
*/
public function getCommand()
{
return $this->commandline;
}

/**
* Gets the command to be executed.
*
* @param Command $command The command to execute
*
* @return Command The command to execute
*/
public function setCommand(Command $command)
{
return $this->commandline = $command;
}

/**
* Gets the command line to be executed.
*
* @return string The command to execute
*
* @deprecated Deprecated since Symfony 2.6 in favor of setCommand, to be removed in Symfony 3.0
*/
public function getCommandLine()
{
return $this->commandline;
return (string) $this->commandline;
}

/**
Expand All @@ -856,10 +892,12 @@ public function getCommandLine()
* @param string $commandline The command to execute
*
* @return self The current Process instance
*
* @deprecated Deprecated since Symfony 2.6 in favor of setCommand, to be removed in Symfony 3.0
*/
public function setCommandLine($commandline)
{
$this->commandline = $commandline;
$this->commandline = Command::fromString($commandline);

return $this;
}
Expand Down Expand Up @@ -1256,13 +1294,6 @@ private function getDescriptors()
}
$descriptors = $this->processPipes->getDescriptors($this->outputDisabled);

if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
$descriptors = array_merge($descriptors, array(array('pipe', 'w')));

$this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code';
}

return $descriptors;
}

Expand Down
45 changes: 6 additions & 39 deletions src/Symfony/Component/Process/ProcessUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
* This class contains static methods only and is not meant to be instantiated.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*
* @internal
* @deprecated Deprecated as of Symfony 2.6, to be removed in symfony 3.0
*/
class ProcessUtils
{
Expand All @@ -35,43 +38,12 @@ private function __construct()
* @param string $argument The argument that will be escaped
*
* @return string The escaped argument
*
* @deprecated Deprecated as of Symfony 2.6, to be removed in symfony 3.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 for deprecating it. Having the fixed escaping is useful for people not using the ProcessBuilder to build the process, for instance Composer: composer/composer#3297

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deprecated it here because I moved it in the Command class. Do you mean it should be left open for external usage?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, yes (and then, Command can use it as well).

Btw, the ProcessBuilder still uses it too

*/
public static function escapeArgument($argument)
{
//Fix for PHP bug #43784 escapeshellarg removes % from given string
//Fix for PHP bug #49446 escapeshellarg doesn't work on Windows
//@see https://bugs.php.net/bug.php?id=43784
//@see https://bugs.php.net/bug.php?id=49446
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
if ('' === $argument) {
return escapeshellarg($argument);
}

$escapedArgument = '';
$quote = false;
foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
if ('"' === $part) {
$escapedArgument .= '\\"';
} elseif (self::isSurroundedBy($part, '%')) {
// Avoid environment variable expansion
$escapedArgument .= '^%"'.substr($part, 1, -1).'"^%';
} else {
// escape trailing backslash
if ('\\' === substr($part, -1)) {
$part .= '\\';
}
$quote = true;
$escapedArgument .= $part;
}
}
if ($quote) {
$escapedArgument = '"'.$escapedArgument.'"';
}

return $escapedArgument;
}

return escapeshellarg($argument);
return Command::escape($argument);
}

/**
Expand Down Expand Up @@ -103,9 +75,4 @@ public static function validateInput($caller, $input)

return $input;
}

private static function isSurroundedBy($arg, $char)
{
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
}
}
Loading
0