]> 91.132.146.200 Git - insipid.git/commitdiff
updated changelog and new version of shellcommand
authorBanana <mail@bananas-playground.net>
Mon, 11 Sep 2023 09:31:01 +0000 (11:31 +0200)
committerBanana <mail@bananas-playground.net>
Mon, 11 Sep 2023 09:31:01 +0000 (11:31 +0200)
ChangeLog
webroot/lib/shellcommand.class.php

index 5f488eeb7709c03c6c6b5a252048fa58c2068b46..56643e56268f298a1f195dc6bb5844b04b12c60a 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,11 @@
 version 2.x - Dragon Chapel ()
 
        + Add top 5 tag or category relation stats in the tag and category overview
+       + Update license to GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
+       + Updated error logging and try/catch for mysql
+       + Updated from https://github.com/druidfi/mysqldump-php to https://github.com/ifsnop/mysqldump-php since develop
+         resumed. Version mysqldump-php v2.11
+       + Updated https://github.com/mikehaertl/php-shellcommand to 1.7.0
 
 version 2.8.1 - Deathwind Chapel (2022-12-31)
 
index d9a4e03da4f2b101e7a442ac65887cfa0b86c0d3..963ced9689ebc6fde2c252395f7ea672d289e82b 100644 (file)
@@ -26,537 +26,543 @@ namespace mikehaertl\shellcommand;
  */
 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 bool|null whether to set the stdin/stdout/stderr streams to
