<?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();
    }
}
