dzzoffice/core/class/class_command.php
2018-03-23 14:20:16 +08:00

324 lines
9.7 KiB
PHP

<?php
/**
* Command
*
* This class represents a shell command.
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @version 1.0.5
* @license http://www.opensource.org/licenses/MIT
*/
class command
{
/**
* @var bool whether to escape any argument passed through addArg(). Default is true.
*/
public $escapeArgs = true;
/**
* @var bool whether to escape the command passed to setCommand() or the constructor.
* This is only useful if $escapeArgs is false. Default is false.
*/
public $escapeCommand = false;
/**
* @var bool whether to use `exec()` instead of `proc_open()`. This can be used on Windows system
* to workaround some quirks there. Note, that any errors from your command will be output directly
* to the PHP output stream. `getStdErr()` will also not work anymore and thus you also won't get
* the error output from `getError()` in this case. You also can't pass any environment
* variables to the command if this is enabled. Default is false.
*/
public $useExec = false;
/**
* @var bool whether to capture stderr (2>&1) when `useExec` is true. This will try to redirect the
* stderr to stdout and provide the complete output of both in `getStdErr()` and `getError()`.
* Default is `true`.
*/
public $captureStdErr = true;
/**
* @var string|null the initial working dir for proc_open(). Default is null for current PHP working dir.
*/
public $procCwd;
/**
* @var array|null an array with environment variables to pass to proc_open(). Default is null for none.
*/
public $procEnv;
/**
* @var array|null an array of other_options for proc_open(). Default is null for none.
*/
public $procOptions;
/**
* @var string the command to execute
*/
protected $_command;
/**
* @var array the list of command arguments
*/
protected $_args = array();
/**
* @var string the full command string to execute
*/
protected $_execCommand;
/**
* @var string the stdout output
*/
protected $_stdOut = '';
/**
* @var string the stderr output
*/
protected $_stdErr = '';
/**
* @var int the exit code
*/
protected $_exitCode;
/**
* @var string the error message
*/
protected $_error = '';
/**
* @var bool whether the command was successfully executed
*/
protected $_executed = false;
/**
* @param string|array $options either a command string or an options array (see setOptions())
*/
public function __construct($options = null)
{
if (is_array($options)) {
$this->setOptions($options);
} elseif (is_string($options)) {
$this->setCommand($options);
}
}
/**
* @param array $options array of name => value options that should be applied to the object
* You can also pass options that use a setter, e.g. you can pass a 'fileName' option which
* will be passed to setFileName().
* @return Command for method chaining
*/
public function setOptions($options)
{
foreach ($options as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
} else {
$method = 'set'.ucfirst($key);
if (method_exists($this, $method)) {
call_user_func(array($this,$method), $value);
} else {
throw new \Exception("Unknown configuration option '$key'");
}
}
}
return $this;
}
/**
* @param string $command the command or full command string to execute, like 'gzip' or 'gzip -d'.
* You can still call addArg() to add more arguments to the command. If $escapeCommand was set to true,
* the command gets escaped through escapeshellcmd().
* @return Command for method chaining
*/
public function setCommand($command)
{
$this->_command = $this->escapeCommand ? escapeshellcmd($command) : $command;
return $this;
}
/**
* @return string|null the command that was set through setCommand() or passed to the constructor. Null if none.
*/
public function getCommand()
{
return $this->_command;
}
/**
* @return string|bool the full command string to execute. If no command was set with setCommand()
* or passed to the constructor it will return false.
*/
public function getExecCommand()
{
if ($this->_execCommand===null) {
$command = $this->getCommand();
if (!$command) {
$this->_error = 'Could not locate any executable command';
return false;
}
$args = $this->getArgs();
$this->_execCommand = $args ? $command.' '.$args : $command;
}
return $this->_execCommand;
}
/**
* @param string $args the command arguments as string. Note that these will not get escaped!
* @return Command for method chaining
*/
public function setArgs($args)
{
$this->_args = array($args);
return $this;
}
/**
* @return string the command args that where set through setArgs() or added with addArg() separated by spaces
*/
public function getArgs()
{
return implode(' ', $this->_args);
}
/**
* @param string $key the argument key to add e.g. `--feature` or `--name=`. If the key does not end with
* and `=`, the $value will be separated by a space, if any. Keys are not escaped unless $value is null
* and $escape is `true`.
* @param string|array|null $value the optional argument value which will get escaped if $escapeArgs is true.
* An array can be passed to add more than one value for a key, e.g. `addArg('--exclude', array('val1','val2'))`
* which will create the option `--exclude 'val1' 'val2'`.
* @param bool|null $escape if set, this overrides the $escapeArgs setting and enforces escaping/no escaping
* @return Command for method chaining
*/
public function addArg($key, $value = null, $escape = null)
{
$old=setlocale(LC_ALL,0);
setlocale(LC_ALL,'us');
$doEscape = $escape!==null ? $escape : $this->escapeArgs;
if ($value===null) {
// Only escape single arguments if explicitely requested
$this->_args[] = $escape ? escapeshellarg($key) : $key;
} else {
$separator = substr($key, -1)==='=' ? '' : ' ';
if (is_array($value)) {
$params = array();
foreach ($value as $v) {
$params[] = $doEscape ? escapeshellarg($v) : $v;
}
$this->_args[] = $key.$separator.implode(' ',$params);
} else {
$this->_args[] = $key.$separator.($doEscape ? escapeshellarg($value) : $value);
}
}
//print_r($old);
setlocale(LC_ALL,$old);
return $this;
}
/**
* @return string the command output (stdout). Empty if none.
*/
public function getOutput()
{
return $this->_stdOut;
}
/**
* @return string the error message, either stderr or internal message. Empty if none.
*/
public function getError()
{
return $this->_error;
}
/**
* @return string the stderr output. Empty if none.
*/
public function getStdErr()
{
return $this->_stdErr;
}
/**
* @return int|null the exit code or null if command was not executed yet
*/
public function getExitCode()
{
return $this->_exitCode;
}
/**
* @return string whether the command was successfully executed
*/
public function getExecuted()
{
return $this->_executed;
}
/**
* Execute the command
*
* @return bool whether execution was successful. If false, error details can be obtained through
* getError(), getStdErr() and getExitCode().
*/
public function execute()
{
$command = $this->getExecCommand();
if (!$command) {
return false;
}
if ($this->useExec) {
$execCommand = $this->captureStdErr ? "$command 2>&1" : $command;
exec($execCommand, $output, $this->_exitCode);
$this->_stdOut = trim(implode("\n", $output));
if ($this->_exitCode!==0) {
$this->_stdErr = $this->_stdOut;
$this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr;
return false;
}
} else {
$descriptors = array(
1 => array('pipe','w'),
2 => array('pipe','a'),
);
$process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions);
if (is_resource($process)) {
$this->_stdOut = trim(stream_get_contents($pipes[1]));
$this->_stdErr = trim(stream_get_contents($pipes[2]));
fclose($pipes[1]);
fclose($pipes[2]);
$this->_exitCode = proc_close($process);
if ($this->_exitCode!==0) {
$this->_error = $this->_stdErr ? $this->_stdErr : "Failed without error message: $command";
return false;
}
} else {
$this->_error = "Could not run command $command";
return false;
}
}
$this->_executed = true;
return true;
}
/**
* @return string the current command string to execute
*/
public function __toString()
{
return (string)$this->getExecCommand();
}
}