-        * non-blocking mode when `proc_open()` is used. This allows to have huge
-        * inputs/outputs without making the process hang. The default is `null`
-        * which will enable the feature on Non-Windows systems. Set it to `true`
-        * or `false` to manually enable/disable it. It does not work on Windows.
-        */
-       public $nonBlockingMode;
-
-       /**
-        * @var int the time in seconds after which a command should be terminated.
-        * This only works in non-blocking mode. Default is `null` which means the
-        * process is never terminated.
-        */
-       public $timeout;
-
-       /**
-        * @var null|string the locale to temporarily set before calling
-        * `escapeshellargs()`. Default is `null` for none.
-        */
-       public $locale;
-
-       /**
-        * @var null|string|resource to pipe to standard input
-        */
-       protected $_stdIn;
-
-       /**
-        * @var string the command to execute
-        */
-       protected $_command;
-
-       /**
-        * @var array the list of command arguments
-        */
-       protected $_args = array();
-
-       /**
-        * @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 (i.e. public
-        * properties) that should be applied to this object. You can also pass
-        * options that use a setter, e.g. you can pass a `fileName` option which
-        * will be passed to `setFileName()`.
-        * @throws \Exception on unknown option keys
-        * @return static 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 with `escapeshellcmd()`.
-        * @return static for method chaining
-        */
-       public function setCommand($command)
-       {
-               if ($this->escapeCommand) {
-                       $command = escapeshellcmd($command);
-               }
-               if ($this->getIsWindows()) {
-                       // Make sure to switch to correct drive like "E:" first if we have
-                       // a full path in command
-                       if (isset($command[1]) && $command[1] === ':') {
-                               $position = 1;
-                               // Could be a quoted absolute path because of spaces.
-                               // i.e. "C:\Program Files (x86)\file.exe"
-                       } elseif (isset($command[2]) && $command[2] === ':') {
-                               $position = 2;
-                       } else {
-                               $position = false;
-                       }
-
-                       // Absolute path. If it's a relative path, let it slide.
-                       if ($position) {
-                               $command = sprintf(
-                                       $command[$position - 1] . ': && cd %s && %s',
-                                       escapeshellarg(dirname($command)),
-                                       escapeshellarg(basename($command))
-                               );
-                       }
-               }
-               $this->_command = $command;
-               return $this;
-       }
-
-       /**
-        * @param string|resource $stdIn If set, the string will be piped to the
-        * command via standard input. This enables the same functionality as
-        * piping on the command line. It can also be a resource like a file
-        * handle or a stream in which case its content will be piped into the
-        * command like an input redirection.
-        * @return static for method chaining
-        */
-       public function setStdIn($stdIn) {
-               $this->_stdIn = $stdIn;
-               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()
-       {
-               $command = $this->getCommand();
-               if (!$command) {
-                       $this->_error = 'Could not locate any executable command';
-                       return false;
-               }
-
-               $args = $this->getArgs();
-               return $args ? $command.' '.$args : $command;
-       }
-
-       /**
-        * @param string $args the command arguments as string like `'--arg1=value1
-        * --arg2=value2'`. Note that this string will not get escaped. This will
-        * overwrite the args added with `addArgs()`.
-        * @return static for method chaining
-        */
-       public function setArgs($args)
-       {
-               $this->_args = array($args);
-               return $this;
-       }
-
-       /**
-        * @return string the command args that where set with `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 `=`, the (optional) $value will
-        * be separated by a space. The key will get escaped if `$escapeArgs` 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 of keys and values
-        * @return static for method chaining
-        */
-       public function addArg($key, $value = null, $escape = null)
-       {
-               $doEscape = $escape !== null ? $escape : $this->escapeArgs;
-               $useLocale = $doEscape && $this->locale !== null;
-
-               if ($useLocale) {
-                       $locale = setlocale(LC_CTYPE, 0);   // Returns current locale setting
-                       setlocale(LC_CTYPE, $this->locale);
-               }
-               if ($value === null) {
-                       $this->_args[] = $doEscape ? escapeshellarg($key) : $key;
-               } else {
-                       if (substr($key, -1) === '=') {
-                               $separator = '=';
-                               $argKey = substr($key, 0, -1);
-                       } else {
-                               $separator = ' ';
-                               $argKey = $key;
-                       }
-                       $argKey = $doEscape ? escapeshellarg($argKey) : $argKey;
-
-                       if (is_array($value)) {
-                               $params = array();
-                               foreach ($value as $v) {
-                                       $params[] = $doEscape ? escapeshellarg($v) : $v;
-                               }
-                               $this->_args[] = $argKey . $separator . implode(' ', $params);
-                       } else {
-                               $this->_args[] = $argKey . $separator .
-                                       ($doEscape ? escapeshellarg($value) : $value);
-                       }
-               }
-               if ($useLocale) {
-                       setlocale(LC_CTYPE, $locale);
-               }
-
-               return $this;
-       }
-
-       /**
-        * @param bool $trim whether to `trim()` the return value. The default is `true`.
-        * @return string the command output (stdout). Empty if none.
-        */
-       public function getOutput($trim = true)
-       {
-               return $trim ? trim($this->_stdOut) : $this->_stdOut;
-       }
-
-       /**
-        * @param bool $trim whether to `trim()` the return value. The default is `true`.
-        * @return string the error message, either stderr or an internal message.
-        * Empty string if none.
-        */
-       public function getError($trim = true)
-       {
-               return $trim ? trim($this->_error) : $this->_error;
-       }
-
-       /**
-        * @param bool $trim whether to `trim()` the return value. The default is `true`.
-        * @return string the stderr output. Empty if none.
-        */
-       public function getStdErr($trim = true)
-       {
-               return $trim ? trim($this->_stdErr) : $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 from `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 = implode("\n", $output);
-                       if ($this->_exitCode !== 0) {
-                               $this->_stdErr = $this->_stdOut;
-                               $this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr;
-                               return false;
-                       }
-               } else {
-                       $isInputStream = $this->_stdIn !== null &&
-                               is_resource($this->_stdIn) &&
-                               in_array(get_resource_type($this->_stdIn), array('file', 'stream'));
-                       $isInputString = is_string($this->_stdIn);
-                       $hasInput = $isInputStream || $isInputString;
-                       $hasTimeout = $this->timeout !== null && $this->timeout > 0;
-
-                       $descriptors = array(
-                               1   => array('pipe','w'),
-                               2   => array('pipe', $this->getIsWindows() ? 'a' : 'w'),
-                       );
-                       if ($hasInput) {
-                               $descriptors[0] = array('pipe', 'r');
-                       }
-
-
-                       // Issue #20 Set non-blocking mode to fix hanging processes
-                       $nonBlocking = $this->nonBlockingMode === null ?
-                               !$this->getIsWindows() : $this->nonBlockingMode;
-
-                       $startTime = $hasTimeout ? time() : 0;
-                       $process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions);
-
-                       if (is_resource($process)) {
-
-                               if ($nonBlocking) {
-                                       stream_set_blocking($pipes[1], false);
-                                       stream_set_blocking($pipes[2], false);
-                                       if ($hasInput) {
-                                               $writtenBytes = 0;
-                                               $isInputOpen = true;
-                                               stream_set_blocking($pipes[0], false);
-                                               if ($isInputStream) {
-                                                       stream_set_blocking($this->_stdIn, false);
-                                               }
-                                       }
-
-                                       // Due to the non-blocking streams we now have to check in
-                                       // a loop if the process is still running. We also need to
-                                       // ensure that all the pipes are written/read alternately
-                                       // until there's nothing left to write/read.
-                                       $isRunning = true;
-                                       while ($isRunning) {
-                                               $status = proc_get_status($process);
-                                               $isRunning = $status['running'];
-
-                                               // We first write to stdIn if we have an input. For big
-                                               // inputs it will only write until the input buffer of
-                                               // the command is full (the command may now wait that
-                                               // we read the output buffers - see below). So we may
-                                               // have to continue writing in another cycle.
-                                               //
-                                               // After everything is written it's safe to close the
-                                               // input pipe.
-                                               if ($isRunning && $hasInput && $isInputOpen) {
-                                                       if ($isInputStream) {
-                                                               $written = stream_copy_to_stream($this->_stdIn, $pipes[0], 16 * 1024, $writtenBytes);
-                                                               if ($written === false || $written === 0) {
-                                                                       $isInputOpen = false;
-                                                                       fclose($pipes[0]);
-                                                               } else {
-                                                                       $writtenBytes += $written;
-                                                               }
-                                                       } else {
-                                                               if ($writtenBytes < strlen($this->_stdIn)) {
-                                                                       $writtenBytes += fwrite($pipes[0], substr($this->_stdIn, $writtenBytes));
-                                                               } else {
-                                                                       $isInputOpen = false;
-                                                                       fclose($pipes[0]);
-                                                               }
-                                                       }
-                                               }
-
-                                               // Read out the output buffers because if they are full
-                                               // the command may block execution. We do this even if
-                                               // $isRunning is `false`, because there could be output
-                                               // left in the buffers.
-                                               //
-                                               // The latter is only an assumption and needs to be
-                                               // verified - but it does not hurt either and works as
-                                               // expected.
-                                               //
-                                               while (($out = fgets($pipes[1])) !== false) {
-                                                       $this->_stdOut .= $out;
-                                               }
-                                               while (($err = fgets($pipes[2])) !== false) {
-                                                       $this->_stdErr .= $err;
-                                               }
-
-                                               $runTime = $hasTimeout ? time() - $startTime : 0;
-                                               if ($isRunning && $hasTimeout && $runTime >= $this->timeout) {
-                                                       // Only send a SIGTERM and handle status in the next cycle
-                                                       proc_terminate($process);
-                                               }
-
-                                               if (!$isRunning) {
-                                                       $this->_exitCode = $status['exitcode'];
-                                                       if ($this->_exitCode !== 0 && empty($this->_stdErr)) {
-                                                               if ($status['stopped']) {
-                                                                       $signal = $status['stopsig'];
-                                                                       $this->_stdErr = "Command stopped by signal $signal";
-                                                               } elseif ($status['signaled']) {
-                                                                       $signal = $status['termsig'];
-                                                                       $this->_stdErr = "Command terminated by signal $signal";
-                                                               } else {
-                                                                       $this->_stdErr = 'Command unexpectedly terminated without error message';
-                                                               }
-                                                       }
-                                                       fclose($pipes[1]);
-                                                       fclose($pipes[2]);
-                                                       proc_close($process);
-                                               } else {
-                                                       // The command is still running. Let's wait some
-                                                       // time before we start the next cycle.
-                                                       usleep(10000);
-                                               }
-                                       }
-                               } else {
-                                       if ($hasInput) {
-                                               if ($isInputStream) {
-                                                       stream_copy_to_stream($this->_stdIn, $pipes[0]);
-                                               } elseif ($isInputString) {
-                                                       fwrite($pipes[0], $this->_stdIn);
-                                               }
-                                               fclose($pipes[0]);
-                                       }
-                                       $this->_stdOut = stream_get_contents($pipes[1]);
-                                       $this->_stdErr = 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 (Exit code: {$this->_exitCode})";
-                                       return false;
-                               }
-                       } else {
-                               $this->_error = "Could not run command $command";
-                               return false;
-                       }
-               }
-
-               $this->_executed = true;
-
-               return true;
-       }
-
-       /**
-        * @return bool whether we are on a Windows OS
-        */
-       public function getIsWindows()
-       {
-               return strncasecmp(PHP_OS, 'WIN', 3)===0;
-       }
-
-       /**
-        * @return string the current command string to execute
-        */
-       public function __toString()
-       {
-               return (string) $this->getExecCommand();
-       }
+    /**
+     * @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 bool|null whether to set the stdin/stdout/stderr streams to
+     * non-blocking mode when `proc_open()` is used. This allows to have huge
+     * inputs/outputs without making the process hang. The default is `null`
+     * which will enable the feature on Non-Windows systems. Set it to `true`
+     * or `false` to manually enable/disable it. It does not work on Windows.
+     */
+    public $nonBlockingMode;
+
+    /**
+     * @var int the time in seconds after which a command should be terminated.
+     * This only works in non-blocking mode. Default is `null` which means the
+     * process is never terminated.
+     */
+    public $timeout;
+
+    /**
+     * @var null|string the locale to temporarily set before calling
+     * `escapeshellargs()`. Default is `null` for none.
+     */
+    public $locale;
+
+    /**
+     * @var null|string|resource to pipe to standard input
+     */
+    protected $_stdIn;
+
+    /**
+     * @var string the command to execute
+     */
+    protected $_command;
+
+    /**
+     * @var array the list of command arguments
+     */
+    protected $_args = array();
+
+    /**
+     * @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 (i.e. public
+     * properties) that should be applied to this object. You can also pass
+     * options that use a setter, e.g. you can pass a `fileName` option which
+     * will be passed to `setFileName()`.
+     * @throws \Exception on unknown option keys
+     * @return static 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 with `escapeshellcmd()`.
+     * @return static for method chaining
+     */
+    public function setCommand($command)
+    {
+        if ($this->escapeCommand) {
+            $command = escapeshellcmd($command);
+        }
+        if ($this->getIsWindows()) {
+            // Make sure to switch to correct drive like "E:" first if we have
+            // a full path in command
+            if (isset($command[1]) && $command[1] === ':') {
+                $position = 1;
+                // Could be a quoted absolute path because of spaces.
+                // i.e. "C:\Program Files (x86)\file.exe"
+            } elseif (isset($command[2]) && $command[2] === ':') {
+                $position = 2;
+            } else {
+                $position = false;
+            }
+
+            // Absolute path. If it's a relative path, let it slide.
+            if ($position) {
+                $command = sprintf(
+                    $command[$position - 1] . ': && cd %s && %s',
+                    escapeshellarg(dirname($command)),
+                    escapeshellarg(basename($command))
+                );
+            }
+        }
+        $this->_command = $command;
+        return $this;
+    }
+
+    /**
+     * @param string|resource $stdIn If set, the string will be piped to the
+     * command via standard input. This enables the same functionality as
+     * piping on the command line. It can also be a resource like a file
+     * handle or a stream in which case its content will be piped into the
+     * command like an input redirection.
+     * @return static for method chaining
+     */
+    public function setStdIn($stdIn) {
+        $this->_stdIn = $stdIn;
+        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()
+    {
+        $command = $this->getCommand();
+        if (!$command) {
+            $this->_error = 'Could not locate any executable command';
+            return false;
+        }
+
+        $args = $this->getArgs();
+        return $args ? $command.' '.$args : $command;
+    }
+
+    /**
+     * @param string $args the command arguments as string like `'--arg1=value1
+     * --arg2=value2'`. Note that this string will not get escaped. This will
+     * overwrite the args added with `addArgs()`.
+     * @return static for method chaining
+     */
+    public function setArgs($args)
+    {
+        $this->_args = array($args);
+        return $this;
+    }
+
+    /**
+     * @return string the command args that where set with `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 `=`, the (optional) $value will
+     * be separated by a space. The key will get escaped if `$escapeArgs` 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 of keys and values
+     * @return static for method chaining
+     */
+    public function addArg($key, $value = null, $escape = null)
+    {
+        $doEscape = $escape !== null ? $escape : $this->escapeArgs;
+        $useLocale = $doEscape && $this->locale !== null;
+
+        if ($useLocale) {
+            $locale = setlocale(LC_CTYPE, 0);   // Returns current locale setting
+            setlocale(LC_CTYPE, $this->locale);
+        }
+        if ($value === null) {
+            $this->_args[] = $doEscape ? escapeshellarg($key) : $key;
+        } else {
+            if (substr($key, -1) === '=') {
+                $separator = '=';
+                $argKey = substr($key, 0, -1);
+            } else {
+                $separator = ' ';
+                $argKey = $key;
+            }
+            $argKey = $doEscape ? escapeshellarg($argKey) : $argKey;
+
+            if (is_array($value)) {
+                $params = array();
+                foreach ($value as $v) {
+                    $params[] = $doEscape ? escapeshellarg($v) : $v;
+                }
+                $this->_args[] = $argKey . $separator . implode(' ', $params);
+            } else {
+                $this->_args[] = $argKey . $separator .
+                    ($doEscape ? escapeshellarg($value) : $value);
+            }
+        }
+        if ($useLocale) {
+            setlocale(LC_CTYPE, $locale);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param bool $trim whether to `trim()` the return value. The default is `true`.
+     * @param string $characters the list of characters to trim. The default
+     * is ` \t\n\r\0\v\f`.
+     * @return string the command output (stdout). Empty if none.
+     */
+    public function getOutput($trim = true, $characters = " \t\n\r\0\v\f")
+    {
+        return $trim ? trim($this->_stdOut, $characters) : $this->_stdOut;
+    }
+
+    /**
+     * @param bool $trim whether to `trim()` the return value. The default is `true`.
+     * @param string $characters the list of characters to trim. The default
+     * is ` \t\n\r\0\v\f`.
+     * @return string the error message, either stderr or an internal message.
+     * Empty string if none.
+     */
+    public function getError($trim = true, $characters = " \t\n\r\0\v\f")
+    {
+        return $trim ? trim($this->_error, $characters) : $this->_error;
+    }
+
+    /**
+     * @param bool $trim whether to `trim()` the return value. The default is `true`.
+     * @param string $characters the list of characters to trim. The default
+     * is ` \t\n\r\0\v\f`.
+     * @return string the stderr output. Empty if none.
+     */
+    public function getStdErr($trim = true, $characters = " \t\n\r\0\v\f")
+    {
+        return $trim ? trim($this->_stdErr, $characters) : $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 from `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 = implode("\n", $output);
+            if ($this->_exitCode !== 0) {
+                $this->_stdErr = $this->_stdOut;
+                $this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr;
+                return false;
+            }
+        } else {
+            $isInputStream = $this->_stdIn !== null &&
+                is_resource($this->_stdIn) &&
+                in_array(get_resource_type($this->_stdIn), array('file', 'stream'));
+            $isInputString = is_string($this->_stdIn);
+            $hasInput = $isInputStream || $isInputString;
+            $hasTimeout = $this->timeout !== null && $this->timeout > 0;
+
+            $descriptors = array(
+                1   => array('pipe','w'),
+                2   => array('pipe', $this->getIsWindows() ? 'a' : 'w'),
+            );
+            if ($hasInput) {
+                $descriptors[0] = array('pipe', 'r');
+            }
+
+
+            // Issue #20 Set non-blocking mode to fix hanging processes
+            $nonBlocking = $this->nonBlockingMode === null ?
+                !$this->getIsWindows() : $this->nonBlockingMode;
+
+            $startTime = $hasTimeout ? time() : 0;
+            $process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions);
+
+            if (is_resource($process)) {
+
+                if ($nonBlocking) {
+                    stream_set_blocking($pipes[1], false);
+                    stream_set_blocking($pipes[2], false);
+                    if ($hasInput) {
+                        $writtenBytes = 0;
+                        $isInputOpen = true;
+                        stream_set_blocking($pipes[0], false);
+                        if ($isInputStream) {
+                            stream_set_blocking($this->_stdIn, false);
+                        }
+                    }
+
+                    // Due to the non-blocking streams we now have to check in
+                    // a loop if the process is still running. We also need to
+                    // ensure that all the pipes are written/read alternately
+                    // until there's nothing left to write/read.
+                    $isRunning = true;
+                    while ($isRunning) {
+                        $status = proc_get_status($process);
+                        $isRunning = $status['running'];
+
+                        // We first write to stdIn if we have an input. For big
+                        // inputs it will only write until the input buffer of
+                        // the command is full (the command may now wait that
+                        // we read the output buffers - see below). So we may
+                        // have to continue writing in another cycle.
+                        //
+                        // After everything is written it's safe to close the
+                        // input pipe.
+                        if ($isRunning && $hasInput && $isInputOpen) {
+                            if ($isInputStream) {
+                                $written = stream_copy_to_stream($this->_stdIn, $pipes[0], 16 * 1024, $writtenBytes);
+                                if ($written === false || $written === 0) {
+                                    $isInputOpen = false;
+                                    fclose($pipes[0]);
+                                } else {
+                                    $writtenBytes += $written;
+                                }
+                            } else {
+                                if ($writtenBytes < strlen($this->_stdIn)) {
+                                    $writtenBytes += fwrite($pipes[0], substr($this->_stdIn, $writtenBytes));
+                                } else {
+                                    $isInputOpen = false;
+                                    fclose($pipes[0]);
+                                }
+                            }
+                        }
+
+                        // Read out the output buffers because if they are full
+                        // the command may block execution. We do this even if
+                        // $isRunning is `false`, because there could be output
+                        // left in the buffers.
+                        //
+                        // The latter is only an assumption and needs to be
+                        // verified - but it does not hurt either and works as
+                        // expected.
+                        //
+                        while (($out = fgets($pipes[1])) !== false) {
+                            $this->_stdOut .= $out;
+                        }
+                        while (($err = fgets($pipes[2])) !== false) {
+                            $this->_stdErr .= $err;
+                        }
+
+                        $runTime = $hasTimeout ? time() - $startTime : 0;
+                        if ($isRunning && $hasTimeout && $runTime >= $this->timeout) {
+                            // Only send a SIGTERM and handle status in the next cycle
+                            proc_terminate($process);
+                        }
+
+                        if (!$isRunning) {
+                            $this->_exitCode = $status['exitcode'];
+                            if ($this->_exitCode !== 0 && empty($this->_stdErr)) {
+                                if ($status['stopped']) {
+                                    $signal = $status['stopsig'];
+                                    $this->_stdErr = "Command stopped by signal $signal";
+                                } elseif ($status['signaled']) {
+                                    $signal = $status['termsig'];
+                                    $this->_stdErr = "Command terminated by signal $signal";
+                                } else {
+                                    $this->_stdErr = 'Command unexpectedly terminated without error message';
+                                }
+                            }
+                            fclose($pipes[1]);
+                            fclose($pipes[2]);
+                            proc_close($process);
+                        } else {
+                            // The command is still running. Let's wait some
+                            // time before we start the next cycle.
+                            usleep(10000);
+                        }
+                    }
+                } else {
+                    if ($hasInput) {
+                        if ($isInputStream) {
+                            stream_copy_to_stream($this->_stdIn, $pipes[0]);
+                        } elseif ($isInputString) {
+                            fwrite($pipes[0], $this->_stdIn);
+                        }
+                        fclose($pipes[0]);
+                    }
+                    $this->_stdOut = stream_get_contents($pipes[1]);
+                    $this->_stdErr = 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 (Exit code: {$this->_exitCode})";
+                    return false;
+                }
+            } else {
+                $this->_error = "Could not run command $command";
+                return false;
+            }
+        }
+
+        $this->_executed = true;
+
+        return true;
+    }
+
+    /**
+     * @return bool whether we are on a Windows OS
+     */
+    public function getIsWindows()
+    {
+        return strncasecmp(PHP_OS, 'WIN', 3)===0;
+    }
+
+    /**
+     * @return string the current command string to execute
+     */
+    public function __toString()
+    {
+        return (string) $this->getExecCommand();
+    }
 }