From c440acc124ff62ac15aedb2cec18cbdeb5a422fd Mon Sep 17 00:00:00 2001
From: yunwuxin <448901948@qq.com>
Date: Mon, 15 Aug 2016 12:03:51 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84console=20=E5=A2=9E=E5=8A=A0o?=
=?UTF-8?q?utput=E4=B8=80=E4=BA=9B=E5=B8=B8=E7=94=A8=E7=9A=84=E6=96=B9?=
=?UTF-8?q?=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
library/think/console/Output.php | 97 ++++-
library/think/console/output/Ask.php | 340 ++++++++++++++++++
.../output/{question => }/Question.php | 2 +-
.../think/console/output/question/Choice.php | 9 +-
.../console/output/question/Confirmation.php | 4 +-
5 files changed, 448 insertions(+), 4 deletions(-)
create mode 100644 library/think/console/output/Ask.php
rename library/think/console/output/{question => }/Question.php (99%)
diff --git a/library/think/console/Output.php b/library/think/console/Output.php
index 62296cea..f9eb214e 100644
--- a/library/think/console/Output.php
+++ b/library/think/console/Output.php
@@ -12,10 +12,14 @@
namespace think\console;
use Exception;
+use think\console\output\Ask;
use think\console\output\Descriptor;
use think\console\output\driver\Buffer;
use think\console\output\driver\Console;
use think\console\output\driver\Nothing;
+use think\console\output\Question;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
/**
* Class Output
@@ -26,6 +30,13 @@ use think\console\output\driver\Nothing;
*
* @see think\console\output\driver\Buffer::fetch
* @method string fetch()
+ *
+ * @method void info($message)
+ * @method void error($message)
+ * @method void comment($message)
+ * @method void warning($message)
+ * @method void highlight($message)
+ * @method void question($message)
*/
class Output
{
@@ -44,6 +55,15 @@ class Output
/** @var Buffer|Console|Nothing */
private $handle = null;
+ protected $styles = [
+ 'info',
+ 'error',
+ 'comment',
+ 'question',
+ 'highlight',
+ 'warning'
+ ];
+
public function __construct($driver = 'console')
{
$class = '\\think\\console\\output\\driver\\' . ucwords($driver);
@@ -51,13 +71,83 @@ class Output
$this->handle = new $class($this);
}
+ public function ask(Input $input, $question, $default = null, $validator = null)
+ {
+ $question = new Question($question, $default);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function askHidden(Input $input, $question, $validator = null)
+ {
+ $question = new Question($question);
+
+ $question->setHidden(true);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function confirm(Input $input, $question, $default = true)
+ {
+ return $this->askQuestion($input, new Confirmation($question, $default));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function choice(Input $input, $question, array $choices, $default = null)
+ {
+ if (null !== $default) {
+ $values = array_flip($choices);
+ $default = $values[$default];
+ }
+
+ return $this->askQuestion($input, new Choice($question, $choices, $default));
+ }
+
+ protected function askQuestion(Input $input, Question $question)
+ {
+ $ask = new Ask($input, $this, $question);
+ $answer = $ask->run();
+
+ if ($input->isInteractive()) {
+ $this->newLine();
+ }
+
+ return $answer;
+ }
+
+ protected function block($style, $message)
+ {
+ $this->writeln("<{$style}>{$message}$style>");
+ }
+
+ /**
+ * 输出空行
+ * @param int $count
+ */
+ public function newLine($count = 1)
+ {
+ $this->write(str_repeat(PHP_EOL, $count));
+ }
+
+ /**
+ * 输出信息并换行
+ * @param string $messages
+ * @param int $type
+ */
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $type);
}
/**
- * {@inheritdoc}
+ * 输出信息
+ * @param string $messages
+ * @param bool $newline
+ * @param int $type
*/
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
@@ -117,6 +207,11 @@ class Output
public function __call($method, $args)
{
+ if (in_array($method, $this->styles)) {
+ array_unshift($args, $method);
+ return call_user_func_array([$this, 'block'], $args);
+ }
+
if ($this->handle && method_exists($this->handle, $method)) {
return call_user_func_array([$this->handle, $method], $args);
} else {
diff --git a/library/think/console/output/Ask.php b/library/think/console/output/Ask.php
new file mode 100644
index 00000000..92111ada
--- /dev/null
+++ b/library/think/console/output/Ask.php
@@ -0,0 +1,340 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\console\Input;
+use think\console\Output;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+
+class Ask
+{
+ private static $stty;
+
+ private static $shell;
+
+ /** @var Input */
+ protected $input;
+
+ /** @var Output */
+ protected $output;
+
+ /** @var Question */
+ protected $question;
+
+ public function __construct(Input $input, Output $output, Question $question)
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->question = $question;
+ }
+
+ public function run()
+ {
+ if (!$this->input->isInteractive()) {
+ return $this->question->getDefault();
+ }
+
+ if (!$this->question->getValidator()) {
+ return $this->doAsk();
+ }
+
+ $that = $this;
+
+ $interviewer = function () use ($that) {
+ return $that->doAsk();
+ };
+
+ return $this->validateAttempts($interviewer);
+ }
+
+ protected function doAsk()
+ {
+ $this->writePrompt();
+
+ $inputStream = STDIN;
+ $autocomplete = $this->question->getAutocompleterValues();
+
+ if (null === $autocomplete || !$this->hasSttyAvailable()) {
+ $ret = false;
+ if ($this->question->isHidden()) {
+ try {
+ $ret = trim($this->getHiddenResponse($inputStream));
+ } catch (\RuntimeException $e) {
+ if (!$this->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($inputStream));
+ }
+
+ $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault();
+
+ if ($normalizer = $this->question->getNormalizer()) {
+ return $normalizer($ret);
+ }
+
+ return $ret;
+ }
+
+ private function autocomplete($inputStream)
+ {
+ $autocomplete = $this->question->getAutocompleterValues();
+ $ret = '';
+
+ $i = 0;
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -icanon -echo');
+
+ while (!feof($inputStream)) {
+ $c = fread($inputStream, 1);
+
+ if ("\177" === $c) {
+ if (0 === $numMatches && 0 !== $i) {
+ --$i;
+ $this->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];
+ $this->output->write(substr($ret, $i));
+ $i = strlen($ret);
+ }
+
+ if ("\n" === $c) {
+ $this->output->write($c);
+ break;
+ }
+
+ $numMatches = 0;
+ }
+
+ continue;
+ } else {
+ $this->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;
+ }
+ }
+ }
+
+ $this->output->write("\033[K");
+
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $this->output->write("\0337");
+ $this->output->highlight(substr($matches[$ofs], $i));
+ $this->output->write("\0338");
+ }
+ }
+
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ return $ret;
+ }
+
+ protected function getHiddenResponse($inputStream)
+ {
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ $exe = __DIR__ . '/../bin/hiddeninput.exe';
+
+ $value = rtrim(shell_exec($exe));
+ $this->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);
+ $this->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));
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ throw new \RuntimeException('Unable to hide the response.');
+ }
+
+ protected function validateAttempts($interviewer)
+ {
+ /** @var \Exception $error */
+ $error = null;
+ $attempts = $this->question->getMaxAttempts();
+ while (null === $attempts || $attempts--) {
+ if (null !== $error) {
+ $this->output->error($error->getMessage());
+ }
+
+ try {
+ return call_user_func($this->question->getValidator(), $interviewer());
+ } catch (\Exception $error) {
+ }
+ }
+
+ throw $error;
+ }
+
+ /**
+ * 显示问题的提示信息
+ */
+ protected function writePrompt()
+ {
+ $text = $this->question->getQuestion();
+ $default = $this->question->getDefault();
+
+ switch (true) {
+ case null === $default:
+ $text = sprintf(' %s:', $text);
+
+ break;
+
+ case $this->question instanceof Confirmation:
+ $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no');
+
+ break;
+
+ case $this->question instanceof Choice && $this->question->isMultiselect():
+ $choices = $this->question->getChoices();
+ $default = explode(',', $default);
+
+ foreach ($default as $key => $value) {
+ $default[$key] = $choices[trim($value)];
+ }
+
+ $text = sprintf(' %s [%s]:', $text, implode(', ', $default));
+
+ break;
+
+ case $this->question instanceof Choice:
+ $choices = $this->question->getChoices();
+ $text = sprintf(' %s [%s]:', $text, $choices[$default]);
+
+ break;
+
+ default:
+ $text = sprintf(' %s [%s]:', $text, $default);
+ }
+
+ $this->output->writeln($text);
+
+ if ($this->question instanceof Choice) {
+ $width = max(array_map('strlen', array_keys($this->question->getChoices())));
+
+ foreach ($this->question->getChoices() as $key => $value) {
+ $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value));
+ }
+ }
+
+ $this->output->write(' > ');
+ }
+
+ private function getShell()
+ {
+ if (null !== self::$shell) {
+ return self::$shell;
+ }
+
+ self::$shell = false;
+
+ if (file_exists('/usr/bin/env')) {
+ $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;
+ }
+
+ 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/output/question/Question.php b/library/think/console/output/Question.php
similarity index 99%
rename from library/think/console/output/question/Question.php
rename to library/think/console/output/Question.php
index 80fddad9..03975f27 100644
--- a/library/think/console/output/question/Question.php
+++ b/library/think/console/output/Question.php
@@ -9,7 +9,7 @@
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
-namespace think\console\helper\question;
+namespace think\console\output;
class Question
{
diff --git a/library/think/console/output/question/Choice.php b/library/think/console/output/question/Choice.php
index 89217139..f6760e5e 100644
--- a/library/think/console/output/question/Choice.php
+++ b/library/think/console/output/question/Choice.php
@@ -9,7 +9,9 @@
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
-namespace think\console\helper\question;
+namespace think\console\output\question;
+
+use think\console\output\Question;
class Choice extends Question
{
@@ -56,6 +58,11 @@ class Choice extends Question
return $this;
}
+ public function isMultiselect()
+ {
+ return $this->multiselect;
+ }
+
/**
* 获取提示
* @return string
diff --git a/library/think/console/output/question/Confirmation.php b/library/think/console/output/question/Confirmation.php
index 0cfd2eec..6598f9b3 100644
--- a/library/think/console/output/question/Confirmation.php
+++ b/library/think/console/output/question/Confirmation.php
@@ -9,7 +9,9 @@
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
-namespace think\console\helper\question;
+namespace think\console\output\question;
+
+use think\console\output\Question;
class Confirmation extends Question
{