diff --git a/library/think/Console.php b/library/think/Console.php
new file mode 100644
index 00000000..63ca114a
--- /dev/null
+++ b/library/think/Console.php
@@ -0,0 +1,944 @@
+
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\console\command\Build as BuildCommand;
+use think\console\command\Command;
+use think\console\command\Help as HelpCommand;
+use think\console\command\Lists as ListCommand;
+use think\console\command\make\Controller as MakeControllerCommand;
+use think\console\command\make\Model as MakeModelCommand;
+use think\console\helper\Debug as DebugFormatterHelper;
+use think\console\helper\Formatter as FormatterHelper;
+use think\console\helper\Process as ProcessHelper;
+use think\console\helper\Question as QuestionHelper;
+use think\console\helper\Set as HelperSet;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\output\Stream;
+
+class Console
+{
+
+ private $name;
+ private $version;
+
+ /** @var Command[] */
+ private $commands = [];
+
+ private $wantHelps = false;
+
+ /** @var Command */
+ private $runningCommand;
+
+ private $catchExceptions = true;
+ private $autoExit = true;
+ private $definition;
+ private $helperSet;
+ private $terminalDimensions;
+ private $defaultCommand;
+
+ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
+ {
+ $this->name = $name;
+ $this->version = $version;
+
+ $this->defaultCommand = 'list';
+ $this->helperSet = $this->getDefaultHelperSet();
+ $this->definition = $this->getDefaultInputDefinition();
+
+ foreach ($this->getDefaultCommands() as $command) {
+ $this->add($command);
+ }
+ }
+
+ /**
+ * 执行当前的指令
+ * @return int
+ * @throws \Exception
+ * @api
+ */
+ public function run()
+ {
+ $input = new Input();
+ $output = new Output();
+
+ $this->configureIO($input, $output);
+
+ try {
+ $exitCode = $this->doRun($input, $output);
+ } catch (\Exception $e) {
+ if (!$this->catchExceptions) {
+ throw $e;
+ }
+
+ $this->renderException($e, $output->getErrorOutput());
+
+ $exitCode = $e->getCode();
+ if (is_numeric($exitCode)) {
+ $exitCode = (int) $exitCode;
+ if (0 === $exitCode) {
+ $exitCode = 1;
+ }
+ } else {
+ $exitCode = 1;
+ }
+ }
+
+ if ($this->autoExit) {
+ if ($exitCode > 255) {
+ $exitCode = 255;
+ }
+
+ exit($exitCode);
+ }
+
+ return $exitCode;
+ }
+
+ /**
+ * 执行指令
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ */
+ public function doRun(Input $input, Output $output)
+ {
+ if (true === $input->hasParameterOption(['--version', '-V'])) {
+ $output->writeln($this->getLongVersion());
+
+ return 0;
+ }
+
+ $name = $this->getCommandName($input);
+
+ if (true === $input->hasParameterOption(['--help', '-h'])) {
+ if (!$name) {
+ $name = 'help';
+ $input = new Input(['help']);
+ } else {
+ $this->wantHelps = true;
+ }
+ }
+
+ if (!$name) {
+ $name = $this->defaultCommand;
+ $input = new Input([$this->defaultCommand]);
+ }
+
+ $command = $this->find($name);
+
+ $this->runningCommand = $command;
+ $exitCode = $this->doRunCommand($command, $input, $output);
+ $this->runningCommand = null;
+
+ return $exitCode;
+ }
+
+ /**
+ * 设置助手集
+ * @param HelperSet $helperSet
+ */
+ public function setHelperSet(HelperSet $helperSet)
+ {
+ $this->helperSet = $helperSet;
+ }
+
+ /**
+ * 获取助手集
+ * @return HelperSet
+ */
+ public function getHelperSet()
+ {
+ return $this->helperSet;
+ }
+
+ /**
+ * 设置输入参数定义
+ * @param InputDefinition $definition
+ */
+ public function setDefinition(InputDefinition $definition)
+ {
+ $this->definition = $definition;
+ }
+
+ /**
+ * 获取输入参数定义
+ * @return InputDefinition The InputDefinition instance
+ */
+ public function getDefinition()
+ {
+ return $this->definition;
+ }
+
+ /**
+ * Gets the help message.
+ * @return string A help message.
+ */
+ public function getHelp()
+ {
+ return $this->getLongVersion();
+ }
+
+ /**
+ * 是否捕获异常
+ * @param bool $boolean
+ * @api
+ */
+ public function setCatchExceptions($boolean)
+ {
+ $this->catchExceptions = (bool) $boolean;
+ }
+
+ /**
+ * 是否自动退出
+ * @param bool $boolean
+ * @api
+ */
+ public function setAutoExit($boolean)
+ {
+ $this->autoExit = (bool) $boolean;
+ }
+
+ /**
+ * 获取名称
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 设置名称
+ * @param string $name
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * 获取版本
+ * @return string
+ * @api
+ */
+ public function getVersion()
+ {
+ return $this->version;
+ }
+
+ /**
+ * 设置版本
+ * @param string $version
+ */
+ public function setVersion($version)
+ {
+ $this->version = $version;
+ }
+
+ /**
+ * 获取完整的版本号
+ * @return string
+ */
+ public function getLongVersion()
+ {
+ if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) {
+ return sprintf('%s version %s', $this->getName(), $this->getVersion());
+ }
+
+ return 'Console Tool';
+ }
+
+ /**
+ * 注册一个指令
+ * @param string $name
+ * @return Command
+ */
+ public function register($name)
+ {
+ return $this->add(new Command($name));
+ }
+
+ /**
+ * 添加指令
+ * @param Command[] $commands
+ */
+ public function addCommands(array $commands)
+ {
+ foreach ($commands as $command) {
+ $this->add($command);
+ }
+ }
+
+ /**
+ * 添加一个指令
+ * @param Command $command
+ * @return Command
+ */
+ public function add(Command $command)
+ {
+ $command->setConsole($this);
+
+ if (!$command->isEnabled()) {
+ $command->setConsole(null);
+ return null;
+ }
+
+ if (null === $command->getDefinition()) {
+ throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
+ }
+
+ $this->commands[$command->getName()] = $command;
+
+ foreach ($command->getAliases() as $alias) {
+ $this->commands[$alias] = $command;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 获取指令
+ * @param string $name 指令名称
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function get($name)
+ {
+ if (!isset($this->commands[$name])) {
+ throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
+ }
+
+ $command = $this->commands[$name];
+
+ if ($this->wantHelps) {
+ $this->wantHelps = false;
+
+ /** @var HelpCommand $helpCommand */
+ $helpCommand = $this->get('help');
+ $helpCommand->setCommand($command);
+
+ return $helpCommand;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 某个指令是否存在
+ * @param string $name 指令民初
+ * @return bool
+ */
+ public function has($name)
+ {
+ return isset($this->commands[$name]);
+ }
+
+ /**
+ * 获取所有的命名空间
+ * @return array
+ */
+ public function getNamespaces()
+ {
+ $namespaces = [];
+ foreach ($this->commands as $command) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
+
+ foreach ($command->getAliases() as $alias) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
+ }
+ }
+
+ return array_values(array_unique(array_filter($namespaces)));
+ }
+
+ /**
+ * 查找注册命名空间中的名称或缩写。
+ * @param string $namespace
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ public function findNamespace($namespace)
+ {
+ $allNamespaces = $this->getNamespaces();
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $namespace);
+ $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces);
+
+ if (empty($namespaces)) {
+ $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
+
+ if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new \InvalidArgumentException($message);
+ }
+
+ $exact = in_array($namespace, $namespaces, true);
+ if (count($namespaces) > 1 && !$exact) {
+ throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
+ }
+
+ return $exact ? $namespace : reset($namespaces);
+ }
+
+ /**
+ * 查找指令
+ * @param string $name 名称或者别名
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function find($name)
+ {
+ $allCommands = array_keys($this->commands);
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $name);
+ $commands = preg_grep('{^' . $expr . '}', $allCommands);
+
+ if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
+ if (false !== $pos = strrpos($name, ':')) {
+ $this->findNamespace(substr($name, 0, $pos));
+ }
+
+ $message = sprintf('Command "%s" is not defined.', $name);
+
+ if ($alternatives = $this->findAlternatives($name, $allCommands)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new \InvalidArgumentException($message);
+ }
+
+ if (count($commands) > 1) {
+ $commandList = $this->commands;
+ $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
+ $commandName = $commandList[$nameOrAlias]->getName();
+
+ return $commandName === $nameOrAlias || !in_array($commandName, $commands);
+ });
+ }
+
+ $exact = in_array($name, $commands, true);
+ if (count($commands) > 1 && !$exact) {
+ $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
+
+ throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
+ }
+
+ return $this->get($exact ? $name : reset($commands));
+ }
+
+ /**
+ * 获取所有的指令
+ * @param string $namespace 命名空间
+ * @return Command[]
+ * @api
+ */
+ public function all($namespace = null)
+ {
+ if (null === $namespace) {
+ return $this->commands;
+ }
+
+ $commands = [];
+ foreach ($this->commands as $name => $command) {
+ if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
+ $commands[$name] = $command;
+ }
+ }
+
+ return $commands;
+ }
+
+ /**
+ * 获取可能的指令名
+ * @param array $names
+ * @return array
+ */
+ public static function getAbbreviations($names)
+ {
+ $abbrevs = [];
+ foreach ($names as $name) {
+ for ($len = strlen($name); $len > 0; --$len) {
+ $abbrev = substr($name, 0, $len);
+ $abbrevs[$abbrev][] = $name;
+ }
+ }
+
+ return $abbrevs;
+ }
+
+ /**
+ * 呈现捕获的异常
+ * @param \Exception $e
+ * @param Stream $output
+ */
+ public function renderException(\Exception $e, Stream $output)
+ {
+ do {
+ $title = sprintf(' [%s] ', get_class($e));
+
+ $len = $this->stringWidth($title);
+
+ $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
+
+ if (defined('HHVM_VERSION') && $width > 1 << 31) {
+ $width = 1 << 31;
+ }
+ $formatter = $output->getFormatter();
+ $lines = [];
+ foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
+ foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
+
+ $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4;
+ $lines[] = [$line, $lineLength];
+
+ $len = max($lineLength, $len);
+ }
+ }
+
+ $messages = ['', ''];
+ $messages[] = $emptyLine = $formatter->format(sprintf('%s', str_repeat(' ', $len)));
+ $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))));
+ foreach ($lines as $line) {
+ $messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])));
+ }
+ $messages[] = $emptyLine;
+ $messages[] = '';
+ $messages[] = '';
+
+ $output->writeln($messages, Output::OUTPUT_RAW);
+
+ if (Output::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
+ $output->writeln('Exception trace:');
+
+ // exception related properties
+ $trace = $e->getTrace();
+ array_unshift($trace, [
+ 'function' => '',
+ 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a',
+ 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a',
+ 'args' => [],
+ ]);
+
+ for ($i = 0, $count = count($trace); $i < $count; ++$i) {
+ $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
+ $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
+ $function = $trace[$i]['function'];
+ $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
+ $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
+
+ $output->writeln(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line));
+ }
+
+ $output->writeln('');
+ $output->writeln('');
+ }
+ } while ($e = $e->getPrevious());
+
+ if (null !== $this->runningCommand) {
+ $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())));
+ $output->writeln('');
+ $output->writeln('');
+ }
+ }
+
+ /**
+ * 获取终端宽度
+ * @return int|null
+ */
+ protected function getTerminalWidth()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[0];
+ }
+
+ /**
+ * 获取终端高度
+ * @return int|null
+ */
+ protected function getTerminalHeight()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[1];
+ }
+
+ /**
+ * 获取当前终端的尺寸
+ * @return array
+ */
+ public function getTerminalDimensions()
+ {
+ if ($this->terminalDimensions) {
+ return $this->terminalDimensions;
+ }
+
+ if ('\\' === DS) {
+ if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ if (preg_match('/^(\d+)x(\d+)$/', $this->getConsoleMode(), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ }
+
+ if ($sttyString = $this->getSttyColumns()) {
+ if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ }
+
+ return [null, null];
+ }
+
+ /**
+ * 设置终端尺寸
+ * @param int $width
+ * @param int $height
+ * @return Console
+ */
+ public function setTerminalDimensions($width, $height)
+ {
+ $this->terminalDimensions = [$width, $height];
+
+ return $this;
+ }
+
+ /**
+ * 配置基于用户的参数和选项的输入和输出实例。
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ */
+ protected function configureIO(Input $input, Output $output)
+ {
+ if (true === $input->hasParameterOption(['--ansi'])) {
+ $output->setDecorated(true);
+ } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
+ $output->setDecorated(false);
+ }
+
+ if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
+ $input->setInteractive(false);
+ } elseif (function_exists('posix_isatty') && $this->getHelperSet()->has('question')) {
+ $inputStream = $this->getHelperSet()->get('question')->getInputStream();
+ if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) {
+ $input->setInteractive(false);
+ }
+ }
+
+ if (true === $input->hasParameterOption(['--quiet', '-q'])) {
+ $output->setVerbosity(Output::VERBOSITY_QUIET);
+ } else {
+ if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3')
+ || $input->getParameterOption('--verbose') === 3
+ ) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2')
+ || $input->getParameterOption('--verbose') === 2
+ ) {
+ $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
+ } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1')
+ || $input->hasParameterOption('--verbose')
+ || $input->getParameterOption('--verbose')
+ ) {
+ $output->setVerbosity(Output::VERBOSITY_VERBOSE);
+ }
+ }
+ }
+
+ /**
+ * 执行指令
+ * @param Command $command 指令实例
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ * @return int
+ * @throws \Exception
+ */
+ protected function doRunCommand(Command $command, Input $input, Output $output)
+ {
+ return $command->run($input, $output);
+ }
+
+ /**
+ * 获取指令的基础名称
+ * @param Input $input
+ * @return string
+ */
+ protected function getCommandName(Input $input)
+ {
+ return $input->getFirstArgument();
+ }
+
+ /**
+ * 获取默认输入定义
+ * @return InputDefinition
+ */
+ protected function getDefaultInputDefinition()
+ {
+ return new InputDefinition([
+ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
+ new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
+ new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
+ new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
+ new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
+ new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
+ new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
+ new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
+ ]);
+ }
+
+ /**
+ * 设置默认命令
+ * @return Command[] An array of default Command instances
+ */
+ protected function getDefaultCommands()
+ {
+ return [
+ new HelpCommand(),
+ new ListCommand(),
+ new MakeControllerCommand(),
+ new MakeModelCommand(),
+ new BuildCommand(),
+ ];
+ }
+
+ /**
+ * 设置默认助手
+ * @return HelperSet
+ */
+ protected function getDefaultHelperSet()
+ {
+ return new HelperSet([
+ new FormatterHelper(),
+ new DebugFormatterHelper(),
+ new ProcessHelper(),
+ new QuestionHelper(),
+ ]);
+ }
+
+ /**
+ * 获取stty列数
+ * @return string
+ */
+ private function getSttyColumns()
+ {
+ if (!function_exists('proc_open')) {
+ return null;
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ return $info;
+ }
+ return null;
+ }
+
+ /**
+ * 获取终端模式
+ * @return string x 或 null
+ */
+ private function getConsoleMode()
+ {
+ if (!function_exists('proc_open')) {
+ return null;
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
+ return $matches[2] . 'x' . $matches[1];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取可能的建议
+ * @param array $abbrevs
+ * @return string
+ */
+ private function getAbbreviationSuggestions($abbrevs)
+ {
+ return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
+ }
+
+ /**
+ * 返回命名空间部分
+ * @param string $name 指令
+ * @param string $limit 部分的命名空间的最大数量
+ * @return string
+ */
+ public function extractNamespace($name, $limit = null)
+ {
+ $parts = explode(':', $name);
+ array_pop($parts);
+
+ return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit));
+ }
+
+ /**
+ * 查找可替代的建议
+ * @param string $name
+ * @param array|\Traversable $collection
+ * @return array
+ */
+ private function findAlternatives($name, $collection)
+ {
+ $threshold = 1e3;
+ $alternatives = [];
+
+ $collectionParts = [];
+ foreach ($collection as $item) {
+ $collectionParts[$item] = explode(':', $item);
+ }
+
+ foreach (explode(':', $name) as $i => $subname) {
+ foreach ($collectionParts as $collectionName => $parts) {
+ $exists = isset($alternatives[$collectionName]);
+ if (!isset($parts[$i]) && $exists) {
+ $alternatives[$collectionName] += $threshold;
+ continue;
+ } elseif (!isset($parts[$i])) {
+ continue;
+ }
+
+ $lev = levenshtein($subname, $parts[$i]);
+ if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
+ $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
+ } elseif ($exists) {
+ $alternatives[$collectionName] += $threshold;
+ }
+ }
+ }
+
+ foreach ($collection as $item) {
+ $lev = levenshtein($name, $item);
+ if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
+ $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
+ }
+ }
+
+ $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
+ return $lev < 2 * $threshold;
+ });
+ asort($alternatives);
+
+ return array_keys($alternatives);
+ }
+
+ /**
+ * 设置默认的指令
+ * @param string $commandName The Command name
+ */
+ public function setDefaultCommand($commandName)
+ {
+ $this->defaultCommand = $commandName;
+ }
+
+ private function stringWidth($string)
+ {
+ if (!function_exists('mb_strwidth')) {
+ return strlen($string);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return strlen($string);
+ }
+
+ return mb_strwidth($string, $encoding);
+ }
+
+ private function splitStringByWidth($string, $width)
+ {
+ if (!function_exists('mb_strwidth')) {
+ return str_split($string, $width);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return str_split($string, $width);
+ }
+
+ $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+ $lines = [];
+ $line = '';
+ foreach (preg_split('//u', $utf8String) as $char) {
+ if (mb_strwidth($line . $char, 'utf8') <= $width) {
+ $line .= $char;
+ continue;
+ }
+ $lines[] = str_pad($line, $width);
+ $line = $char;
+ }
+ if (strlen($line)) {
+ $lines[] = count($lines) ? str_pad($line, $width) : $line;
+ }
+
+ mb_convert_variables($encoding, 'utf8', $lines);
+
+ return $lines;
+ }
+
+ /**
+ * 返回所有的命名空间
+ * @param string $name
+ * @return array
+ */
+ private function extractAllNamespaces($name)
+ {
+ $parts = explode(':', $name, -1);
+ $namespaces = [];
+
+ foreach ($parts as $part) {
+ if (count($namespaces)) {
+ $namespaces[] = end($namespaces) . ':' . $part;
+ } else {
+ $namespaces[] = $part;
+ }
+ }
+
+ return $namespaces;
+ }
+
+}
diff --git a/library/think/Process.php b/library/think/Process.php
new file mode 100644
index 00000000..919cf4f2
--- /dev/null
+++ b/library/think/Process.php
@@ -0,0 +1,1206 @@
+
+// +----------------------------------------------------------------------
+
+namespace think;
+
+
+use think\process\exception\Failed as ProcessFailedException;
+use think\process\exception\Timeout as ProcessTimeoutException;
+use think\process\pipes\Pipes;
+use think\process\Utils;
+use think\process\pipes\Unix as UnixPipes;
+use think\process\pipes\Windows as WindowsPipes;
+
+class Process
+{
+
+ const ERR = 'err';
+ const OUT = 'out';
+
+ const STATUS_READY = 'ready';
+ const STATUS_STARTED = 'started';
+ const STATUS_TERMINATED = 'terminated';
+
+ const STDIN = 0;
+ const STDOUT = 1;
+ const STDERR = 2;
+
+ const TIMEOUT_PRECISION = 0.2;
+
+ private $callback;
+ private $commandline;
+ private $cwd;
+ private $env;
+ private $input;
+ private $starttime;
+ private $lastOutputTime;
+ private $timeout;
+ private $idleTimeout;
+ private $options;
+ private $exitcode;
+ private $fallbackExitcode;
+ private $processInformation;
+ private $outputDisabled = false;
+ private $stdout;
+ private $stderr;
+ private $enhanceWindowsCompatibility = true;
+ private $enhanceSigchildCompatibility;
+ private $process;
+ private $status = self::STATUS_READY;
+ private $incrementalOutputOffset = 0;
+ private $incrementalErrorOutputOffset = 0;
+ private $tty;
+ private $pty;
+
+ private $useFileHandles = false;
+
+ /** @var Pipes */
+ private $processPipes;
+
+ private $latestSignal;
+
+ private static $sigchild;
+
+ /**
+ * @var array
+ */
+ public static $exitCodes = [
+ 0 => 'OK',
+ 1 => 'General error',
+ 2 => 'Misuse of shell builtins',
+ 126 => 'Invoked command cannot execute',
+ 127 => 'Command not found',
+ 128 => 'Invalid exit argument',
+ // signals
+ 129 => 'Hangup',
+ 130 => 'Interrupt',
+ 131 => 'Quit and dump core',
+ 132 => 'Illegal instruction',
+ 133 => 'Trace/breakpoint trap',
+ 134 => 'Process aborted',
+ 135 => 'Bus error: "access to undefined portion of memory object"',
+ 136 => 'Floating point exception: "erroneous arithmetic operation"',
+ 137 => 'Kill (terminate immediately)',
+ 138 => 'User-defined 1',
+ 139 => 'Segmentation violation',
+ 140 => 'User-defined 2',
+ 141 => 'Write to pipe with no one reading',
+ 142 => 'Signal raised by alarm',
+ 143 => 'Termination (request to terminate)',
+ // 144 - not defined
+ 145 => 'Child process terminated, stopped (or continued*)',
+ 146 => 'Continue if stopped',
+ 147 => 'Stop executing temporarily',
+ 148 => 'Terminal stop signal',
+ 149 => 'Background process attempting to read from tty ("in")',
+ 150 => 'Background process attempting to write to tty ("out")',
+ 151 => 'Urgent data available on socket',
+ 152 => 'CPU time limit exceeded',
+ 153 => 'File size limit exceeded',
+ 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
+ 155 => 'Profiling timer expired',
+ // 156 - not defined
+ 157 => 'Pollable event',
+ // 158 - not defined
+ 159 => 'Bad syscall',
+ ];
+
+ /**
+ * 构造方法
+ * @param string $commandline 指令
+ * @param string|null $cwd 工作目录
+ * @param array|null $env 环境变量
+ * @param string|null $input 输入
+ * @param int|float|null $timeout 超时时间
+ * @param array $options proc_open的选项
+ * @throws \RuntimeException
+ * @api
+ */
+ public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = [])
+ {
+ if (!function_exists('proc_open')) {
+ throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
+ }
+
+ $this->commandline = $commandline;
+ $this->cwd = $cwd;
+
+ if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) {
+ $this->cwd = getcwd();
+ }
+ if (null !== $env) {
+ $this->setEnv($env);
+ }
+
+ $this->input = $input;
+ $this->setTimeout($timeout);
+ $this->useFileHandles = '\\' === DS;
+ $this->pty = false;
+ $this->enhanceWindowsCompatibility = true;
+ $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled();
+ $this->options = array_replace([
+ 'suppress_errors' => true,
+ 'binary_pipes' => true
+ ], $options);
+ }
+
+ public function __destruct()
+ {
+ $this->stop();
+ }
+
+ public function __clone()
+ {
+ $this->resetProcessData();
+ }
+
+ /**
+ * 运行指令
+ * @param callback|null $callback
+ * @return int
+ */
+ public function run($callback = null)
+ {
+ $this->start($callback);
+
+ return $this->wait();
+ }
+
+ /**
+ * 运行指令
+ * @param callable|null $callback
+ * @return self
+ * @throws \RuntimeException
+ * @throws ProcessFailedException
+ */
+ public function mustRun($callback = null)
+ {
+ if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
+ }
+
+ if (0 !== $this->run($callback)) {
+ throw new ProcessFailedException($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 启动进程并写到 STDIN 输入后返回。
+ * @param callable|null $callback
+ * @throws \RuntimeException
+ * @throws \RuntimeException
+ * @throws \LogicException
+ */
+ public function start($callback = null)
+ {
+ if ($this->isRunning()) {
+ throw new \RuntimeException('Process is already running');
+ }
+ if ($this->outputDisabled && null !== $callback) {
+ throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.');
+ }
+
+ $this->resetProcessData();
+ $this->starttime = $this->lastOutputTime = microtime(true);
+ $this->callback = $this->buildCallback($callback);
+ $descriptors = $this->getDescriptors();
+
+ $commandline = $this->commandline;
+
+ if ('\\' === DS && $this->enhanceWindowsCompatibility) {
+ $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')';
+ foreach ($this->processPipes->getFiles() as $offset => $filename) {
+ $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($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);
+
+ if (!is_resource($this->process)) {
+ throw new \RuntimeException('Unable to launch a new process.');
+ }
+ $this->status = self::STATUS_STARTED;
+
+ if ($this->tty) {
+ return;
+ }
+
+ $this->updateStatus(false);
+ $this->checkTimeout();
+ }
+
+ /**
+ * 重启进程
+ * @param callable|null $callback
+ * @return Process
+ * @throws \RuntimeException
+ * @throws \RuntimeException
+ */
+ public function restart($callback = null)
+ {
+ if ($this->isRunning()) {
+ throw new \RuntimeException('Process is already running');
+ }
+
+ $process = clone $this;
+ $process->start($callback);
+
+ return $process;
+ }
+
+ /**
+ * 等待要终止的进程
+ * @param callable|null $callback
+ * @return int
+ */
+ public function wait($callback = null)
+ {
+ $this->requireProcessIsStarted(__FUNCTION__);
+
+ $this->updateStatus(false);
+ if (null !== $callback) {
+ $this->callback = $this->buildCallback($callback);
+ }
+
+ do {
+ $this->checkTimeout();
+ $running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen();
+ $close = '\\' !== DS || !$running;
+ $this->readPipes(true, $close);
+ } while ($running);
+
+ while ($this->isRunning()) {
+ usleep(1000);
+ }
+
+ if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
+ throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
+ }
+
+ return $this->exitcode;
+ }
+
+ /**
+ * 获取PID
+ * @return int|null
+ * @throws \RuntimeException
+ */
+ public function getPid()
+ {
+ if ($this->isSigchildEnabled()) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.');
+ }
+
+ $this->updateStatus(false);
+
+ return $this->isRunning() ? $this->processInformation['pid'] : null;
+ }
+
+ /**
+ * 将一个 POSIX 信号发送到进程中
+ * @param int $signal
+ * @return Process
+ */
+ public function signal($signal)
+ {
+ $this->doSignal($signal, true);
+
+ return $this;
+ }
+
+ /**
+ * 禁用从底层过程获取输出和错误输出。
+ * @return Process
+ */
+ public function disableOutput()
+ {
+ if ($this->isRunning()) {
+ throw new \RuntimeException('Disabling output while the process is running is not possible.');
+ }
+ if (null !== $this->idleTimeout) {
+ throw new \LogicException('Output can not be disabled while an idle timeout is set.');
+ }
+
+ $this->outputDisabled = true;
+
+ return $this;
+ }
+
+ /**
+ * 开启从底层过程获取输出和错误输出。
+ * @return Process
+ * @throws \RuntimeException
+ */
+ public function enableOutput()
+ {
+ if ($this->isRunning()) {
+ throw new \RuntimeException('Enabling output while the process is running is not possible.');
+ }
+
+ $this->outputDisabled = false;
+
+ return $this;
+ }
+
+ /**
+ * 输出是否禁用
+ * @return bool
+ */
+ public function isOutputDisabled()
+ {
+ return $this->outputDisabled;
+ }
+
+ /**
+ * 获取当前的输出管道
+ * @return string
+ * @throws \LogicException
+ * @throws \LogicException
+ * @api
+ */
+ public function getOutput()
+ {
+ if ($this->outputDisabled) {
+ throw new \LogicException('Output has been disabled.');
+ }
+
+ $this->requireProcessIsStarted(__FUNCTION__);
+
+ $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
+
+ return $this->stdout;
+ }
+
+ /**
+ * 以增量方式返回的输出结果。
+ * @return string
+ */
+ public function getIncrementalOutput()
+ {
+ $this->requireProcessIsStarted(__FUNCTION__);
+
+ $data = $this->getOutput();
+
+ $latest = substr($data, $this->incrementalOutputOffset);
+
+ if (false === $latest) {
+ return '';
+ }
+
+ $this->incrementalOutputOffset = strlen($data);
+
+ return $latest;
+ }
+
+ /**
+ * 清空输出
+ * @return Process
+ */
+ public function clearOutput()
+ {
+ $this->stdout = '';
+ $this->incrementalOutputOffset = 0;
+
+ return $this;
+ }
+
+ /**
+ * 返回当前的错误输出的过程 (STDERR)。
+ * @return string
+ */
+ public function getErrorOutput()
+ {
+ if ($this->outputDisabled) {
+ throw new \LogicException('Output has been disabled.');
+ }
+
+ $this->requireProcessIsStarted(__FUNCTION__);
+
+ $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
+
+ return $this->stderr;
+ }
+
+ /**
+ * 以增量方式返回 errorOutput
+ * @return string
+ */
+ public function getIncrementalErrorOutput()
+ {
+ $this->requireProcessIsStarted(__FUNCTION__);
+
+ $data = $this->getErrorOutput();
+
+ $latest = substr($data, $this->incrementalErrorOutputOffset);
+
+ if (false === $latest) {
+ return '';
+ }
+
+ $this->incrementalErrorOutputOffset = strlen($data);
+
+ return $latest;
+ }
+
+ /**
+ * 清空 errorOutput
+ * @return Process
+ */
+ public function clearErrorOutput()
+ {
+ $this->stderr = '';
+ $this->incrementalErrorOutputOffset = 0;
+
+ return $this;
+ }
+
+ /**
+ * 获取退出码
+ * @return null|int
+ */
+ public function getExitCode()
+ {
+ if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
+ }
+
+ $this->updateStatus(false);
+
+ return $this->exitcode;
+ }
+
+ /**
+ * 获取退出文本
+ * @return null|string
+ */
+ public function getExitCodeText()
+ {
+ if (null === $exitcode = $this->getExitCode()) {
+ return null;
+ }
+
+ return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
+ }
+
+ /**
+ * 检查是否成功
+ * @return bool
+ */
+ public function isSuccessful()
+ {
+ return 0 === $this->getExitCode();
+ }
+
+ /**
+ * 是否未捕获的信号已被终止子进程
+ * @return bool
+ */
+ public function hasBeenSignaled()
+ {
+ $this->requireProcessIsTerminated(__FUNCTION__);
+
+ if ($this->isSigchildEnabled()) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
+ }
+
+ $this->updateStatus(false);
+
+ return $this->processInformation['signaled'];
+ }
+
+ /**
+ * 返回导致子进程终止其执行的数。
+ * @return int
+ */
+ public function getTermSignal()
+ {
+ $this->requireProcessIsTerminated(__FUNCTION__);
+
+ if ($this->isSigchildEnabled()) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
+ }
+
+ $this->updateStatus(false);
+
+ return $this->processInformation['termsig'];
+ }
+
+ /**
+ * 检查子进程信号是否已停止
+ * @return bool
+ */
+ public function hasBeenStopped()
+ {
+ $this->requireProcessIsTerminated(__FUNCTION__);
+
+ $this->updateStatus(false);
+
+ return $this->processInformation['stopped'];
+ }
+
+ /**
+ * 返回导致子进程停止其执行的数。
+ * @return int
+ */
+ public function getStopSignal()
+ {
+ $this->requireProcessIsTerminated(__FUNCTION__);
+
+ $this->updateStatus(false);
+
+ return $this->processInformation['stopsig'];
+ }
+
+ /**
+ * 检查是否正在运行
+ * @return bool
+ */
+ public function isRunning()
+ {
+ if (self::STATUS_STARTED !== $this->status) {
+ return false;
+ }
+
+ $this->updateStatus(false);
+
+ return $this->processInformation['running'];
+ }
+
+ /**
+ * 检查是否已开始
+ * @return bool
+ */
+ public function isStarted()
+ {
+ return $this->status != self::STATUS_READY;
+ }
+
+ /**
+ * 检查是否已终止
+ * @return bool
+ */
+ public function isTerminated()
+ {
+ $this->updateStatus(false);
+
+ return $this->status == self::STATUS_TERMINATED;
+ }
+
+ /**
+ * 获取当前的状态
+ * @return string
+ */
+ public function getStatus()
+ {
+ $this->updateStatus(false);
+
+ return $this->status;
+ }
+
+ /**
+ * 终止进程
+ */
+ public function stop()
+ {
+ if ($this->isRunning()) {
+ if ('\\' === DS && !$this->isSigchildEnabled()) {
+ exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode);
+ if ($exitCode > 0) {
+ throw new \RuntimeException('Unable to kill the process');
+ }
+ } else {
+ $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`);
+ foreach ($pids as $pid) {
+ if (is_numeric($pid)) {
+ posix_kill($pid, 9);
+ }
+ }
+ }
+ }
+
+ $this->updateStatus(false);
+ if ($this->processInformation['running']) {
+ $this->close();
+ }
+
+ return $this->exitcode;
+ }
+
+ /**
+ * 添加一行输出
+ * @param string $line
+ */
+ public function addOutput($line)
+ {
+ $this->lastOutputTime = microtime(true);
+ $this->stdout .= $line;
+ }
+
+ /**
+ * 添加一行错误输出
+ * @param string $line
+ */
+ public function addErrorOutput($line)
+ {
+ $this->lastOutputTime = microtime(true);
+ $this->stderr .= $line;
+ }
+
+ /**
+ * 获取被执行的指令
+ * @return string
+ */
+ public function getCommandLine()
+ {
+ return $this->commandline;
+ }
+
+ /**
+ * 设置指令
+ * @param string $commandline
+ * @return self
+ */
+ public function setCommandLine($commandline)
+ {
+ $this->commandline = $commandline;
+
+ return $this;
+ }
+
+ /**
+ * 获取超时时间
+ * @return float|null
+ */
+ public function getTimeout()
+ {
+ return $this->timeout;
+ }
+
+ /**
+ * 获取idle超时时间
+ * @return float|null
+ */
+ public function getIdleTimeout()
+ {
+ return $this->idleTimeout;
+ }
+
+ /**
+ * 设置超时时间
+ * @param int|float|null $timeout
+ * @return self
+ */
+ public function setTimeout($timeout)
+ {
+ $this->timeout = $this->validateTimeout($timeout);
+
+ return $this;
+ }
+
+ /**
+ * 设置idle超时时间
+ * @param int|float|null $timeout
+ * @return self
+ */
+ public function setIdleTimeout($timeout)
+ {
+ if (null !== $timeout && $this->outputDisabled) {
+ throw new \LogicException('Idle timeout can not be set while the output is disabled.');
+ }
+
+ $this->idleTimeout = $this->validateTimeout($timeout);
+
+ return $this;
+ }
+
+ /**
+ * 设置TTY
+ * @param bool $tty
+ * @return self
+ */
+ public function setTty($tty)
+ {
+ if ('\\' === DS && $tty) {
+ throw new \RuntimeException('TTY mode is not supported on Windows platform.');
+ }
+ if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) {
+ throw new \RuntimeException('TTY mode requires /dev/tty to be readable.');
+ }
+
+ $this->tty = (bool)$tty;
+
+ return $this;
+ }
+
+ /**
+ * 检查是否是tty模式
+ * @return bool
+ */
+ public function isTty()
+ {
+ return $this->tty;
+ }
+
+ /**
+ * 设置pty模式
+ * @param bool $bool
+ * @return self
+ */
+ public function setPty($bool)
+ {
+ $this->pty = (bool)$bool;
+
+ return $this;
+ }
+
+ /**
+ * 是否是pty模式
+ * @return bool
+ */
+ public function isPty()
+ {
+ return $this->pty;
+ }
+
+ /**
+ * 获取工作目录
+ * @return string|null
+ */
+ public function getWorkingDirectory()
+ {
+ if (null === $this->cwd) {
+ return getcwd() ?: null;
+ }
+
+ return $this->cwd;
+ }
+
+ /**
+ * 设置工作目录
+ * @param string $cwd
+ * @return self
+ */
+ public function setWorkingDirectory($cwd)
+ {
+ $this->cwd = $cwd;
+
+ return $this;
+ }
+
+ /**
+ * 获取环境变量
+ * @return array
+ */
+ public function getEnv()
+ {
+ return $this->env;
+ }
+
+ /**
+ * 设置环境变量
+ * @param array $env
+ * @return self
+ */
+ public function setEnv(array $env)
+ {
+ $env = array_filter($env, function ($value) {
+ return !is_array($value);
+ });
+
+ $this->env = [];
+ foreach ($env as $key => $value) {
+ $this->env[(binary)$key] = (binary)$value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取输入
+ * @return null|string
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+
+ /**
+ * 设置输入
+ * @param mixed $input
+ * @return self
+ */
+ public function setInput($input)
+ {
+ if ($this->isRunning()) {
+ throw new \LogicException('Input can not be set while the process is running.');
+ }
+
+ $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input);
+
+ return $this;
+ }
+
+ /**
+ * 获取proc_open的选项
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * 设置proc_open的选项
+ * @param array $options
+ * @return self
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = $options;
+
+ return $this;
+ }
+
+ /**
+ * 是否兼容windows
+ * @return bool
+ */
+ public function getEnhanceWindowsCompatibility()
+ {
+ return $this->enhanceWindowsCompatibility;
+ }
+
+ /**
+ * 设置是否兼容windows
+ * @param bool $enhance
+ * @return self
+ */
+ public function setEnhanceWindowsCompatibility($enhance)
+ {
+ $this->enhanceWindowsCompatibility = (bool)$enhance;
+
+ return $this;
+ }
+
+ /**
+ * 返回是否 sigchild 兼容模式激活
+ * @return bool
+ */
+ public function getEnhanceSigchildCompatibility()
+ {
+ return $this->enhanceSigchildCompatibility;
+ }
+
+ /**
+ * 激活 sigchild 兼容性模式。
+ * @param bool $enhance
+ * @return self
+ */
+ public function setEnhanceSigchildCompatibility($enhance)
+ {
+ $this->enhanceSigchildCompatibility = (bool)$enhance;
+
+ return $this;
+ }
+
+ /**
+ * 是否超时
+ */
+ public function checkTimeout()
+ {
+ if ($this->status !== self::STATUS_STARTED) {
+ return;
+ }
+
+ if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
+ $this->stop();
+
+ throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL);
+ }
+
+ if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
+ $this->stop();
+
+ throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE);
+ }
+ }
+
+ /**
+ * 是否支持pty
+ * @return bool
+ */
+ public static function isPtySupported()
+ {
+ static $result;
+
+ if (null !== $result) {
+ return $result;
+ }
+
+ if ('\\' === DS) {
+ return $result = false;
+ }
+
+ $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes);
+ if (is_resource($proc)) {
+ proc_close($proc);
+
+ return $result = true;
+ }
+
+ return $result = false;
+ }
+
+ /**
+ * 创建所需的 proc_open 的描述符
+ * @return array
+ */
+ private function getDescriptors()
+ {
+ if ('\\' === DS) {
+ $this->processPipes = WindowsPipes::create($this, $this->input);
+ } else {
+ $this->processPipes = UnixPipes::create($this, $this->input);
+ }
+ $descriptors = $this->processPipes->getDescriptors($this->outputDisabled);
+
+ if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
+
+ $descriptors = array_merge($descriptors, [['pipe', 'w']]);
+
+ $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code';
+ }
+
+ return $descriptors;
+ }
+
+ /**
+ * 建立 wait () 使用的回调。
+ * @param callable|null $callback
+ * @return callable
+ */
+ protected function buildCallback($callback)
+ {
+ $out = self::OUT;
+ $callback = function ($type, $data) use ($callback, $out) {
+ if ($out == $type) {
+ $this->addOutput($data);
+ } else {
+ $this->addErrorOutput($data);
+ }
+
+ if (null !== $callback) {
+ call_user_func($callback, $type, $data);
+ }
+ };
+
+ return $callback;
+ }
+
+ /**
+ * 更新状态
+ * @param bool $blocking
+ */
+ protected function updateStatus($blocking)
+ {
+ if (self::STATUS_STARTED !== $this->status) {
+ return;
+ }
+
+ $this->processInformation = proc_get_status($this->process);
+ $this->captureExitCode();
+
+ $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true);
+
+ if (!$this->processInformation['running']) {
+ $this->close();
+ }
+ }
+
+ /**
+ * 是否开启 '--enable-sigchild'
+ * @return bool
+ */
+ protected function isSigchildEnabled()
+ {
+ if (null !== self::$sigchild) {
+ return self::$sigchild;
+ }
+
+ if (!function_exists('phpinfo')) {
+ return self::$sigchild = false;
+ }
+
+ ob_start();
+ phpinfo(INFO_GENERAL);
+
+ return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
+ }
+
+ /**
+ * 验证是否超时
+ * @param int|float|null $timeout
+ * @return float|null
+ */
+ private function validateTimeout($timeout)
+ {
+ $timeout = (float)$timeout;
+
+ if (0.0 === $timeout) {
+ $timeout = null;
+ } elseif ($timeout < 0) {
+ throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
+ }
+
+ return $timeout;
+ }
+
+ /**
+ * 读取pipes
+ * @param bool $blocking
+ * @param bool $close
+ */
+ private function readPipes($blocking, $close)
+ {
+ $result = $this->processPipes->readAndWrite($blocking, $close);
+
+ $callback = $this->callback;
+ foreach ($result as $type => $data) {
+ if (3 == $type) {
+ $this->fallbackExitcode = (int)$data;
+ } else {
+ $callback($type === self::STDOUT ? self::OUT : self::ERR, $data);
+ }
+ }
+ }
+
+ /**
+ * 捕获退出码
+ */
+ private function captureExitCode()
+ {
+ if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) {
+ $this->exitcode = $this->processInformation['exitcode'];
+ }
+ }
+
+ /**
+ * 关闭资源
+ * @return int 退出码
+ */
+ private function close()
+ {
+ $this->processPipes->close();
+ if (is_resource($this->process)) {
+ $exitcode = proc_close($this->process);
+ } else {
+ $exitcode = -1;
+ }
+
+ $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1);
+ $this->status = self::STATUS_TERMINATED;
+
+ if (-1 === $this->exitcode && null !== $this->fallbackExitcode) {
+ $this->exitcode = $this->fallbackExitcode;
+ } elseif (-1 === $this->exitcode && $this->processInformation['signaled']
+ && 0 < $this->processInformation['termsig']
+ ) {
+ $this->exitcode = 128 + $this->processInformation['termsig'];
+ }
+
+ return $this->exitcode;
+ }
+
+ /**
+ * 重置数据
+ */
+ private function resetProcessData()
+ {
+ $this->starttime = null;
+ $this->callback = null;
+ $this->exitcode = null;
+ $this->fallbackExitcode = null;
+ $this->processInformation = null;
+ $this->stdout = null;
+ $this->stderr = null;
+ $this->process = null;
+ $this->latestSignal = null;
+ $this->status = self::STATUS_READY;
+ $this->incrementalOutputOffset = 0;
+ $this->incrementalErrorOutputOffset = 0;
+ }
+
+ /**
+ * 将一个 POSIX 信号发送到进程中。
+ * @param int $signal
+ * @param bool $throwException
+ * @return bool
+ */
+ private function doSignal($signal, $throwException)
+ {
+ if (!$this->isRunning()) {
+ if ($throwException) {
+ throw new \LogicException('Can not send signal on a non running process.');
+ }
+
+ return false;
+ }
+
+ if ($this->isSigchildEnabled()) {
+ if ($throwException) {
+ throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.');
+ }
+
+ return false;
+ }
+
+ if (true !== @proc_terminate($this->process, $signal)) {
+ if ($throwException) {
+ throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
+ }
+
+ return false;
+ }
+
+ $this->latestSignal = $signal;
+
+ return true;
+ }
+
+ /**
+ * 确保进程已经开启
+ * @param string $functionName
+ */
+ private function requireProcessIsStarted($functionName)
+ {
+ if (!$this->isStarted()) {
+ throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName));
+ }
+ }
+
+ /**
+ * 确保进程已经终止
+ * @param string $functionName
+ */
+ private function requireProcessIsTerminated($functionName)
+ {
+ if (!$this->isTerminated()) {
+ throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/Input.php b/library/think/console/Input.php
new file mode 100644
index 00000000..c60d6082
--- /dev/null
+++ b/library/think/console/Input.php
@@ -0,0 +1,471 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console;
+
+use think\console\input\Definition;
+use think\console\input\Argument;
+use think\console\input\Option;
+
+class Input
+{
+
+ /**
+ * @var Definition
+ */
+ protected $definition;
+
+ /**
+ * @var Option[]
+ */
+ protected $options = [];
+
+ /**
+ * @var Argument[]
+ */
+ protected $arguments = [];
+
+ protected $interactive = true;
+
+ private $tokens;
+ private $parsed;
+
+
+ public function __construct($argv = null)
+ {
+ if (null === $argv) {
+ $argv = $_SERVER['argv'];
+ // 去除命令名
+ array_shift($argv);
+ }
+
+ $this->tokens = $argv;
+
+ $this->definition = new Definition();
+ }
+
+
+ protected function setTokens(array $tokens)
+ {
+ $this->tokens = $tokens;
+ }
+
+ /**
+ * 绑定实例
+ * @param Definition $definition A InputDefinition instance
+ */
+ public function bind(Definition $definition)
+ {
+ $this->arguments = [];
+ $this->options = [];
+ $this->definition = $definition;
+
+ $this->parse();
+ }
+
+
+ /**
+ * 解析参数
+ */
+ protected function parse()
+ {
+ $parseOptions = true;
+ $this->parsed = $this->tokens;
+ while (null !== $token = array_shift($this->parsed)) {
+ if ($parseOptions && '' == $token) {
+ $this->parseArgument($token);
+ } elseif ($parseOptions && '--' == $token) {
+ $parseOptions = false;
+ } elseif ($parseOptions && 0 === strpos($token, '--')) {
+ $this->parseLongOption($token);
+ } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
+ $this->parseShortOption($token);
+ } else {
+ $this->parseArgument($token);
+ }
+ }
+ }
+
+
+ /**
+ * 解析短选项
+ * @param string $token 当前的指令.
+ */
+ private function parseShortOption($token)
+ {
+ $name = substr($token, 1);
+
+ if (strlen($name) > 1) {
+ if ($this->definition->hasShortcut($name[0])
+ && $this->definition->getOptionForShortcut($name[0])->acceptValue()
+ ) {
+ $this->addShortOption($name[0], substr($name, 1));
+ } else {
+ $this->parseShortOptionSet($name);
+ }
+ } else {
+ $this->addShortOption($name, null);
+ }
+ }
+
+ /**
+ * 解析短选项
+ * @param string $name 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseShortOptionSet($name)
+ {
+ $len = strlen($name);
+ for ($i = 0; $i < $len; ++$i) {
+ if (!$this->definition->hasShortcut($name[$i])) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i]));
+ }
+
+ $option = $this->definition->getOptionForShortcut($name[$i]);
+ if ($option->acceptValue()) {
+ $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
+
+ break;
+ } else {
+ $this->addLongOption($option->getName(), null);
+ }
+ }
+ }
+
+ /**
+ * 解析完整选项
+ * @param string $token 当前指令
+ */
+ private function parseLongOption($token)
+ {
+ $name = substr($token, 2);
+
+ if (false !== $pos = strpos($name, '=')) {
+ $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1));
+ } else {
+ $this->addLongOption($name, null);
+ }
+ }
+
+ /**
+ * 解析参数
+ * @param string $token 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseArgument($token)
+ {
+ $c = count($this->arguments);
+
+ if ($this->definition->hasArgument($c)) {
+ $arg = $this->definition->getArgument($c);
+
+ $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
+
+ } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
+ $arg = $this->definition->getArgument($c - 1);
+
+ $this->arguments[$arg->getName()][] = $token;
+ } else {
+ throw new \RuntimeException('Too many arguments.');
+ }
+ }
+
+
+ /**
+ * 添加一个短选项的值
+ * @param string $shortcut 短名称
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addShortOption($shortcut, $value)
+ {
+ if (!$this->definition->hasShortcut($shortcut)) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
+ }
+
+ /**
+ * 添加一个完整选项的值
+ * @param string $name 选项名
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addLongOption($name, $value)
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ $option = $this->definition->getOption($name);
+
+ if (false === $value) {
+ $value = null;
+ }
+
+ if (null !== $value && !$option->acceptValue()) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value));
+ }
+
+ if (null === $value && $option->acceptValue() && count($this->parsed)) {
+ $next = array_shift($this->parsed);
+ if (isset($next[0]) && '-' !== $next[0]) {
+ $value = $next;
+ } elseif (empty($next)) {
+ $value = '';
+ } else {
+ array_unshift($this->parsed, $next);
+ }
+ }
+
+ if (null === $value) {
+ if ($option->isValueRequired()) {
+ throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name));
+ }
+
+ if (!$option->isArray()) {
+ $value = $option->isValueOptional() ? $option->getDefault() : true;
+ }
+ }
+
+ if ($option->isArray()) {
+ $this->options[$name][] = $value;
+ } else {
+ $this->options[$name] = $value;
+ }
+ }
+
+ /**
+ * 获取第一个参数
+ * @return string|null
+ */
+ public function getFirstArgument()
+ {
+ foreach ($this->tokens as $token) {
+ if ($token && '-' === $token[0]) {
+ continue;
+ }
+
+ return $token;
+ }
+ return null;
+ }
+
+
+ /**
+ * 检查原始参数是否包含某个值
+ * @param string|array $values 需要检查的值
+ * @return bool
+ */
+ public function hasParameterOption($values)
+ {
+ $values = (array)$values;
+
+ foreach ($this->tokens as $token) {
+ foreach ($values as $value) {
+ if ($token === $value || 0 === strpos($token, $value . '=')) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取原始选项的值
+ * @param string|array $values 需要检查的值
+ * @param mixed $default 默认值
+ * @return mixed The option value
+ */
+ public function getParameterOption($values, $default = false)
+ {
+ $values = (array)$values;
+ $tokens = $this->tokens;
+
+ while (0 < count($tokens)) {
+ $token = array_shift($tokens);
+
+ foreach ($values as $value) {
+ if ($token === $value || 0 === strpos($token, $value . '=')) {
+ if (false !== $pos = strpos($token, '=')) {
+ return substr($token, $pos + 1);
+ }
+
+ return array_shift($tokens);
+ }
+ }
+ }
+
+ return $default;
+ }
+
+
+ /**
+ * 验证输入
+ * @throws \RuntimeException
+ */
+ public function validate()
+ {
+ if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) {
+ throw new \RuntimeException('Not enough arguments.');
+ }
+ }
+
+ /**
+ * 检查输入是否是交互的
+ * @return bool
+ */
+ public function isInteractive()
+ {
+ return $this->interactive;
+ }
+
+ /**
+ * 设置输入的交互
+ * @param bool
+ */
+ public function setInteractive($interactive)
+ {
+ $this->interactive = (bool)$interactive;
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[]
+ */
+ public function getArguments()
+ {
+ return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
+ }
+
+ /**
+ * 根据名称获取参数
+ * @param string $name 参数名
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument($name)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)
+ ->getDefault();
+ }
+
+ /**
+ * 设置参数的值
+ * @param string $name 参数名
+ * @param string $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setArgument($name, $value)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $this->arguments[$name] = $value;
+ }
+
+ /**
+ * 检查是否存在某个参数
+ * @param string|int $name 参数名或位置
+ * @return bool
+ */
+ public function hasArgument($name)
+ {
+ return $this->definition->hasArgument($name);
+ }
+
+ /**
+ * 获取所有的选项
+ * @return Option[]
+ */
+ public function getOptions()
+ {
+ return array_merge($this->definition->getOptionDefaults(), $this->options);
+ }
+
+ /**
+ * 获取选项值
+ * @param string $name 选项名称
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getOption($name)
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
+ }
+
+ /**
+ * 设置选项值
+ * @param string $name 选项名
+ * @param string|bool $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setOption($name, $value)
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ $this->options[$name] = $value;
+ }
+
+ /**
+ * 是否有某个选项
+ * @param string $name 选项名
+ * @return bool
+ */
+ public function hasOption($name)
+ {
+ return $this->definition->hasOption($name);
+ }
+
+ /**
+ * 转义指令
+ * @param string $token
+ * @return string
+ */
+ public function escapeToken($token)
+ {
+ return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
+ }
+
+ /**
+ * 返回传递给命令的参数的字符串
+ * @return string
+ */
+ public function __toString()
+ {
+ $tokens = array_map(function ($token) {
+ if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
+ return $match[1] . $this->escapeToken($match[2]);
+ }
+
+ if ($token && $token[0] !== '-') {
+ return $this->escapeToken($token);
+ }
+
+ return $token;
+ }, $this->tokens);
+
+ return implode(' ', $tokens);
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/Output.php b/library/think/console/Output.php
new file mode 100644
index 00000000..44eba7a5
--- /dev/null
+++ b/library/think/console/Output.php
@@ -0,0 +1,86 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console;
+
+use think\console\output\Formatter;
+use think\console\output\Stream;
+
+class Output extends Stream
+{
+
+ /** @var Stream */
+ private $stderr;
+
+ public function __construct()
+ {
+ $outputStream = 'php://stdout';
+ if (!$this->hasStdoutSupport()) {
+ $outputStream = 'php://output';
+ }
+
+ parent::__construct(fopen($outputStream, 'w'));
+
+ $this->stderr = new Stream(fopen('php://stderr', 'w'), $this->getFormatter());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDecorated($decorated)
+ {
+ parent::setDecorated($decorated);
+ $this->stderr->setDecorated($decorated);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFormatter(Formatter $formatter)
+ {
+ parent::setFormatter($formatter);
+ $this->stderr->setFormatter($formatter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setVerbosity($level)
+ {
+ parent::setVerbosity($level);
+ $this->stderr->setVerbosity($level);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrorOutput()
+ {
+ return $this->stderr;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setErrorOutput(Output $error)
+ {
+ $this->stderr = $error;
+ }
+
+ /**
+ * 检查当前环境是否支持控制台输出写入标准输出。
+ * @return bool
+ */
+ protected function hasStdoutSupport()
+ {
+ return ('OS400' != php_uname('s'));
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/bin/README.md b/library/think/console/bin/README.md
new file mode 100644
index 00000000..9acc52fb
--- /dev/null
+++ b/library/think/console/bin/README.md
@@ -0,0 +1 @@
+console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。
diff --git a/library/think/console/bin/hiddeninput.exe b/library/think/console/bin/hiddeninput.exe
new file mode 100644
index 00000000..c8cf65e8
Binary files /dev/null and b/library/think/console/bin/hiddeninput.exe differ
diff --git a/library/think/console/command/Build.php b/library/think/console/command/Build.php
new file mode 100644
index 00000000..7201ef63
--- /dev/null
+++ b/library/think/console/command/Build.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class Build extends Command
+{
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setName('build')
+ ->setDefinition([new Option('config', null, Option::VALUE_OPTIONAL, "build.php path")])
+ ->setDescription('Build Application Dirs');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+
+ if ($input->hasOption('config')) {
+ $build = include $input->getOption('config');
+ } else {
+ $build = include APP_PATH . 'build.php';
+ }
+ if (empty($build)) {
+ $output->writeln("Build Config Is Empty");
+ return;
+ }
+ \think\Build::run($build);
+ $output->writeln("Successed");
+
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/command/Command.php b/library/think/console/command/Command.php
new file mode 100644
index 00000000..82237593
--- /dev/null
+++ b/library/think/console/command/Command.php
@@ -0,0 +1,501 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\Console;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\helper\Set as HelperSet;
+use think\console\input\Option;
+use think\console\Output;
+
+class Command
+{
+
+ /** @var Console */
+ private $console;
+ private $name;
+ private $aliases = [];
+ private $definition;
+ private $help;
+ private $description;
+ private $ignoreValidationErrors = false;
+ private $consoleDefinitionMerged = false;
+ private $consoleDefinitionMergedWithArgs = false;
+ private $code;
+ private $synopsis = [];
+ private $usages = [];
+
+ /** @var HelperSet */
+ private $helperSet;
+
+ /**
+ * 构造方法
+ * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置
+ * @throws \LogicException
+ * @api
+ */
+ public function __construct($name = null)
+ {
+ $this->definition = new Definition();
+
+ if (null !== $name) {
+ $this->setName($name);
+ }
+
+ $this->configure();
+
+ if (!$this->name) {
+ throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
+ }
+ }
+
+ /**
+ * 忽略验证错误
+ */
+ public function ignoreValidationErrors()
+ {
+ $this->ignoreValidationErrors = true;
+ }
+
+ /**
+ * 设置控制台
+ * @param Console $console
+ */
+ public function setConsole(Console $console = null)
+ {
+ $this->console = $console;
+ if ($console) {
+ $this->setHelperSet($console->getHelperSet());
+ } else {
+ $this->helperSet = null;
+ }
+ }
+
+ /**
+ * 设置帮助集
+ * @param HelperSet $helperSet
+ */
+ public function setHelperSet(HelperSet $helperSet)
+ {
+ $this->helperSet = $helperSet;
+ }
+
+ /**
+ * 获取帮助集
+ * @return HelperSet
+ */
+ public function getHelperSet()
+ {
+ return $this->helperSet;
+ }
+
+ /**
+ * 获取控制台
+ * @return Console
+ * @api
+ */
+ public function getConsole()
+ {
+ return $this->console;
+ }
+
+ /**
+ * 是否有效
+ * @return bool
+ */
+ public function isEnabled()
+ {
+ return true;
+ }
+
+ /**
+ * 配置指令
+ */
+ protected function configure()
+ {
+ }
+
+ /**
+ * 执行指令
+ * @param Input $input
+ * @param Output $output
+ * @return null|int
+ * @throws \LogicException
+ * @see setCode()
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ throw new \LogicException('You must override the execute() method in the concrete command class.');
+ }
+
+ /**
+ * 用户验证
+ * @param Input $input
+ * @param Output $output
+ */
+ protected function interact(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 初始化
+ * @param Input $input An InputInterface instance
+ * @param Output $output An OutputInterface instance
+ */
+ protected function initialize(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 执行
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ * @throws \Exception
+ * @see setCode()
+ * @see execute()
+ */
+ public function run(Input $input, Output $output)
+ {
+ $this->getSynopsis(true);
+ $this->getSynopsis(false);
+
+ $this->mergeConsoleDefinition();
+
+ try {
+ $input->bind($this->definition);
+ } catch (\Exception $e) {
+ if (!$this->ignoreValidationErrors) {
+ throw $e;
+ }
+ }
+
+ $this->initialize($input, $output);
+
+ if ($input->isInteractive()) {
+ $this->interact($input, $output);
+ }
+
+ $input->validate();
+
+ if ($this->code) {
+ $statusCode = call_user_func($this->code, $input, $output);
+ } else {
+ $statusCode = $this->execute($input, $output);
+ }
+
+ return is_numeric($statusCode) ? (int)$statusCode : 0;
+ }
+
+ /**
+ * 设置执行代码
+ * @param callable $code callable(InputInterface $input, OutputInterface $output)
+ * @return Command
+ * @throws \InvalidArgumentException
+ * @see execute()
+ */
+ public function setCode(callable $code)
+ {
+ if (!is_callable($code)) {
+ throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.');
+ }
+
+ if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) {
+ $r = new \ReflectionFunction($code);
+ if (null === $r->getClosureThis()) {
+ $code = \Closure::bind($code, $this);
+ }
+ }
+
+ $this->code = $code;
+
+ return $this;
+ }
+
+ /**
+ * 合并参数定义
+ * @param bool $mergeArgs
+ */
+ public function mergeConsoleDefinition($mergeArgs = true)
+ {
+ if (null === $this->console
+ || (true === $this->consoleDefinitionMerged
+ && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs))
+ ) {
+ return;
+ }
+
+ if ($mergeArgs) {
+ $currentArguments = $this->definition->getArguments();
+ $this->definition->setArguments($this->console->getDefinition()->getArguments());
+ $this->definition->addArguments($currentArguments);
+ }
+
+ $this->definition->addOptions($this->console->getDefinition()->getOptions());
+
+ $this->consoleDefinitionMerged = true;
+ if ($mergeArgs) {
+ $this->consoleDefinitionMergedWithArgs = true;
+ }
+ }
+
+ /**
+ * 设置参数定义
+ * @param array|Definition $definition
+ * @return Command
+ * @api
+ */
+ public function setDefinition($definition)
+ {
+ if ($definition instanceof Definition) {
+ $this->definition = $definition;
+ } else {
+ $this->definition->setDefinition($definition);
+ }
+
+ $this->consoleDefinitionMerged = false;
+
+ return $this;
+ }
+
+ /**
+ * 获取参数定义
+ * @return Definition
+ * @api
+ */
+ public function getDefinition()
+ {
+ return $this->definition;
+ }
+
+ /**
+ * 获取当前指令的参数定义
+ * @return Definition
+ */
+ public function getNativeDefinition()
+ {
+ return $this->getDefinition();
+ }
+
+ /**
+ * 添加参数
+ * @param string $name 名称
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addArgument($name, $mode = null, $description = '', $default = null)
+ {
+ $this->definition->addArgument(new Argument($name, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 添加选项
+ * @param string $name 选项名称
+ * @param string $shortcut 别名
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
+ {
+ $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 设置指令名称
+ * @param string $name
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function setName($name)
+ {
+ $this->validateName($name);
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 获取指令名称
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 设置描述
+ * @param string $description
+ * @return Command
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * 设置帮助信息
+ * @param string $help
+ * @return Command
+ */
+ public function setHelp($help)
+ {
+ $this->help = $help;
+
+ return $this;
+ }
+
+ /**
+ * 获取帮助信息
+ * @return string
+ */
+ public function getHelp()
+ {
+ return $this->help;
+ }
+
+
+ /**
+ * 描述信息
+ * @return string
+ */
+ public function getProcessedHelp()
+ {
+ $name = $this->name;
+
+ $placeholders = [
+ '%command.name%',
+ '%command.full_name%',
+ ];
+ $replacements = [
+ $name,
+ $_SERVER['PHP_SELF'] . ' ' . $name,
+ ];
+
+ return str_replace($placeholders, $replacements, $this->getHelp());
+ }
+
+ /**
+ * 设置别名
+ * @param string[] $aliases
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function setAliases($aliases)
+ {
+ if (!is_array($aliases) && !$aliases instanceof \Traversable) {
+ throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable');
+ }
+
+ foreach ($aliases as $alias) {
+ $this->validateName($alias);
+ }
+
+ $this->aliases = $aliases;
+
+ return $this;
+ }
+
+ /**
+ * 获取别名
+ * @return array
+ */
+ public function getAliases()
+ {
+ return $this->aliases;
+ }
+
+ /**
+ * 获取简介
+ * @param bool $short 是否简单的
+ * @return string
+ */
+ public function getSynopsis($short = false)
+ {
+ $key = $short ? 'short' : 'long';
+
+ if (!isset($this->synopsis[$key])) {
+ $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
+ }
+
+ return $this->synopsis[$key];
+ }
+
+ /**
+ * 添加用法介绍
+ * @param string $usage
+ */
+ public function addUsage($usage)
+ {
+ if (0 !== strpos($usage, $this->name)) {
+ $usage = sprintf('%s %s', $this->name, $usage);
+ }
+
+ $this->usages[] = $usage;
+
+ return $this;
+ }
+
+ /**
+ * 获取用法介绍
+ * @return array
+ */
+ public function getUsages()
+ {
+ return $this->usages;
+ }
+
+ /**
+ * 获取助手
+ * @param string $name
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getHelper($name)
+ {
+ return $this->helperSet->get($name);
+ }
+
+ /**
+ * 验证指令名称
+ * @param string $name
+ * @throws \InvalidArgumentException
+ */
+ private function validateName($name)
+ {
+ if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
+ throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/command/Help.php b/library/think/console/command/Help.php
new file mode 100644
index 00000000..a7bcaade
--- /dev/null
+++ b/library/think/console/command/Help.php
@@ -0,0 +1,71 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\helper\Descriptor as DescriptorHelper;
+
+class Help extends Command
+{
+
+ private $command;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->ignoreValidationErrors();
+
+ $this->setName('help')->setDefinition([
+ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
+ ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command:
+
+ php %command.full_name% list
+
+To display the list of available commands, please use the list command.
+EOF
+ );
+ }
+
+ /**
+ * Sets the command.
+ * @param Command $command The command to set
+ */
+ public function setCommand(Command $command)
+ {
+ $this->command = $command;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ if (null === $this->command) {
+ $this->command = $this->getConsole()->find($input->getArgument('command_name'));
+ }
+
+
+ $helper = new DescriptorHelper();
+ $helper->describe($output, $this->command, [
+ 'raw_text' => $input->getOption('raw'),
+ ]);
+
+ $this->command = null;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/command/Lists.php b/library/think/console/command/Lists.php
new file mode 100644
index 00000000..0eac10d5
--- /dev/null
+++ b/library/think/console/command/Lists.php
@@ -0,0 +1,77 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+
+use think\console\Input;
+use think\console\Output;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\input\Definition as InputDefinition;
+use think\console\helper\Descriptor as DescriptorHelper;
+
+class Lists extends Command
+{
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands:
+
+ php %command.full_name%
+
+You can also display the commands for a specific namespace:
+
+ php %command.full_name% test
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ php %command.full_name% --raw
+EOF
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNativeDefinition()
+ {
+ return $this->createDefinition();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+
+ $helper = new DescriptorHelper();
+ $helper->describe($output, $this->getConsole(), [
+ 'raw_text' => $input->getOption('raw'),
+ 'namespace' => $input->getArgument('namespace'),
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function createDefinition()
+ {
+ return new InputDefinition([
+ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list')
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/command/make/Controller.php b/library/think/console/command/make/Controller.php
new file mode 100644
index 00000000..f5014fec
--- /dev/null
+++ b/library/think/console/command/make/Controller.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Command;
+
+class Controller extends Command
+{
+
+ public function __construct()
+ {
+ parent::__construct("make:controller");
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/command/make/Model.php b/library/think/console/command/make/Model.php
new file mode 100644
index 00000000..ef36c18f
--- /dev/null
+++ b/library/think/console/command/make/Model.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+
+use think\console\command\Command;
+
+
+class Model extends Command
+{
+
+ public function __construct()
+ {
+ parent::__construct("make:model");
+ }
+
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Debug.php b/library/think/console/helper/Debug.php
new file mode 100644
index 00000000..9e244657
--- /dev/null
+++ b/library/think/console/helper/Debug.php
@@ -0,0 +1,115 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+
+class Debug extends Helper
+{
+
+ private $colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
+ private $started = [];
+ private $count = -1;
+
+ /**
+ * 启动调试会话的格式
+ * @param string $id 会话的 id
+ * @param string $message 要显示的消息
+ * @param string $prefix 要使用的前缀
+ * @return string
+ */
+ public function start($id, $message, $prefix = 'RUN')
+ {
+ $this->started[$id] = ['border' => ++$this->count % count($this->colors)];
+
+ return sprintf("%s %s > %s>\n", $this->getBorder($id), $prefix, $message);
+ }
+
+ /**
+ * 添加设置会话的进度条格式
+ * @param string $id 会话的 id
+ * @param string $buffer 要显示的消息
+ * @param bool $error 是否输出错误
+ * @param string $prefix 输出的前缀
+ * @param string $errorPrefix 输出错误的前缀
+ * @return string
+ */
+ public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR')
+ {
+ $message = '';
+
+ if ($error) {
+ if (isset($this->started[$id]['out'])) {
+ $message .= "\n";
+ unset($this->started[$id]['out']);
+ }
+ if (!isset($this->started[$id]['err'])) {
+ $message .= sprintf("%s %s > ", $this->getBorder($id), $errorPrefix);
+ $this->started[$id]['err'] = true;
+ }
+
+ $message .= str_replace("\n", sprintf("\n%s %s > ", $this->getBorder($id), $errorPrefix), $buffer);
+ } else {
+ if (isset($this->started[$id]['err'])) {
+ $message .= "\n";
+ unset($this->started[$id]['err']);
+ }
+ if (!isset($this->started[$id]['out'])) {
+ $message .= sprintf("%s %s > ", $this->getBorder($id), $prefix);
+ $this->started[$id]['out'] = true;
+ }
+
+ $message .= str_replace("\n", sprintf("\n%s %s > ", $this->getBorder($id), $prefix), $buffer);
+ }
+
+ return $message;
+ }
+
+ /**
+ * 停止一个会话
+ * @param string $id 会话的 id
+ * @param string $message 要显示的消息
+ * @param bool $successful 是否显示成功消息
+ * @param string $prefix 前缀
+ * @return string
+ */
+ public function stop($id, $message, $successful, $prefix = 'RES')
+ {
+ $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : '';
+
+ if ($successful) {
+ return sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
+ }
+
+ $message = sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
+
+ unset($this->started[$id]['out'], $this->started[$id]['err']);
+
+ return $message;
+ }
+
+ /**
+ * @param string $id
+ * @return string
+ */
+ private function getBorder($id)
+ {
+ return sprintf(' >', $this->colors[$this->started[$id]['border']]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'debug_formatter';
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Descriptor.php b/library/think/console/helper/Descriptor.php
new file mode 100644
index 00000000..18c0eab8
--- /dev/null
+++ b/library/think/console/helper/Descriptor.php
@@ -0,0 +1,54 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+use think\console\helper\descriptor\Descriptor as OutputDescriptor;
+use think\console\Output;
+
+class Descriptor extends Helper
+{
+
+ /**
+ * @var OutputDescriptor
+ */
+ private $descriptor;
+
+ /**
+ * 构造方法
+ */
+ public function __construct()
+ {
+ $this->descriptor = new OutputDescriptor();
+ }
+
+ /**
+ * 描述
+ * @param Output $output
+ * @param object $object
+ * @param array $options
+ * @throws \InvalidArgumentException
+ */
+ public function describe(Output $output, $object, array $options = [])
+ {
+ $options = array_merge([
+ 'raw_text' => false
+ ], $options);
+
+ $this->descriptor->describe($output, $object, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'descriptor';
+ }
+}
diff --git a/library/think/console/helper/Formatter.php b/library/think/console/helper/Formatter.php
new file mode 100644
index 00000000..18ce5742
--- /dev/null
+++ b/library/think/console/helper/Formatter.php
@@ -0,0 +1,74 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+use think\console\output\Formatter as OutputFormatter;
+
+class Formatter extends Helper
+{
+
+ /**
+ * 设置消息在某一节的格式
+ * @param string $section 节名称
+ * @param string $message 消息
+ * @param string $style 样式
+ * @return string
+ */
+ public function formatSection($section, $message, $style = 'info')
+ {
+ return sprintf('<%s>[%s]%s> %s', $style, $section, $style, $message);
+ }
+
+ /**
+ * 设置消息作为文本块的格式
+ * @param string|array $messages 消息
+ * @param string $style 样式
+ * @param bool $large 是否返回一个大段文本
+ * @return string The formatter message
+ */
+ public function formatBlock($messages, $style, $large = false)
+ {
+ if (!is_array($messages)) {
+ $messages = [$messages];
+ }
+
+ $len = 0;
+ $lines = [];
+ foreach ($messages as $message) {
+ $message = OutputFormatter::escape($message);
+ $lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
+ $len = max($this->strlen($message) + ($large ? 4 : 2), $len);
+ }
+
+ $messages = $large ? [str_repeat(' ', $len)] : [];
+ for ($i = 0; isset($lines[$i]); ++$i) {
+ $messages[] = $lines[$i] . str_repeat(' ', $len - $this->strlen($lines[$i]));
+ }
+ if ($large) {
+ $messages[] = str_repeat(' ', $len);
+ }
+
+ for ($i = 0; isset($messages[$i]); ++$i) {
+ $messages[$i] = sprintf('<%s>%s%s>', $style, $messages[$i], $style);
+ }
+
+ return implode("\n", $messages);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'formatter';
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Helper.php b/library/think/console/helper/Helper.php
new file mode 100644
index 00000000..66be1891
--- /dev/null
+++ b/library/think/console/helper/Helper.php
@@ -0,0 +1,121 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+use think\console\helper\Set as HelperSet;
+use think\console\output\Formatter;
+
+abstract class Helper
+{
+
+ protected $helperSet = null;
+
+ /**
+ * 设置与此助手关联的助手集。
+ * @param HelperSet $helperSet
+ */
+ public function setHelperSet(HelperSet $helperSet = null)
+ {
+ $this->helperSet = $helperSet;
+ }
+
+ /**
+ * 获取与此助手关联的助手集。
+ * @return HelperSet
+ */
+ public function getHelperSet()
+ {
+ return $this->helperSet;
+ }
+
+ /**
+ * 获取名称
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * 返回字符串的长度
+ * @param string $string
+ * @return int
+ */
+ public static function strlen($string)
+ {
+ if (!function_exists('mb_strwidth')) {
+ return strlen($string);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return strlen($string);
+ }
+
+ return mb_strwidth($string, $encoding);
+ }
+
+ public static function formatTime($secs)
+ {
+ static $timeFormats = [
+ [0, '< 1 sec'],
+ [2, '1 sec'],
+ [59, 'secs', 1],
+ [60, '1 min'],
+ [3600, 'mins', 60],
+ [5400, '1 hr'],
+ [86400, 'hrs', 3600],
+ [129600, '1 day'],
+ [604800, 'days', 86400],
+ ];
+
+ foreach ($timeFormats as $format) {
+ if ($secs >= $format[0]) {
+ continue;
+ }
+
+ if (2 == count($format)) {
+ return $format[1];
+ }
+
+ return ceil($secs / $format[2]) . ' ' . $format[1];
+ }
+ return null;
+ }
+
+ public static function formatMemory($memory)
+ {
+ if ($memory >= 1024 * 1024 * 1024) {
+ return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
+ }
+
+ if ($memory >= 1024 * 1024) {
+ return sprintf('%.1f MiB', $memory / 1024 / 1024);
+ }
+
+ if ($memory >= 1024) {
+ return sprintf('%d KiB', $memory / 1024);
+ }
+
+ return sprintf('%d B', $memory);
+ }
+
+ public static function strlenWithoutDecoration(Formatter $formatter, $string)
+ {
+ $isDecorated = $formatter->isDecorated();
+ $formatter->setDecorated(false);
+ // remove <...> formatting
+ $string = $formatter->format($string);
+ // remove already formatted characters
+ $string = preg_replace("/\033\[[^m]*m/", '', $string);
+ $formatter->setDecorated($isDecorated);
+
+ return self::strlen($string);
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Process.php b/library/think/console/helper/Process.php
new file mode 100644
index 00000000..67f66307
--- /dev/null
+++ b/library/think/console/helper/Process.php
@@ -0,0 +1,119 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+
+use think\console\Output;
+use think\process\Builder as ProcessBuilder;
+use think\process as ThinkProcess;
+use think\process\exception\Failed as ProcessFailedException;
+
+class Process extends Helper
+{
+
+ /**
+ * 运行一个外部进程。
+ * @param Output $output 一个Output实例
+ * @param string|array|ThinkProcess $cmd 指令
+ * @param string|null $error 错误信息
+ * @param callable|null $callback 回调
+ * @param int $verbosity
+ * @return ThinkProcess
+ */
+ public function run(Output $output, $cmd, $error = null, $callback = null, $verbosity = Output::VERBOSITY_VERY_VERBOSE)
+ {
+ /** @var Debug $formatter */
+ $formatter = $this->getHelperSet()->get('debug_formatter');
+
+ if (is_array($cmd)) {
+ $process = ProcessBuilder::create($cmd)->getProcess();
+ } elseif ($cmd instanceof ThinkProcess) {
+ $process = $cmd;
+ } else {
+ $process = new ThinkProcess($cmd);
+ }
+
+ if ($verbosity <= $output->getVerbosity()) {
+ $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine())));
+ }
+
+ if ($output->isDebug()) {
+ $callback = $this->wrapCallback($output, $process, $callback);
+ }
+
+ $process->run($callback);
+
+ if ($verbosity <= $output->getVerbosity()) {
+ $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode());
+ $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful()));
+ }
+
+ if (!$process->isSuccessful() && null !== $error) {
+ $output->writeln(sprintf('%s', $this->escapeString($error)));
+ }
+
+ return $process;
+ }
+
+ /**
+ * 运行指令
+ * @param Output $output
+ * @param string|ThinkProcess $cmd
+ * @param string|null $error
+ * @param callable|null $callback
+ * @return ThinkProcess
+ */
+ public function mustRun(Output $output, $cmd, $error = null, $callback = null)
+ {
+ $process = $this->run($output, $cmd, $error, $callback);
+
+ if (!$process->isSuccessful()) {
+ throw new ProcessFailedException($process);
+ }
+
+ return $process;
+ }
+
+ /**
+ * 包装过程回调来添加调试输出
+ * @param Output $output
+ * @param ThinkProcess $process
+ * @param callable|null $callback
+ * @return callable
+ */
+ public function wrapCallback(Output $output, ThinkProcess $process, $callback = null)
+ {
+ /** @var Debug $formatter */
+ $formatter = $this->getHelperSet()->get('debug_formatter');
+
+ return function ($type, $buffer) use ($output, $process, $callback, $formatter) {
+ $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), ThinkProcess::ERR === $type));
+
+ if (null !== $callback) {
+ call_user_func($callback, $type, $buffer);
+ }
+ };
+ }
+
+ private function escapeString($str)
+ {
+ return str_replace('<', '\\<', $str);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'process';
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Question.php b/library/think/console/helper/Question.php
new file mode 100644
index 00000000..107ad607
--- /dev/null
+++ b/library/think/console/helper/Question.php
@@ -0,0 +1,395 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+
+use think\console\Input;
+use think\console\Output;
+use think\console\helper\question\Question as OutputQuestion;
+use think\console\helper\question\Choice as ChoiceQuestion;
+use think\console\output\formatter\Style as OutputFormatterStyle;
+
+class Question extends Helper
+{
+
+ private $inputStream;
+ private static $shell;
+ private static $stty;
+
+ /**
+ * 向用户提问
+ * @param Input $input
+ * @param Output $output
+ * @param OutputQuestion $question
+ * @return string
+ */
+ public function ask(Input $input, Output $output, OutputQuestion $question)
+ {
+ if (!$input->isInteractive()) {
+ return $question->getDefault();
+ }
+
+ if (!$question->getValidator()) {
+ return $this->doAsk($output, $question);
+ }
+
+ $interviewer = function () use ($output, $question) {
+ return $this->doAsk($output, $question);
+ };
+
+ return $this->validateAttempts($interviewer, $output, $question);
+ }
+
+ /**
+ * 设置输入流
+ * @param resource $stream
+ * @throws \InvalidArgumentException
+ */
+ public function setInputStream($stream)
+ {
+ if (!is_resource($stream)) {
+ throw new \InvalidArgumentException('Input stream must be a valid resource.');
+ }
+
+ $this->inputStream = $stream;
+ }
+
+ /**
+ * 获取输入流
+ * @return resource
+ */
+ public function getInputStream()
+ {
+ return $this->inputStream;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'question';
+ }
+
+ /**
+ * 提问
+ * @param Output $output
+ * @param OutputQuestion $question
+ * @return bool|mixed|null|string
+ * @throws \Exception
+ * @throws \RuntimeException
+ */
+ private function doAsk(Output $output, OutputQuestion $question)
+ {
+ $this->writePrompt($output, $question);
+
+ $inputStream = $this->inputStream ?: STDIN;
+ $autocomplete = $question->getAutocompleterValues();
+
+ if (null === $autocomplete || !$this->hasSttyAvailable()) {
+ $ret = false;
+ if ($question->isHidden()) {
+ try {
+ $ret = trim($this->getHiddenResponse($output, $inputStream));
+ } catch (\RuntimeException $e) {
+ if (!$question->isHiddenFallback()) {
+ throw $e;
+ }
+ }
+ }
+
+ if (false === $ret) {
+ $ret = fgets($inputStream, 4096);
+ if (false === $ret) {
+ throw new \RuntimeException('Aborted');
+ }
+ $ret = trim($ret);
+ }
+ } else {
+ $ret = trim($this->autocomplete($output, $question, $inputStream));
+ }
+
+ $ret = strlen($ret) > 0 ? $ret : $question->getDefault();
+
+ if ($normalizer = $question->getNormalizer()) {
+ return $normalizer($ret);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * 显示提示
+ * @param Output $output
+ * @param OutputQuestion $question
+ */
+ protected function writePrompt(Output $output, OutputQuestion $question)
+ {
+ $message = $question->getQuestion();
+
+ if ($question instanceof ChoiceQuestion) {
+ $width = max(array_map('strlen', array_keys($question->getChoices())));
+
+ $messages = (array)$question->getQuestion();
+ foreach ($question->getChoices() as $key => $value) {
+ $messages[] = sprintf(" [%-${width}s] %s", $key, $value);
+ }
+
+ $output->writeln($messages);
+
+ $message = $question->getPrompt();
+ }
+
+ $output->write($message);
+ }
+
+ /**
+ * 输出错误
+ * @param Output $output
+ * @param \Exception $error
+ */
+ protected function writeError(Output $output, \Exception $error)
+ {
+ if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
+ $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
+ } else {
+ $message = '' . $error->getMessage() . '';
+ }
+
+ $output->writeln($message);
+ }
+
+ /**
+ * 自动完成问题
+ * @param Output $output
+ * @param OutputQuestion $question
+ * @param $inputStream
+ * @return string
+ */
+ private function autocomplete(Output $output, OutputQuestion $question, $inputStream)
+ {
+ $autocomplete = $question->getAutocompleterValues();
+ $ret = '';
+
+ $i = 0;
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -icanon -echo');
+
+ $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
+
+ while (!feof($inputStream)) {
+ $c = fread($inputStream, 1);
+
+ if ("\177" === $c) {
+ if (0 === $numMatches && 0 !== $i) {
+ $i--;
+ $output->write("\033[1D");
+ }
+
+ if ($i === 0) {
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+ } else {
+ $numMatches = 0;
+ }
+
+ $ret = substr($ret, 0, $i);
+ } elseif ("\033" === $c) {
+ $c .= fread($inputStream, 2);
+
+ if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
+ if ('A' === $c[2] && -1 === $ofs) {
+ $ofs = 0;
+ }
+
+ if (0 === $numMatches) {
+ continue;
+ }
+
+ $ofs += ('A' === $c[2]) ? -1 : 1;
+ $ofs = ($numMatches + $ofs) % $numMatches;
+ }
+ } elseif (ord($c) < 32) {
+ if ("\t" === $c || "\n" === $c) {
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $ret = $matches[$ofs];
+ $output->write(substr($ret, $i));
+ $i = strlen($ret);
+ }
+
+ if ("\n" === $c) {
+ $output->write($c);
+ break;
+ }
+
+ $numMatches = 0;
+ }
+
+ continue;
+ } else {
+ $output->write($c);
+ $ret .= $c;
+ $i++;
+
+ $numMatches = 0;
+ $ofs = 0;
+
+ foreach ($autocomplete as $value) {
+ if (0 === strpos($value, $ret) && $i !== strlen($value)) {
+ $matches[$numMatches++] = $value;
+ }
+ }
+ }
+
+ $output->write("\033[K");
+
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $output->write("\0337");
+ $output->write('' . substr($matches[$ofs], $i) . '');
+ $output->write("\0338");
+ }
+ }
+
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ return $ret;
+ }
+
+ /**
+ * 从用户获取隐藏的响应
+ * @param Output $output
+ * @return string
+ * @throws \RuntimeException
+ */
+ private function getHiddenResponse(Output $output, $inputStream)
+ {
+ if ('\\' === DS) {
+ $exe = __DIR__ . '/../bin/hiddeninput.exe';
+
+ if ('phar:' === substr(__FILE__, 0, 5)) {
+ $tmpExe = sys_get_temp_dir() . '/hiddeninput.exe';
+ copy($exe, $tmpExe);
+ $exe = $tmpExe;
+ }
+
+ $value = rtrim(shell_exec($exe));
+ $output->writeln('');
+
+ if (isset($tmpExe)) {
+ unlink($tmpExe);
+ }
+
+ return $value;
+ }
+
+ if ($this->hasSttyAvailable()) {
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -echo');
+ $value = fgets($inputStream, 4096);
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ if (false === $value) {
+ throw new \RuntimeException('Aborted');
+ }
+
+ $value = trim($value);
+ $output->writeln('');
+
+ return $value;
+ }
+
+ if (false !== $shell = $this->getShell()) {
+ $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
+ $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
+ $value = rtrim(shell_exec($command));
+ $output->writeln('');
+
+ return $value;
+ }
+
+ throw new \RuntimeException('Unable to hide the response.');
+ }
+
+ /**
+ * 验证重试次数
+ * @param callable $interviewer
+ * @param Output $output
+ * @param OutputQuestion $question
+ * @return string
+ * @throws null
+ */
+ private function validateAttempts($interviewer, Output $output, OutputQuestion $question)
+ {
+ $error = null;
+ $attempts = $question->getMaxAttempts();
+ while (null === $attempts || $attempts--) {
+ if (null !== $error) {
+ $this->writeError($output, $error);
+ }
+
+ try {
+ return call_user_func($question->getValidator(), $interviewer());
+ } catch (\Exception $error) {
+ }
+ }
+
+ throw $error;
+ }
+
+ /**
+ * 获取一个有效的 unix 终端。
+ * @return string|bool
+ */
+ private function getShell()
+ {
+ if (null !== self::$shell) {
+ return self::$shell;
+ }
+
+ self::$shell = false;
+
+ if (file_exists('/usr/bin/env')) {
+ // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
+ $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
+ foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
+ if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
+ self::$shell = $sh;
+ break;
+ }
+ }
+ }
+
+ return self::$shell;
+ }
+
+ /**
+ * 检查有用的stty
+ * @return bool
+ */
+ private function hasSttyAvailable()
+ {
+ if (null !== self::$stty) {
+ return self::$stty;
+ }
+
+ exec('stty 2>&1', $output, $exitcode);
+
+ return self::$stty = $exitcode === 0;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/Set.php b/library/think/console/helper/Set.php
new file mode 100644
index 00000000..644649c8
--- /dev/null
+++ b/library/think/console/helper/Set.php
@@ -0,0 +1,100 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper;
+
+
+use think\console\command\Command;
+
+class Set implements \IteratorAggregate
+{
+
+ private $helpers = [];
+ private $command;
+
+ /**
+ * 构造方法
+ * @param Helper[] $helpers 助手实例数组
+ */
+ public function __construct(array $helpers = [])
+ {
+ /**
+ * @var int|string $alias
+ * @var Helper $helper
+ */
+ foreach ($helpers as $alias => $helper) {
+ $this->set($helper, is_int($alias) ? null : $alias);
+ }
+ }
+
+ /**
+ * 添加一个助手
+ * @param Helper $helper 助手实例
+ * @param string $alias 别名
+ */
+ public function set(Helper $helper, $alias = null)
+ {
+ $this->helpers[$helper->getName()] = $helper;
+ if (null !== $alias) {
+ $this->helpers[$alias] = $helper;
+ }
+
+ $helper->setHelperSet($this);
+ }
+
+ /**
+ * 是否有某个助手
+ * @param string $name 助手名称
+ * @return bool
+ */
+ public function has($name)
+ {
+ return isset($this->helpers[$name]);
+ }
+
+ /**
+ * 获取助手
+ * @param string $name 助手名称
+ * @return Helper
+ * @throws \InvalidArgumentException
+ */
+ public function get($name)
+ {
+ if (!$this->has($name)) {
+ throw new \InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
+ }
+
+ return $this->helpers[$name];
+ }
+
+ /**
+ * 设置与这个助手关联的命令集
+ * @param Command $command
+ */
+ public function setCommand(Command $command = null)
+ {
+ $this->command = $command;
+ }
+
+ /**
+ * 获取与这个助手关联的命令集
+ * @return Command
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->helpers);
+ }
+}
diff --git a/library/think/console/helper/descriptor/Console.php b/library/think/console/helper/descriptor/Console.php
new file mode 100644
index 00000000..168c31fd
--- /dev/null
+++ b/library/think/console/helper/descriptor/Console.php
@@ -0,0 +1,150 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper\descriptor;
+
+
+use think\console\command\Command;
+use think\Console as ThinkConsole;
+
+class Console
+{
+
+ const GLOBAL_NAMESPACE = '_global';
+
+ /**
+ * @var ThinkConsole
+ */
+ private $console;
+
+ /**
+ * @var null|string
+ */
+ private $namespace;
+
+ /**
+ * @var array
+ */
+ private $namespaces;
+
+ /**
+ * @var Command[]
+ */
+ private $commands;
+
+ /**
+ * @var Command[]
+ */
+ private $aliases;
+
+ /**
+ * 构造方法
+ * @param ThinkConsole $console
+ * @param string|null $namespace
+ */
+ public function __construct(ThinkConsole $console, $namespace = null)
+ {
+ $this->console = $console;
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @return array
+ */
+ public function getNamespaces()
+ {
+ if (null === $this->namespaces) {
+ $this->inspectConsole();
+ }
+
+ return $this->namespaces;
+ }
+
+ /**
+ * @return Command[]
+ */
+ public function getCommands()
+ {
+ if (null === $this->commands) {
+ $this->inspectConsole();
+ }
+
+ return $this->commands;
+ }
+
+ /**
+ * @param string $name
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function getCommand($name)
+ {
+ if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
+ throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name));
+ }
+
+ return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name];
+ }
+
+ private function inspectConsole()
+ {
+ $this->commands = [];
+ $this->namespaces = [];
+
+ $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null);
+ foreach ($this->sortCommands($all) as $namespace => $commands) {
+ $names = [];
+
+ /** @var Command $command */
+ foreach ($commands as $name => $command) {
+ if (!$command->getName()) {
+ continue;
+ }
+
+ if ($command->getName() === $name) {
+ $this->commands[$name] = $command;
+ } else {
+ $this->aliases[$name] = $command;
+ }
+
+ $names[] = $name;
+ }
+
+ $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
+ }
+ }
+
+ /**
+ * @param array $commands
+ * @return array
+ */
+ private function sortCommands(array $commands)
+ {
+ $namespacedCommands = [];
+ foreach ($commands as $name => $command) {
+ $key = $this->console->extractNamespace($name, 1);
+ if (!$key) {
+ $key = '_global';
+ }
+
+ $namespacedCommands[$key][$name] = $command;
+ }
+ ksort($namespacedCommands);
+
+ foreach ($namespacedCommands as &$commandsSet) {
+ ksort($commandsSet);
+ }
+ // unset reference to keep scope clear
+ unset($commandsSet);
+
+ return $namespacedCommands;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/descriptor/Descriptor.php b/library/think/console/helper/descriptor/Descriptor.php
new file mode 100644
index 00000000..b9a96d87
--- /dev/null
+++ b/library/think/console/helper/descriptor/Descriptor.php
@@ -0,0 +1,319 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper\descriptor;
+
+use think\console\Output;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\input\Definition as InputDefinition;
+use think\console\command\Command;
+use think\Console;
+use think\console\helper\descriptor\Console as ConsoleDescription;
+
+class Descriptor
+{
+
+ /**
+ * @var Output
+ */
+ protected $output;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function describe(Output $output, $object, array $options = [])
+ {
+ $this->output = $output;
+
+ switch (true) {
+ case $object instanceof InputArgument:
+ $this->describeInputArgument($object, $options);
+ break;
+ case $object instanceof InputOption:
+ $this->describeInputOption($object, $options);
+ break;
+ case $object instanceof InputDefinition:
+ $this->describeInputDefinition($object, $options);
+ break;
+ case $object instanceof Command:
+ $this->describeCommand($object, $options);
+ break;
+ case $object instanceof Console:
+ $this->describeConsole($object, $options);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
+ }
+ }
+
+ /**
+ * 输出内容
+ * @param string $content
+ * @param bool $decorated
+ */
+ protected function write($content, $decorated = false)
+ {
+ $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW);
+ }
+
+ /**
+ * 描述参数
+ * @param InputArgument $argument
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputArgument(InputArgument $argument, array $options = [])
+ {
+ if (null !== $argument->getDefault()
+ && (!is_array($argument->getDefault())
+ || count($argument->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName());
+ $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
+
+ $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options);
+ }
+
+ /**
+ * 描述选项
+ * @param InputOption $option
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputOption(InputOption $option, array $options = [])
+ {
+ if ($option->acceptValue() && null !== $option->getDefault()
+ && (!is_array($option->getDefault())
+ || count($option->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = '=' . strtoupper($option->getName());
+
+ if ($option->isValueOptional()) {
+ $value = '[' . $value . ']';
+ }
+ }
+
+ $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
+ $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value));
+
+ $spacingWidth = $totalWidth - strlen($synopsis) + 2;
+
+ $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options);
+ }
+
+ /**
+ * 描述输入
+ * @param InputDefinition $definition
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ {
+ $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
+ foreach ($definition->getArguments() as $argument) {
+ $totalWidth = max($totalWidth, strlen($argument->getName()));
+ }
+
+ if ($definition->getArguments()) {
+ $this->writeText('Arguments:', $options);
+ $this->writeText("\n");
+ foreach ($definition->getArguments() as $argument) {
+ $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
+ $this->writeText("\n");
+ }
+ }
+
+ if ($definition->getArguments() && $definition->getOptions()) {
+ $this->writeText("\n");
+ }
+
+ if ($definition->getOptions()) {
+ $laterOptions = [];
+
+ $this->writeText('Options:', $options);
+ foreach ($definition->getOptions() as $option) {
+ if (strlen($option->getShortcut()) > 1) {
+ $laterOptions[] = $option;
+ continue;
+ }
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ foreach ($laterOptions as $option) {
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ }
+ }
+
+ /**
+ * 描述指令
+ * @param Command $command
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeCommand(Command $command, array $options = [])
+ {
+ $command->getSynopsis(true);
+ $command->getSynopsis(false);
+ $command->mergeConsoleDefinition(false);
+
+ $this->writeText('Usage:', $options);
+ foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $usage, $options);
+ }
+ $this->writeText("\n");
+
+ $definition = $command->getNativeDefinition();
+ if ($definition->getOptions() || $definition->getArguments()) {
+ $this->writeText("\n");
+ $this->describeInputDefinition($definition, $options);
+ $this->writeText("\n");
+ }
+
+ if ($help = $command->getProcessedHelp()) {
+ $this->writeText("\n");
+ $this->writeText('Help:', $options);
+ $this->writeText("\n");
+ $this->writeText(' ' . str_replace("\n", "\n ", $help), $options);
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * 描述控制台
+ * @param Console $console
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeConsole(Console $console, array $options = [])
+ {
+ $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
+ $description = new ConsoleDescription($console, $describedNamespace);
+
+ if (isset($options['raw_text']) && $options['raw_text']) {
+ $width = $this->getColumnWidth($description->getCommands());
+
+ foreach ($description->getCommands() as $command) {
+ $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options);
+ $this->writeText("\n");
+ }
+ } else {
+ if ('' != $help = $console->getHelp()) {
+ $this->writeText("$help\n\n", $options);
+ }
+
+ $this->writeText("Usage:\n", $options);
+ $this->writeText(" command [options] [arguments]\n\n", $options);
+
+ $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options);
+
+ $this->writeText("\n");
+ $this->writeText("\n");
+
+ $width = $this->getColumnWidth($description->getCommands());
+
+ if ($describedNamespace) {
+ $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options);
+ } else {
+ $this->writeText('Available commands:', $options);
+ }
+
+ // add commands by namespace
+ foreach ($description->getNamespaces() as $namespace) {
+ if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $namespace['id'] . '', $options);
+ }
+
+ foreach ($namespace['commands'] as $name) {
+ $this->writeText("\n");
+ $spacingWidth = $width - strlen($name);
+ $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)
+ ->getDescription()), $options);
+ }
+ }
+
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function writeText($content, array $options = [])
+ {
+ $this->write(isset($options['raw_text'])
+ && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true);
+ }
+
+ /**
+ * 格式化
+ * @param mixed $default
+ * @return string
+ */
+ private function formatDefaultValue($default)
+ {
+ return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ }
+
+ /**
+ * @param Command[] $commands
+ * @return int
+ */
+ private function getColumnWidth(array $commands)
+ {
+ $width = 0;
+ foreach ($commands as $command) {
+ $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width;
+ }
+
+ return $width + 2;
+ }
+
+ /**
+ * @param InputOption[] $options
+ * @return int
+ */
+ private function calculateTotalWidthForOptions($options)
+ {
+ $totalWidth = 0;
+ foreach ($options as $option) {
+ $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
+
+ if ($option->acceptValue()) {
+ $valueLength = 1 + strlen($option->getName()); // = + value
+ $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
+
+ $nameLength += $valueLength;
+ }
+ $totalWidth = max($totalWidth, $nameLength);
+ }
+
+ return $totalWidth;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/question/Choice.php b/library/think/console/helper/question/Choice.php
new file mode 100644
index 00000000..8badcd7b
--- /dev/null
+++ b/library/think/console/helper/question/Choice.php
@@ -0,0 +1,157 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper\question;
+
+
+class Choice extends Question
+{
+
+ private $choices;
+ private $multiselect = false;
+ private $prompt = ' > ';
+ private $errorMessage = 'Value "%s" is invalid';
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param array $choices 选项
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, array $choices, $default = null)
+ {
+ parent::__construct($question, $default);
+
+ $this->choices = $choices;
+ $this->setValidator($this->getDefaultValidator());
+ $this->setAutocompleterValues($choices);
+ }
+
+ /**
+ * 可选项
+ * @return array
+ */
+ public function getChoices()
+ {
+ return $this->choices;
+ }
+
+ /**
+ * 设置可否多选
+ * @param bool $multiselect
+ * @return self
+ */
+ public function setMultiselect($multiselect)
+ {
+ $this->multiselect = $multiselect;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ /**
+ * 获取提示
+ * @return string
+ */
+ public function getPrompt()
+ {
+ return $this->prompt;
+ }
+
+ /**
+ * 设置提示
+ * @param string $prompt
+ * @return self
+ */
+ public function setPrompt($prompt)
+ {
+ $this->prompt = $prompt;
+
+ return $this;
+ }
+
+ /**
+ * 设置错误提示信息
+ * @param string $errorMessage
+ * @return self
+ */
+ public function setErrorMessage($errorMessage)
+ {
+ $this->errorMessage = $errorMessage;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ /**
+ * 获取默认的验证方法
+ * @return callable
+ */
+ private function getDefaultValidator()
+ {
+ $choices = $this->choices;
+ $errorMessage = $this->errorMessage;
+ $multiselect = $this->multiselect;
+ $isAssoc = $this->isAssoc($choices);
+
+ return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
+ // Collapse all spaces.
+ $selectedChoices = str_replace(' ', '', $selected);
+
+ if ($multiselect) {
+ // Check for a separated comma values
+ if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $selected));
+ }
+ $selectedChoices = explode(',', $selectedChoices);
+ } else {
+ $selectedChoices = [$selected];
+ }
+
+ $multiselectChoices = [];
+ foreach ($selectedChoices as $value) {
+ $results = [];
+ foreach ($choices as $key => $choice) {
+ if ($choice === $value) {
+ $results[] = $key;
+ }
+ }
+
+ if (count($results) > 1) {
+ throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results)));
+ }
+
+ $result = array_search($value, $choices);
+
+ if (!$isAssoc) {
+ if (!empty($result)) {
+ $result = $choices[$result];
+ } elseif (isset($choices[$value])) {
+ $result = $choices[$value];
+ }
+ } elseif (empty($result) && array_key_exists($value, $choices)) {
+ $result = $value;
+ }
+
+ if (empty($result)) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $value));
+ }
+ array_push($multiselectChoices, $result);
+ }
+
+ if ($multiselect) {
+ return $multiselectChoices;
+ }
+
+ return current($multiselectChoices);
+ };
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/question/Confirmation.php b/library/think/console/helper/question/Confirmation.php
new file mode 100644
index 00000000..e67fd439
--- /dev/null
+++ b/library/think/console/helper/question/Confirmation.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper\question;
+
+
+class Confirmation extends Question
+{
+
+ private $trueAnswerRegex;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param bool $default 默认答案
+ * @param string $trueAnswerRegex 验证正则
+ */
+ public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i')
+ {
+ parent::__construct($question, (bool)$default);
+
+ $this->trueAnswerRegex = $trueAnswerRegex;
+ $this->setNormalizer($this->getDefaultNormalizer());
+ }
+
+ /**
+ * 获取默认的答案回调
+ * @return callable
+ */
+ private function getDefaultNormalizer()
+ {
+ $default = $this->getDefault();
+ $regex = $this->trueAnswerRegex;
+
+ return function ($answer) use ($default, $regex) {
+ if (is_bool($answer)) {
+ return $answer;
+ }
+
+ $answerIsTrue = (bool)preg_match($regex, $answer);
+ if (false === $default) {
+ return $answer && $answerIsTrue;
+ }
+
+ return !$answer || $answerIsTrue;
+ };
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/helper/question/Question.php b/library/think/console/helper/question/Question.php
new file mode 100644
index 00000000..0a0cbce4
--- /dev/null
+++ b/library/think/console/helper/question/Question.php
@@ -0,0 +1,211 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\helper\question;
+
+class Question
+{
+
+ private $question;
+ private $attempts;
+ private $hidden = false;
+ private $hiddenFallback = true;
+ private $autocompleterValues;
+ private $validator;
+ private $default;
+ private $normalizer;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, $default = null)
+ {
+ $this->question = $question;
+ $this->default = $default;
+ }
+
+ /**
+ * 获取问题
+ * @return string
+ */
+ public function getQuestion()
+ {
+ return $this->question;
+ }
+
+ /**
+ * 获取默认答案
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 是否隐藏答案
+ * @return bool
+ */
+ public function isHidden()
+ {
+ return $this->hidden;
+ }
+
+ /**
+ * 隐藏答案
+ * @param bool $hidden
+ * @return Question
+ */
+ public function setHidden($hidden)
+ {
+ if ($this->autocompleterValues) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->hidden = (bool)$hidden;
+
+ return $this;
+ }
+
+ /**
+ * 不能被隐藏是否撤销
+ * @return bool
+ */
+ public function isHiddenFallback()
+ {
+ return $this->hiddenFallback;
+ }
+
+ /**
+ * 设置不能被隐藏的时候的操作
+ * @param bool $fallback
+ * @return Question
+ */
+ public function setHiddenFallback($fallback)
+ {
+ $this->hiddenFallback = (bool)$fallback;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动完成
+ * @return null|array|\Traversable
+ */
+ public function getAutocompleterValues()
+ {
+ return $this->autocompleterValues;
+ }
+
+ /**
+ * 设置自动完成的值
+ * @param null|array|\Traversable $values
+ * @return Question
+ * @throws \InvalidArgumentException
+ * @throws \LogicException
+ */
+ public function setAutocompleterValues($values)
+ {
+ if (is_array($values) && $this->isAssoc($values)) {
+ $values = array_merge(array_keys($values), array_values($values));
+ }
+
+ if (null !== $values && !is_array($values)) {
+ if (!$values instanceof \Traversable || $values instanceof \Countable) {
+ throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.');
+ }
+ }
+
+ if ($this->hidden) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->autocompleterValues = $values;
+
+ return $this;
+ }
+
+ /**
+ * 设置答案的验证器
+ * @param null|callable $validator
+ * @return Question The current instance
+ */
+ public function setValidator($validator)
+ {
+ $this->validator = $validator;
+
+ return $this;
+ }
+
+ /**
+ * 获取验证器
+ * @return null|callable
+ */
+ public function getValidator()
+ {
+ return $this->validator;
+ }
+
+ /**
+ * 设置最大重试次数
+ * @param null|int $attempts
+ * @return Question
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxAttempts($attempts)
+ {
+ if (null !== $attempts && $attempts < 1) {
+ throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.');
+ }
+
+ $this->attempts = $attempts;
+
+ return $this;
+ }
+
+ /**
+ * 获取最大重试次数
+ * @return null|int
+ */
+ public function getMaxAttempts()
+ {
+ return $this->attempts;
+ }
+
+ /**
+ * 设置响应的回调
+ * @param string|\Closure $normalizer
+ * @return Question
+ */
+ public function setNormalizer($normalizer)
+ {
+ $this->normalizer = $normalizer;
+
+ return $this;
+ }
+
+ /**
+ * 获取相应回调
+ * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
+ * @return string|\Closure
+ */
+ public function getNormalizer()
+ {
+ return $this->normalizer;
+ }
+
+ protected function isAssoc($array)
+ {
+ return (bool)count(array_filter(array_keys($array), 'is_string'));
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/input/Argument.php b/library/think/console/input/Argument.php
new file mode 100644
index 00000000..aa8c6768
--- /dev/null
+++ b/library/think/console/input/Argument.php
@@ -0,0 +1,116 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+
+class Argument
+{
+
+ const REQUIRED = 1;
+ const OPTIONAL = 2;
+ const IS_ARRAY = 4;
+
+ private $name;
+ private $mode;
+ private $default;
+ private $description;
+
+ /**
+ * 构造方法
+ * @param string $name 参数名
+ * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL
+ * @param string $description 描述
+ * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($name, $mode = null, $description = '', $default = null)
+ {
+ if (null === $mode) {
+ $mode = self::OPTIONAL;
+ } elseif (!is_int($mode) || $mode > 7 || $mode < 1) {
+ throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
+ }
+
+ $this->name = $name;
+ $this->mode = $mode;
+ $this->description = $description;
+
+ $this->setDefault($default);
+ }
+
+ /**
+ * 获取参数名
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否必须
+ * @return bool
+ */
+ public function isRequired()
+ {
+ return self::REQUIRED === (self::REQUIRED & $this->mode);
+ }
+
+ /**
+ * 该参数是否接受数组
+ * @return bool
+ */
+ public function isArray()
+ {
+ return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
+ }
+
+ /**
+ * 设置默认值
+ * @param mixed $default 默认值
+ * @throws \LogicException
+ */
+ public function setDefault($default = null)
+ {
+ if (self::REQUIRED === $this->mode && null !== $default) {
+ throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
+ }
+
+ if ($this->isArray()) {
+ if (null === $default) {
+ $default = [];
+ } elseif (!is_array($default)) {
+ throw new \LogicException('A default value for an array argument must be an array.');
+ }
+ }
+
+ $this->default = $default;
+ }
+
+ /**
+ * 获取默认值
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/input/Definition.php b/library/think/console/input/Definition.php
new file mode 100644
index 00000000..bfff210b
--- /dev/null
+++ b/library/think/console/input/Definition.php
@@ -0,0 +1,376 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+
+class Definition
+{
+
+ /**
+ * @var Argument[]
+ */
+ private $arguments;
+
+ private $requiredCount;
+ private $hasAnArrayArgument = false;
+ private $hasOptional;
+
+ /**
+ * @var Option[]
+ */
+ private $options;
+ private $shortcuts;
+
+ /**
+ * 构造方法
+ * @param array $definition
+ * @api
+ */
+ public function __construct(array $definition = [])
+ {
+ $this->setDefinition($definition);
+ }
+
+ /**
+ * 设置指令的定义
+ * @param array $definition 定义的数组
+ */
+ public function setDefinition(array $definition)
+ {
+ $arguments = [];
+ $options = [];
+ foreach ($definition as $item) {
+ if ($item instanceof Option) {
+ $options[] = $item;
+ } else {
+ $arguments[] = $item;
+ }
+ }
+
+ $this->setArguments($arguments);
+ $this->setOptions($options);
+ }
+
+ /**
+ * 设置参数
+ * @param Argument[] $arguments 参数数组
+ */
+ public function setArguments($arguments = [])
+ {
+ $this->arguments = [];
+ $this->requiredCount = 0;
+ $this->hasOptional = false;
+ $this->hasAnArrayArgument = false;
+ $this->addArguments($arguments);
+ }
+
+ /**
+ * 添加参数
+ * @param Argument[] $arguments 参数数组
+ * @api
+ */
+ public function addArguments($arguments = [])
+ {
+ if (null !== $arguments) {
+ foreach ($arguments as $argument) {
+ $this->addArgument($argument);
+ }
+ }
+ }
+
+ /**
+ * 添加一个参数
+ * @param Argument $argument 参数
+ * @throws \LogicException
+ */
+ public function addArgument(Argument $argument)
+ {
+ if (isset($this->arguments[$argument->getName()])) {
+ throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
+ }
+
+ if ($this->hasAnArrayArgument) {
+ throw new \LogicException('Cannot add an argument after an array argument.');
+ }
+
+ if ($argument->isRequired() && $this->hasOptional) {
+ throw new \LogicException('Cannot add a required argument after an optional one.');
+ }
+
+ if ($argument->isArray()) {
+ $this->hasAnArrayArgument = true;
+ }
+
+ if ($argument->isRequired()) {
+ ++$this->requiredCount;
+ } else {
+ $this->hasOptional = true;
+ }
+
+ $this->arguments[$argument->getName()] = $argument;
+ }
+
+ /**
+ * 根据名称或者位置获取参数
+ * @param string|int $name 参数名或者位置
+ * @return Argument 参数
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument($name)
+ {
+ if (!$this->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return $arguments[$name];
+ }
+
+ /**
+ * 根据名称或位置检查是否具有某个参数
+ * @param string|int $name 参数名或者位置
+ * @return bool
+ * @api
+ */
+ public function hasArgument($name)
+ {
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return isset($arguments[$name]);
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[] 参数数组
+ */
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * 获取参数数量
+ * @return int
+ */
+ public function getArgumentCount()
+ {
+ return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments);
+ }
+
+ /**
+ * 获取必填的参数的数量
+ * @return int
+ */
+ public function getArgumentRequiredCount()
+ {
+ return $this->requiredCount;
+ }
+
+ /**
+ * 获取参数默认值
+ * @return array
+ */
+ public function getArgumentDefaults()
+ {
+ $values = [];
+ foreach ($this->arguments as $argument) {
+ $values[$argument->getName()] = $argument->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 设置选项
+ * @param Option[] $options 选项数组
+ */
+ public function setOptions($options = [])
+ {
+ $this->options = [];
+ $this->shortcuts = [];
+ $this->addOptions($options);
+ }
+
+ /**
+ * 添加选项
+ * @param Option[] $options 选项数组
+ * @api
+ */
+ public function addOptions($options = [])
+ {
+ foreach ($options as $option) {
+ $this->addOption($option);
+ }
+ }
+
+ /**
+ * 添加一个选项
+ * @param Option $option 选项
+ * @throws \LogicException
+ * @api
+ */
+ public function addOption(Option $option)
+ {
+ if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
+ throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+ }
+
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ if (isset($this->shortcuts[$shortcut])
+ && !$option->equals($this->options[$this->shortcuts[$shortcut]])
+ ) {
+ throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
+ }
+ }
+ }
+
+ $this->options[$option->getName()] = $option;
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ $this->shortcuts[$shortcut] = $option->getName();
+ }
+ }
+ }
+
+ /**
+ * 根据名称获取选项
+ * @param string $name 选项名
+ * @return Option
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function getOption($name)
+ {
+ if (!$this->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ return $this->options[$name];
+ }
+
+ /**
+ * 根据名称检查是否有这个选项
+ * @param string $name 选项名
+ * @return bool
+ * @api
+ */
+ public function hasOption($name)
+ {
+ return isset($this->options[$name]);
+ }
+
+ /**
+ * 获取所有选项
+ * @return Option[]
+ * @api
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * 根据名称检查某个选项是否有短名称
+ * @param string $name 短名称
+ * @return bool
+ */
+ public function hasShortcut($name)
+ {
+ return isset($this->shortcuts[$name]);
+ }
+
+ /**
+ * 根据短名称获取选项
+ * @param string $shortcut 短名称
+ * @return Option
+ */
+ public function getOptionForShortcut($shortcut)
+ {
+ return $this->getOption($this->shortcutToName($shortcut));
+ }
+
+ /**
+ * 获取所有选项的默认值
+ * @return array
+ */
+ public function getOptionDefaults()
+ {
+ $values = [];
+ foreach ($this->options as $option) {
+ $values[$option->getName()] = $option->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 根据短名称获取选项名
+ * @param string $shortcut 短名称
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ private function shortcutToName($shortcut)
+ {
+ if (!isset($this->shortcuts[$shortcut])) {
+ throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ return $this->shortcuts[$shortcut];
+ }
+
+ /**
+ * 获取该指令的介绍
+ * @param bool $short 是否简洁介绍
+ * @return string
+ */
+ public function getSynopsis($short = false)
+ {
+ $elements = [];
+
+ if ($short && $this->getOptions()) {
+ $elements[] = '[options]';
+ } elseif (!$short) {
+ foreach ($this->getOptions() as $option) {
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '');
+ }
+
+ $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
+ $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
+ }
+ }
+
+ if (count($elements) && $this->getArguments()) {
+ $elements[] = '[--]';
+ }
+
+ foreach ($this->getArguments() as $argument) {
+ $element = '<' . $argument->getName() . '>';
+ if (!$argument->isRequired()) {
+ $element = '[' . $element . ']';
+ } elseif ($argument->isArray()) {
+ $element = $element . ' (' . $element . ')';
+ }
+
+ if ($argument->isArray()) {
+ $element .= '...';
+ }
+
+ $elements[] = $element;
+ }
+
+ return implode(' ', $elements);
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/input/Option.php b/library/think/console/input/Option.php
new file mode 100644
index 00000000..4406b278
--- /dev/null
+++ b/library/think/console/input/Option.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Option
+{
+
+ const VALUE_NONE = 1;
+ const VALUE_REQUIRED = 2;
+ const VALUE_OPTIONAL = 4;
+ const VALUE_IS_ARRAY = 8;
+
+ private $name;
+ private $shortcut;
+ private $mode;
+ private $default;
+ private $description;
+
+ /**
+ * 构造方法
+ * @param string $name 选项名
+ * @param string|array $shortcut 短名称,多个用|隔开或者使用数组
+ * @param int $mode 选项类型(可选类型为 self::VALUE_*)
+ * @param string $description 描述
+ * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null)
+ {
+ if (0 === strpos($name, '--')) {
+ $name = substr($name, 2);
+ }
+
+ if (empty($name)) {
+ throw new \InvalidArgumentException('An option name cannot be empty.');
+ }
+
+ if (empty($shortcut)) {
+ $shortcut = null;
+ }
+
+ if (null !== $shortcut) {
+ if (is_array($shortcut)) {
+ $shortcut = implode('|', $shortcut);
+ }
+ $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
+ $shortcuts = array_filter($shortcuts);
+ $shortcut = implode('|', $shortcuts);
+
+ if (empty($shortcut)) {
+ throw new \InvalidArgumentException('An option shortcut cannot be empty.');
+ }
+ }
+
+ if (null === $mode) {
+ $mode = self::VALUE_NONE;
+ } elseif (!is_int($mode) || $mode > 15 || $mode < 1) {
+ throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
+ }
+
+ $this->name = $name;
+ $this->shortcut = $shortcut;
+ $this->mode = $mode;
+ $this->description = $description;
+
+ if ($this->isArray() && !$this->acceptValue()) {
+ throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
+ }
+
+ $this->setDefault($default);
+ }
+
+ /**
+ * 获取短名称
+ * @return string
+ */
+ public function getShortcut()
+ {
+ return $this->shortcut;
+ }
+
+ /**
+ * 获取选项名
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否可以设置值
+ * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false
+ */
+ public function acceptValue()
+ {
+ return $this->isValueRequired() || $this->isValueOptional();
+ }
+
+ /**
+ * 是否必须
+ * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false
+ */
+ public function isValueRequired()
+ {
+ return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
+ }
+
+ /**
+ * 是否可选
+ * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false
+ */
+ public function isValueOptional()
+ {
+ return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
+ }
+
+ /**
+ * 选项值是否接受数组
+ * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false
+ */
+ public function isArray()
+ {
+ return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
+ }
+
+ /**
+ * 设置默认值
+ * @param mixed $default 默认值
+ * @throws \LogicException
+ */
+ public function setDefault($default = null)
+ {
+ if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
+ throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
+ }
+
+ if ($this->isArray()) {
+ if (null === $default) {
+ $default = [];
+ } elseif (!is_array($default)) {
+ throw new \LogicException('A default value for an array option must be an array.');
+ }
+ }
+
+ $this->default = $this->acceptValue() ? $default : false;
+ }
+
+ /**
+ * 获取默认值
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 获取描述文字
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * 检查所给选项是否是当前这个
+ * @param Option $option
+ * @return bool
+ */
+ public function equals(Option $option)
+ {
+ return $option->getName() === $this->getName()
+ && $option->getShortcut() === $this->getShortcut()
+ && $option->getDefault() === $this->getDefault()
+ && $option->isArray() === $this->isArray()
+ && $option->isValueRequired() === $this->isValueRequired()
+ && $option->isValueOptional() === $this->isValueOptional();
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/output/Formatter.php b/library/think/console/output/Formatter.php
new file mode 100644
index 00000000..4179680e
--- /dev/null
+++ b/library/think/console/output/Formatter.php
@@ -0,0 +1,196 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\output;
+
+use think\console\output\formatter\Style;
+use think\console\output\formatter\Stack as StyleStack;
+
+class Formatter
+{
+
+ private $decorated = false;
+ private $styles = [];
+ private $styleStack;
+
+ /**
+ * 转义
+ * @param string $text
+ * @return string
+ */
+ public static function escape($text)
+ {
+ return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red'));
+ $this->setStyle('info', new Style('green'));
+ $this->setStyle('comment', new Style('yellow'));
+ $this->setStyle('question', new Style('black', 'cyan'));
+
+ $this->styleStack = new StyleStack();
+ }
+
+ /**
+ * 设置外观标识
+ * @param bool $decorated 是否美化文职
+ */
+ public function setDecorated($decorated)
+ {
+ $this->decorated = (bool)$decorated;
+ }
+
+ /**
+ * 获取外观标识
+ * @return bool
+ */
+ public function isDecorated()
+ {
+ return $this->decorated;
+ }
+
+ /**
+ * 添加一个新样式
+ * @param string $name 样式名
+ * @param Style $style 样式实例
+ */
+ public function setStyle($name, Style $style)
+ {
+ $this->styles[strtolower($name)] = $style;
+ }
+
+ /**
+ * 是否有这个样式
+ * @param string $name
+ * @return bool
+ */
+ public function hasStyle($name)
+ {
+ return isset($this->styles[strtolower($name)]);
+ }
+
+ /**
+ * 获取样式
+ * @param string $name
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function getStyle($name)
+ {
+ if (!$this->hasStyle($name)) {
+ throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name));
+ }
+
+ return $this->styles[strtolower($name)];
+ }
+
+ /**
+ * 使用所给的样式格式化文字
+ * @param string $message 文字
+ * @return string
+ */
+ public function format($message)
+ {
+ $offset = 0;
+ $output = '';
+ $tagRegex = '[a-z][a-z0-9_=;-]*';
+ preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE);
+ foreach ($matches[0] as $i => $match) {
+ $pos = $match[1];
+ $text = $match[0];
+
+ if (0 != $pos && '\\' == $message[$pos - 1]) {
+ continue;
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset));
+ $offset = $pos + strlen($text);
+
+ if ($open = '/' != $text[1]) {
+ $tag = $matches[1][$i][0];
+ } else {
+ $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : '';
+ }
+
+ if (!$open && !$tag) {
+ // >
+ $this->styleStack->pop();
+ } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) {
+ $output .= $this->applyCurrentStyle($text);
+ } elseif ($open) {
+ $this->styleStack->push($style);
+ } else {
+ $this->styleStack->pop($style);
+ }
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset));
+
+ return str_replace('\\<', '<', $output);
+ }
+
+ /**
+ * @return StyleStack
+ */
+ public function getStyleStack()
+ {
+ return $this->styleStack;
+ }
+
+ /**
+ * 根据字符串创建新的样式实例
+ * @param string $string
+ * @return Style|bool
+ */
+ private function createStyleFromString($string)
+ {
+ if (isset($this->styles[$string])) {
+ return $this->styles[$string];
+ }
+
+ if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) {
+ return false;
+ }
+
+ $style = new Style();
+ foreach ($matches as $match) {
+ array_shift($match);
+
+ if ('fg' == $match[0]) {
+ $style->setForeground($match[1]);
+ } elseif ('bg' == $match[0]) {
+ $style->setBackground($match[1]);
+ } else {
+ try {
+ $style->setOption($match[1]);
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+ }
+ }
+
+ return $style;
+ }
+
+ /**
+ * 从堆栈应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ private function applyCurrentStyle($text)
+ {
+ return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/output/Stream.php b/library/think/console/output/Stream.php
new file mode 100644
index 00000000..f66f0775
--- /dev/null
+++ b/library/think/console/output/Stream.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+
+class Stream
+{
+
+ const VERBOSITY_QUIET = 0;
+ const VERBOSITY_NORMAL = 1;
+ const VERBOSITY_VERBOSE = 2;
+ const VERBOSITY_VERY_VERBOSE = 3;
+ const VERBOSITY_DEBUG = 4;
+
+ const OUTPUT_NORMAL = 0;
+ const OUTPUT_RAW = 1;
+ const OUTPUT_PLAIN = 2;
+
+ private $verbosity = self::VERBOSITY_NORMAL;
+ private $formatter;
+
+
+ private $stream;
+
+ /**
+ * 构造方法
+ */
+ public function __construct($stream, Formatter $formatter = null)
+ {
+ if (!is_resource($stream) || 'stream' !== get_resource_type($stream)) {
+ throw new \InvalidArgumentException('The StreamOutput class needs a stream as its first argument.');
+ }
+
+ $this->stream = $stream;
+
+ $decorated = $this->hasColorSupport();
+
+ $this->formatter = $formatter ?: new Formatter();
+ $this->formatter->setDecorated($decorated);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFormatter(Formatter $formatter)
+ {
+ $this->formatter = $formatter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormatter()
+ {
+ return $this->formatter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDecorated($decorated)
+ {
+ $this->formatter->setDecorated($decorated);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDecorated()
+ {
+ return $this->formatter->isDecorated();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setVerbosity($level)
+ {
+ $this->verbosity = (int)$level;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getVerbosity()
+ {
+ return $this->verbosity;
+ }
+
+ public function isQuiet()
+ {
+ return self::VERBOSITY_QUIET === $this->verbosity;
+ }
+
+ public function isVerbose()
+ {
+ return self::VERBOSITY_VERBOSE <= $this->verbosity;
+ }
+
+ public function isVeryVerbose()
+ {
+ return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
+ }
+
+ public function isDebug()
+ {
+ return self::VERBOSITY_DEBUG <= $this->verbosity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeln($messages, $type = self::OUTPUT_NORMAL)
+ {
+ $this->write($messages, true, $type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
+ {
+ if (self::VERBOSITY_QUIET === $this->verbosity) {
+ return;
+ }
+
+ $messages = (array)$messages;
+
+ foreach ($messages as $message) {
+ switch ($type) {
+ case self::OUTPUT_NORMAL:
+ $message = $this->formatter->format($message);
+ break;
+ case self::OUTPUT_RAW:
+ break;
+ case self::OUTPUT_PLAIN:
+ $message = strip_tags($this->formatter->format($message));
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
+ }
+
+ $this->doWrite($message, $newline);
+ }
+ }
+
+ /**
+ * 将消息写入到输出。
+ * @param string $message 消息
+ * @param bool $newline 是否另起一行
+ */
+ protected function doWrite($message, $newline)
+ {
+ if (false === @fwrite($this->stream, $message . ($newline ? PHP_EOL : ''))) {
+ throw new \RuntimeException('Unable to write output.');
+ }
+
+ fflush($this->stream);
+ }
+
+ /**
+ * @return resource
+ */
+ public function getStream()
+ {
+ return $this->stream;
+ }
+
+ /**
+ * 是否支持着色
+ * @return bool
+ */
+ protected function hasColorSupport()
+ {
+ if (DIRECTORY_SEPARATOR == '\\') {
+ return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
+ }
+
+ return function_exists('posix_isatty') && @posix_isatty($this->stream);
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/output/formatter/Stack.php b/library/think/console/output/formatter/Stack.php
new file mode 100644
index 00000000..8e85e4f1
--- /dev/null
+++ b/library/think/console/output/formatter/Stack.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+
+class Stack
+{
+
+ /**
+ * @var Style[]
+ */
+ private $styles;
+
+ /**
+ * @var Style
+ */
+ private $emptyStyle;
+
+ /**
+ * 构造方法
+ * @param Style|null $emptyStyle
+ */
+ public function __construct(Style $emptyStyle = null)
+ {
+ $this->emptyStyle = $emptyStyle ?: new Style();
+ $this->reset();
+ }
+
+ /**
+ * 重置堆栈
+ */
+ public function reset()
+ {
+ $this->styles = [];
+ }
+
+ /**
+ * 推一个样式进入堆栈
+ * @param Style $style
+ */
+ public function push(Style $style)
+ {
+ $this->styles[] = $style;
+ }
+
+ /**
+ * 从堆栈中弹出一个样式
+ * @param Style|null $style
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function pop(Style $style = null)
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ if (null === $style) {
+ return array_pop($this->styles);
+ }
+
+ /**
+ * @var int $index
+ * @var Style $stackedStyle
+ */
+ foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
+ if ($style->apply('') === $stackedStyle->apply('')) {
+ $this->styles = array_slice($this->styles, 0, $index);
+
+ return $stackedStyle;
+ }
+ }
+
+ throw new \InvalidArgumentException('Incorrectly nested style tag found.');
+ }
+
+ /**
+ * 计算堆栈的当前样式。
+ * @return Style
+ */
+ public function getCurrent()
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ return $this->styles[count($this->styles) - 1];
+ }
+
+ /**
+ * @param Style $emptyStyle
+ * @return Stack
+ */
+ public function setEmptyStyle(Style $emptyStyle)
+ {
+ $this->emptyStyle = $emptyStyle;
+
+ return $this;
+ }
+
+ /**
+ * @return Style
+ */
+ public function getEmptyStyle()
+ {
+ return $this->emptyStyle;
+ }
+}
\ No newline at end of file
diff --git a/library/think/console/output/formatter/Style.php b/library/think/console/output/formatter/Style.php
new file mode 100644
index 00000000..1ffbd811
--- /dev/null
+++ b/library/think/console/output/formatter/Style.php
@@ -0,0 +1,189 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Style
+{
+
+ private static $availableForegroundColors = [
+ 'black' => ['set' => 30, 'unset' => 39],
+ 'red' => ['set' => 31, 'unset' => 39],
+ 'green' => ['set' => 32, 'unset' => 39],
+ 'yellow' => ['set' => 33, 'unset' => 39],
+ 'blue' => ['set' => 34, 'unset' => 39],
+ 'magenta' => ['set' => 35, 'unset' => 39],
+ 'cyan' => ['set' => 36, 'unset' => 39],
+ 'white' => ['set' => 37, 'unset' => 39],
+ ];
+ private static $availableBackgroundColors = [
+ 'black' => ['set' => 40, 'unset' => 49],
+ 'red' => ['set' => 41, 'unset' => 49],
+ 'green' => ['set' => 42, 'unset' => 49],
+ 'yellow' => ['set' => 43, 'unset' => 49],
+ 'blue' => ['set' => 44, 'unset' => 49],
+ 'magenta' => ['set' => 45, 'unset' => 49],
+ 'cyan' => ['set' => 46, 'unset' => 49],
+ 'white' => ['set' => 47, 'unset' => 49],
+ ];
+ private static $availableOptions = [
+ 'bold' => ['set' => 1, 'unset' => 22],
+ 'underscore' => ['set' => 4, 'unset' => 24],
+ 'blink' => ['set' => 5, 'unset' => 25],
+ 'reverse' => ['set' => 7, 'unset' => 27],
+ 'conceal' => ['set' => 8, 'unset' => 28],
+ ];
+
+ private $foreground;
+ private $background;
+ private $options = [];
+
+ /**
+ * 初始化输出的样式
+ * @param string|null $foreground 字体颜色
+ * @param string|null $background 背景色
+ * @param array $options 格式
+ * @api
+ */
+ public function __construct($foreground = null, $background = null, array $options = [])
+ {
+ if (null !== $foreground) {
+ $this->setForeground($foreground);
+ }
+ if (null !== $background) {
+ $this->setBackground($background);
+ }
+ if (count($options)) {
+ $this->setOptions($options);
+ }
+ }
+
+ /**
+ * 设置字体颜色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setForeground($color = null)
+ {
+ if (null === $color) {
+ $this->foreground = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableForegroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors))));
+ }
+
+ $this->foreground = static::$availableForegroundColors[$color];
+ }
+
+ /**
+ * 设置背景色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setBackground($color = null)
+ {
+ if (null === $color) {
+ $this->background = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableBackgroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
+ }
+
+ $this->background = static::$availableBackgroundColors[$color];
+ }
+
+ /**
+ * 设置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException When the option name isn't defined
+ * @api
+ */
+ public function setOption($option)
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ if (!in_array(static::$availableOptions[$option], $this->options)) {
+ $this->options[] = static::$availableOptions[$option];
+ }
+ }
+
+ /**
+ * 重置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException
+ */
+ public function unsetOption($option)
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ $pos = array_search(static::$availableOptions[$option], $this->options);
+ if (false !== $pos) {
+ unset($this->options[$pos]);
+ }
+ }
+
+ /**
+ * 批量设置字体格式
+ * @param array $options
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = [];
+
+ foreach ($options as $option) {
+ $this->setOption($option);
+ }
+ }
+
+ /**
+ * 应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ public function apply($text)
+ {
+ $setCodes = [];
+ $unsetCodes = [];
+
+ if (null !== $this->foreground) {
+ $setCodes[] = $this->foreground['set'];
+ $unsetCodes[] = $this->foreground['unset'];
+ }
+ if (null !== $this->background) {
+ $setCodes[] = $this->background['set'];
+ $unsetCodes[] = $this->background['unset'];
+ }
+ if (count($this->options)) {
+ foreach ($this->options as $option) {
+ $setCodes[] = $option['set'];
+ $unsetCodes[] = $option['unset'];
+ }
+ }
+
+ if (0 === count($setCodes)) {
+ return $text;
+ }
+
+ return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
+ }
+}
\ No newline at end of file
diff --git a/library/think/process/Builder.php b/library/think/process/Builder.php
new file mode 100644
index 00000000..826e6745
--- /dev/null
+++ b/library/think/process/Builder.php
@@ -0,0 +1,234 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process;
+
+use think\Process;
+
+class Builder
+{
+
+ private $arguments;
+ private $cwd;
+ private $env = null;
+ private $input;
+ private $timeout = 60;
+ private $options = [];
+ private $inheritEnv = true;
+ private $prefix = [];
+ private $outputDisabled = false;
+
+ /**
+ * 构造方法
+ * @param string[] $arguments 参数
+ */
+ public function __construct(array $arguments = [])
+ {
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * 创建一个实例
+ * @param string[] $arguments 参数
+ * @return self
+ */
+ public static function create(array $arguments = [])
+ {
+ return new static($arguments);
+ }
+
+ /**
+ * 添加一个参数
+ * @param string $argument 参数
+ * @return self
+ */
+ public function add($argument)
+ {
+ $this->arguments[] = $argument;
+
+ return $this;
+ }
+
+ /**
+ * 添加一个前缀
+ * @param string|array $prefix
+ * @return self
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = is_array($prefix) ? $prefix : [$prefix];
+
+ return $this;
+ }
+
+ /**
+ * 设置参数
+ * @param string[] $arguments
+ * @return self
+ */
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+
+ return $this;
+ }
+
+ /**
+ * 设置工作目录
+ * @param null|string $cwd
+ * @return self
+ */
+ public function setWorkingDirectory($cwd)
+ {
+ $this->cwd = $cwd;
+
+ return $this;
+ }
+
+ /**
+ * 是否初始化环境变量
+ * @param bool $inheritEnv
+ * @return self
+ */
+ public function inheritEnvironmentVariables($inheritEnv = true)
+ {
+ $this->inheritEnv = $inheritEnv;
+
+ return $this;
+ }
+
+ /**
+ * 设置环境变量
+ * @param string $name
+ * @param null|string $value
+ * @return self
+ */
+ public function setEnv($name, $value)
+ {
+ $this->env[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 添加环境变量
+ * @param array $variables
+ * @return self
+ */
+ public function addEnvironmentVariables(array $variables)
+ {
+ $this->env = array_replace($this->env, $variables);
+
+ return $this;
+ }
+
+ /**
+ * 设置输入
+ * @param mixed $input
+ * @return self
+ */
+ public function setInput($input)
+ {
+ $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input);
+
+ return $this;
+ }
+
+ /**
+ * 设置超时时间
+ * @param float|null $timeout
+ * @return self
+ */
+ public function setTimeout($timeout)
+ {
+ if (null === $timeout) {
+ $this->timeout = null;
+
+ return $this;
+ }
+
+ $timeout = (float)$timeout;
+
+ if ($timeout < 0) {
+ throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
+ }
+
+ $this->timeout = $timeout;
+
+ return $this;
+ }
+
+ /**
+ * 设置proc_open选项
+ * @param string $name
+ * @param string $value
+ * @return self
+ */
+ public function setOption($name, $value)
+ {
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 禁止输出
+ * @return self
+ */
+ public function disableOutput()
+ {
+ $this->outputDisabled = true;
+
+ return $this;
+ }
+
+ /**
+ * 开启输出
+ * @return self
+ */
+ public function enableOutput()
+ {
+ $this->outputDisabled = false;
+
+ return $this;
+ }
+
+ /**
+ * 创建一个Process实例
+ * @return Process
+ */
+ public function getProcess()
+ {
+ if (0 === count($this->prefix) && 0 === count($this->arguments)) {
+ throw new \LogicException('You must add() command arguments before calling getProcess().');
+ }
+
+ $options = $this->options;
+
+ $arguments = array_merge($this->prefix, $this->arguments);
+ $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments));
+
+ if ($this->inheritEnv) {
+ // include $_ENV for BC purposes
+ $env = array_replace($_ENV, $_SERVER, $this->env);
+ } else {
+ $env = $this->env;
+ }
+
+ $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options);
+
+ if ($this->outputDisabled) {
+ $process->disableOutput();
+ }
+
+ return $process;
+ }
+}
\ No newline at end of file
diff --git a/library/think/process/Utils.php b/library/think/process/Utils.php
new file mode 100644
index 00000000..28506e87
--- /dev/null
+++ b/library/think/process/Utils.php
@@ -0,0 +1,76 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process;
+
+
+class Utils
+{
+
+ /**
+ * 转义字符串
+ * @param string $argument
+ * @return string
+ */
+ public static function escapeArgument($argument)
+ {
+
+ 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;
+ }
+
+ /**
+ * 验证并进行规范化Process输入。
+ * @param string $caller
+ * @param mixed $input
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ public static function validateInput($caller, $input)
+ {
+ if (null !== $input) {
+ if (is_resource($input)) {
+ return $input;
+ }
+ if (is_scalar($input)) {
+ return (string)$input;
+ }
+ throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller));
+ }
+ return $input;
+ }
+
+ private static function isSurroundedBy($arg, $char)
+ {
+ return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
+ }
+
+}
\ No newline at end of file
diff --git a/library/think/process/exception/Faild.php b/library/think/process/exception/Faild.php
new file mode 100644
index 00000000..23df369d
--- /dev/null
+++ b/library/think/process/exception/Faild.php
@@ -0,0 +1,43 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\exception;
+
+
+use think\Process;
+
+class Failed extends \RuntimeException
+{
+
+ private $process;
+
+ public function __construct(Process $process)
+ {
+ if ($process->isSuccessful()) {
+ throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.');
+ }
+
+ $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText());
+
+ if (!$process->isOutputDisabled()) {
+ $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput());
+ }
+
+ parent::__construct($error);
+
+ $this->process = $process;
+ }
+
+ public function getProcess()
+ {
+ return $this->process;
+ }
+}
diff --git a/library/think/process/exception/Timeout.php b/library/think/process/exception/Timeout.php
new file mode 100644
index 00000000..62933e24
--- /dev/null
+++ b/library/think/process/exception/Timeout.php
@@ -0,0 +1,62 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\exception;
+
+
+use think\Process;
+
+class Timeout extends \RuntimeException
+{
+
+ const TYPE_GENERAL = 1;
+ const TYPE_IDLE = 2;
+
+ private $process;
+ private $timeoutType;
+
+ public function __construct(Process $process, $timeoutType)
+ {
+ $this->process = $process;
+ $this->timeoutType = $timeoutType;
+
+ parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout()));
+ }
+
+ public function getProcess()
+ {
+ return $this->process;
+ }
+
+ public function isGeneralTimeout()
+ {
+ return $this->timeoutType === self::TYPE_GENERAL;
+ }
+
+ public function isIdleTimeout()
+ {
+ return $this->timeoutType === self::TYPE_IDLE;
+ }
+
+ public function getExceededTimeout()
+ {
+ switch ($this->timeoutType) {
+ case self::TYPE_GENERAL:
+ return $this->process->getTimeout();
+
+ case self::TYPE_IDLE:
+ return $this->process->getIdleTimeout();
+
+ default:
+ throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/think/process/pipes/Pipes.php b/library/think/process/pipes/Pipes.php
new file mode 100644
index 00000000..51da44f3
--- /dev/null
+++ b/library/think/process/pipes/Pipes.php
@@ -0,0 +1,94 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+abstract class Pipes
+{
+
+ /** @var array */
+ public $pipes = [];
+
+ /** @var string */
+ protected $inputBuffer = '';
+ /** @var resource|null */
+ protected $input;
+
+ /** @var bool */
+ private $blocked = true;
+
+ const CHUNK_SIZE = 16384;
+
+ /**
+ * 返回用于 proc_open 描述符的数组
+ * @return array
+ */
+ abstract public function getDescriptors();
+
+ /**
+ * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。
+ * @return string[]
+ */
+ abstract public function getFiles();
+
+ /**
+ * 文件句柄和管道中读取数据。
+ * @param bool $blocking 是否使用阻塞调用
+ * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。
+ * @return string[]
+ */
+ abstract public function readAndWrite($blocking, $close = false);
+
+ /**
+ * 返回当前状态如果有打开的文件句柄或管道。
+ * @return bool
+ */
+ abstract public function areOpen();
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function close()
+ {
+ foreach ($this->pipes as $pipe) {
+ fclose($pipe);
+ }
+ $this->pipes = [];
+ }
+
+ /**
+ * 检查系统调用已被中断
+ * @return bool
+ */
+ protected function hasSystemCallBeenInterrupted()
+ {
+ $lastError = error_get_last();
+
+ return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
+ }
+
+ protected function unblock()
+ {
+ if (!$this->blocked) {
+ return;
+ }
+
+ foreach ($this->pipes as $pipe) {
+ stream_set_blocking($pipe, 0);
+ }
+ if (null !== $this->input) {
+ stream_set_blocking($this->input, 0);
+ }
+
+ $this->blocked = false;
+ }
+}
\ No newline at end of file
diff --git a/library/think/process/pipes/Unix.php b/library/think/process/pipes/Unix.php
new file mode 100644
index 00000000..328b19f5
--- /dev/null
+++ b/library/think/process/pipes/Unix.php
@@ -0,0 +1,197 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+
+use think\Process;
+
+class Unix extends Pipes
+{
+
+ /** @var bool */
+ private $ttyMode;
+ /** @var bool */
+ private $ptyMode;
+ /** @var bool */
+ private $disableOutput;
+
+ public function __construct($ttyMode, $ptyMode, $input, $disableOutput)
+ {
+ $this->ttyMode = (bool)$ttyMode;
+ $this->ptyMode = (bool)$ptyMode;
+ $this->disableOutput = (bool)$disableOutput;
+
+ if (is_resource($input)) {
+ $this->input = $input;
+ } else {
+ $this->inputBuffer = (string)$input;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescriptors()
+ {
+ if ($this->disableOutput) {
+ $nullstream = fopen('/dev/null', 'c');
+
+ return [
+ ['pipe', 'r'],
+ $nullstream,
+ $nullstream,
+ ];
+ }
+
+ if ($this->ttyMode) {
+ return [
+ ['file', '/dev/tty', 'r'],
+ ['file', '/dev/tty', 'w'],
+ ['file', '/dev/tty', 'w'],
+ ];
+ }
+
+ if ($this->ptyMode && Process::isPtySupported()) {
+ return [
+ ['pty'],
+ ['pty'],
+ ['pty'],
+ ];
+ }
+
+ return [
+ ['pipe', 'r'],
+ ['pipe', 'w'], // stdout
+ ['pipe', 'w'], // stderr
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFiles()
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readAndWrite($blocking, $close = false)
+ {
+
+ if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+
+ if (empty($this->pipes)) {
+ return [];
+ }
+
+ $this->unblock();
+
+ $read = [];
+
+ if (null !== $this->input) {
+ $r = array_merge($this->pipes, ['input' => $this->input]);
+ } else {
+ $r = $this->pipes;
+ }
+
+ unset($r[0]);
+
+ $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null;
+ $e = null;
+
+ if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
+
+ if (!$this->hasSystemCallBeenInterrupted()) {
+ $this->pipes = [];
+ }
+
+ return $read;
+ }
+
+ if (0 === $n) {
+ return $read;
+ }
+
+ foreach ($r as $pipe) {
+
+ $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input';
+ $data = '';
+ while ('' !== $dataread = (string)fread($pipe, self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+
+ if ('' !== $data) {
+ if ($type === 'input') {
+ $this->inputBuffer .= $data;
+ } else {
+ $read[$type] = $data;
+ }
+ }
+
+ if (false === $data || (true === $close && feof($pipe) && '' === $data)) {
+ if ($type === 'input') {
+ $this->input = null;
+ } else {
+ fclose($this->pipes[$type]);
+ unset($this->pipes[$type]);
+ }
+ }
+ }
+
+ if (null !== $w && 0 < count($w)) {
+ while (strlen($this->inputBuffer)) {
+ $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k
+ if ($written > 0) {
+ $this->inputBuffer = (string)substr($this->inputBuffer, $written);
+ } else {
+ break;
+ }
+ }
+ }
+
+ if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+
+ return $read;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function areOpen()
+ {
+ return (bool)$this->pipes;
+ }
+
+ /**
+ * 创建一个新的 UnixPipes 实例
+ * @param Process $process
+ * @param string|resource $input
+ * @return self
+ */
+ public static function create(Process $process, $input)
+ {
+ return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled());
+ }
+}
\ No newline at end of file
diff --git a/library/think/process/pipes/Windows.php b/library/think/process/pipes/Windows.php
new file mode 100644
index 00000000..31579eab
--- /dev/null
+++ b/library/think/process/pipes/Windows.php
@@ -0,0 +1,229 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+
+use think\Process;
+
+class Windows extends Pipes
+{
+
+ /** @var array */
+ private $files = [];
+ /** @var array */
+ private $fileHandles = [];
+ /** @var array */
+ private $readBytes = [
+ Process::STDOUT => 0,
+ Process::STDERR => 0,
+ ];
+ /** @var bool */
+ private $disableOutput;
+
+ public function __construct($disableOutput, $input)
+ {
+ $this->disableOutput = (bool)$disableOutput;
+
+ if (!$this->disableOutput) {
+
+ $this->files = [
+ Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'),
+ Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'),
+ ];
+ foreach ($this->files as $offset => $file) {
+ $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb');
+ if (false === $this->fileHandles[$offset]) {
+ throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
+ }
+ }
+ }
+
+ if (is_resource($input)) {
+ $this->input = $input;
+ } else {
+ $this->inputBuffer = $input;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ $this->removeFiles();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescriptors()
+ {
+ if ($this->disableOutput) {
+ $nullstream = fopen('NUL', 'c');
+
+ return [
+ ['pipe', 'r'],
+ $nullstream,
+ $nullstream,
+ ];
+ }
+
+ return [
+ ['pipe', 'r'],
+ ['file', 'NUL', 'w'],
+ ['file', 'NUL', 'w'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFiles()
+ {
+ return $this->files;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readAndWrite($blocking, $close = false)
+ {
+ $this->write($blocking, $close);
+
+ $read = [];
+ $fh = $this->fileHandles;
+ foreach ($fh as $type => $fileHandle) {
+ if (0 !== fseek($fileHandle, $this->readBytes[$type])) {
+ continue;
+ }
+ $data = '';
+ $dataread = null;
+ while (!feof($fileHandle)) {
+ if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+ }
+ if (0 < $length = strlen($data)) {
+ $this->readBytes[$type] += $length;
+ $read[$type] = $data;
+ }
+
+ if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) {
+ fclose($this->fileHandles[$type]);
+ unset($this->fileHandles[$type]);
+ }
+ }
+
+ return $read;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function areOpen()
+ {
+ return (bool)$this->pipes && (bool)$this->fileHandles;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function close()
+ {
+ parent::close();
+ foreach ($this->fileHandles as $handle) {
+ fclose($handle);
+ }
+ $this->fileHandles = [];
+ }
+
+ /**
+ * 创建一个新的 WindowsPipes 实例。
+ * @param Process $process
+ * @param $input
+ * @return self
+ */
+ public static function create(Process $process, $input)
+ {
+ return new static($process->isOutputDisabled(), $input);
+ }
+
+ /**
+ * 删除临时文件
+ */
+ private function removeFiles()
+ {
+ foreach ($this->files as $filename) {
+ if (file_exists($filename)) {
+ @unlink($filename);
+ }
+ }
+ $this->files = [];
+ }
+
+ /**
+ * 写入到 stdin 输入
+ * @param bool $blocking
+ * @param bool $close
+ */
+ private function write($blocking, $close)
+ {
+ if (empty($this->pipes)) {
+ return;
+ }
+
+ $this->unblock();
+
+ $r = null !== $this->input ? ['input' => $this->input] : null;
+ $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null;
+ $e = null;
+
+ if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
+ if (!$this->hasSystemCallBeenInterrupted()) {
+ $this->pipes = [];
+ }
+
+ return;
+ }
+
+ if (0 === $n) {
+ return;
+ }
+
+ if (null !== $w && 0 < count($r)) {
+ $data = '';
+ while ($dataread = fread($r['input'], self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+
+ $this->inputBuffer .= $data;
+
+ if (false === $data || (true === $close && feof($r['input']) && '' === $data)) {
+ $this->input = null;
+ }
+ }
+
+ if (null !== $w && 0 < count($w)) {
+ while (strlen($this->inputBuffer)) {
+ $written = fwrite($w[0], $this->inputBuffer, 2 << 18);
+ if ($written > 0) {
+ $this->inputBuffer = (string)substr($this->inputBuffer, $written);
+ } else {
+ break;
+ }
+ }
+ }
+
+ if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/mode/console.php b/mode/console.php
new file mode 100644
index 00000000..2a35e7c7
--- /dev/null
+++ b/mode/console.php
@@ -0,0 +1,22 @@
+
+// +----------------------------------------------------------------------
+
+/**
+ * ThinkPHP CLI模式定义
+ */
+
+return [
+
+ 'config' => [
+ 'commands' => []
+ ]
+
+];