-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.')"'; | ||
} | ||
|
||
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 : ''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ? $c = new Command('ls -al');
$c->pipped(new Command('wc -l'))
->append(new Command('exit $code', false)); There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. new Command('cmd1')
->append(new Command('cmd2')->pipe(new Command('cmd3'))); But if I want execute new Command('cmd1')
->append(new Command('cmd2'))
->pipe(new Command('cmd3')); It will not works There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should rename |
||
$this->cwd = $cwd; | ||
|
||
|
@@ -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.'); | ||
|
@@ -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; | ||
} | ||
|
||
/** | ||
|
@@ -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; | ||
} | ||
|
@@ -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; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
{ | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I deprecated it here because I moved it in the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
|
||
/** | ||
|
@@ -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]; | ||
} | ||
} |
There was a problem hiding this comment.
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 readableThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8000I prefer to keep the __toString method to keep the commandline, and use
prepareForExecution
to wrap this commandline with OS dependant parameters