diff --git a/README.md b/README.md index 41c6c58..b74ae3b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,14 @@ or } ``` +By default, all arguments are escaped using +[escapeshellarg](https://secure.php.net/manual/en/function.escapeshellarg.php). +If you need to pass unescaped arguments, use `{!name!}`, like so: + +```php +Command::exec('echo {!path!}', ['path' => '$PATH']); +``` + ## Testing ``` bash diff --git a/src/Command.php b/src/Command.php index 6e2515f..6c76296 100644 --- a/src/Command.php +++ b/src/Command.php @@ -13,22 +13,27 @@ private function __construct() /** * Execute command with params. * - * @param string $commandLine + * @param string $command * @param array $params * * @return bool|string * * @throws \Exception */ - public static function exec($commandLine, array $params = array()) + public static function exec($command, array $params = array(), $mergeStdErr=true) { - if (empty($commandLine)) { - throw new \Exception('Command line is empty'); + if (empty($command)) { + throw new \InvalidArgumentException('Command line is empty'); } - $commandLine = self::bindParams($commandLine, $params); + $command = self::bindParams($command, $params); - exec($commandLine, $output, $code); + if ($mergeStdErr) { + // Redirect stderr to stdout to include it in $output + $command .= ' 2>&1'; + } + + exec($command, $output, $code); if (count($output) === 0) { $output = $code; @@ -37,7 +42,7 @@ public static function exec($commandLine, array $params = array()) } if ($code !== 0) { - throw new \Exception($output . ' Command line: ' . $commandLine); + throw new CommandException($command, $output, $code); } return $output; @@ -46,38 +51,26 @@ public static function exec($commandLine, array $params = array()) /** * Bind params to command. * - * @param string $commandLine + * @param string $command * @param array $params * * @return string */ - public static function bindParams($commandLine, array $params) + public static function bindParams($command, array $params) { - if (count($params) > 0) { - $wrapper = function ($string) { - return '{' . $string . '}'; - }; - $converter = function ($var) { - if (is_array($var)) { - $var = implode(' ', $var); - } - - return $var; - }; + $wrappers = array(); + $converters = array(); + foreach ($params as $key => $value) { + + // Escaped + $wrappers[] = '{' . $key . '}'; + $converters[] = escapeshellarg(is_array($value) ? implode(' ', $value) : $value); - $commandLine = str_replace( - array_map( - $wrapper, - array_keys($params) - ), - array_map( - $converter, - array_values($params) - ), - $commandLine - ); + // Unescaped + $wrappers[] = '{!' . $key . '!}'; + $converters[] = is_array($value) ? implode(' ', $value) : $value; } - return $commandLine; + return str_replace($wrappers, $converters, $command); } } diff --git a/src/CommandException.php b/src/CommandException.php new file mode 100644 index 0000000..370cbaf --- /dev/null +++ b/src/CommandException.php @@ -0,0 +1,39 @@ +command = $command; + $this->output = $output; + $this->returnCode = $returnCode; + + if ($this->returnCode == 127) { + $message = 'Command not found: "' . $this->getCommand() . '"'; + } else { + $message = 'Command "' . $this->getCommand() . '" exited with code ' . $this->getReturnCode() . ': ' . $this->getOutput(); + } + + parent::__construct($message); + } + + public function getCommand() + { + return $this->command; + } + + public function getOutput() + { + return $this->output; + } + + public function getReturnCode() + { + return $this->returnCode; + } +} diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 0ca5a1a..3ca78da 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -31,7 +31,7 @@ public function testExec() */ public function testExecException() { - $this->setExpectedException('Exception'); + $this->setExpectedException('pastuhov\Command\CommandException'); $output = Command::exec( 'echo111' @@ -43,10 +43,40 @@ public function testExecException() */ public function testExecEmptyCommand() { - $this->setExpectedException('Exception'); + $this->setExpectedException('InvalidArgumentException'); $output = Command::exec( '' ); } + + /** + * Test that arguments are escaped by default + */ + public function testArgumentsEscapedByDefault() + { + $output = Command::exec( + 'echo {phrase}', + [ + 'phrase' => 'hello $PATH', + ] + ); + + $this->assertEquals('hello $PATH', $output); + } + + /** + * Test that unescaped arguments can be passed + */ + public function testUnescapedArguments() + { + $output = Command::exec( + 'echo {!phrase!}', + [ + 'phrase' => 'hello $PATH', + ] + ); + + $this->assertRegexp('/\//', $output); + } }