Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Console\Helper; Chris@0: Chris@0: use Symfony\Component\Console\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\Console\Exception\RuntimeException; Chris@14: use Symfony\Component\Console\Formatter\OutputFormatter; Chris@14: use Symfony\Component\Console\Formatter\OutputFormatterStyle; Chris@0: use Symfony\Component\Console\Input\InputInterface; Chris@0: use Symfony\Component\Console\Input\StreamableInputInterface; Chris@0: use Symfony\Component\Console\Output\ConsoleOutputInterface; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@17: use Symfony\Component\Console\Question\ChoiceQuestion; Chris@0: use Symfony\Component\Console\Question\Question; Chris@0: Chris@0: /** Chris@0: * The QuestionHelper class provides helpers to interact with the user. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class QuestionHelper extends Helper Chris@0: { Chris@0: private $inputStream; Chris@0: private static $shell; Chris@0: private static $stty; Chris@0: Chris@0: /** Chris@0: * Asks a question to the user. Chris@0: * Chris@0: * @return mixed The user answer Chris@0: * Chris@0: * @throws RuntimeException If there is no data to read in the input stream Chris@0: */ Chris@0: public function ask(InputInterface $input, OutputInterface $output, Question $question) Chris@0: { Chris@0: if ($output instanceof ConsoleOutputInterface) { Chris@0: $output = $output->getErrorOutput(); Chris@0: } Chris@0: Chris@0: if (!$input->isInteractive()) { Chris@17: $default = $question->getDefault(); Chris@17: Chris@18: if (null === $default) { Chris@18: return $default; Chris@18: } Chris@18: Chris@18: if ($validator = $question->getValidator()) { Chris@18: return \call_user_func($question->getValidator(), $default); Chris@18: } elseif ($question instanceof ChoiceQuestion) { Chris@14: $choices = $question->getChoices(); Chris@14: Chris@17: if (!$question->isMultiselect()) { Chris@17: return isset($choices[$default]) ? $choices[$default] : $default; Chris@17: } Chris@17: Chris@17: $default = explode(',', $default); Chris@17: foreach ($default as $k => $v) { Chris@17: $v = trim($v); Chris@17: $default[$k] = isset($choices[$v]) ? $choices[$v] : $v; Chris@17: } Chris@14: } Chris@14: Chris@17: return $default; Chris@0: } Chris@0: Chris@0: if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { Chris@0: $this->inputStream = $stream; Chris@0: } Chris@0: Chris@0: if (!$question->getValidator()) { Chris@0: return $this->doAsk($output, $question); Chris@0: } Chris@0: Chris@0: $interviewer = function () use ($output, $question) { Chris@0: return $this->doAsk($output, $question); Chris@0: }; Chris@0: Chris@0: return $this->validateAttempts($interviewer, $output, $question); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the input stream to read from when interacting with the user. Chris@0: * Chris@0: * This is mainly useful for testing purpose. Chris@0: * Chris@0: * @deprecated since version 3.2, to be removed in 4.0. Use Chris@0: * StreamableInputInterface::setStream() instead. Chris@0: * Chris@0: * @param resource $stream The input stream Chris@0: * Chris@0: * @throws InvalidArgumentException In case the stream is not a resource Chris@0: */ Chris@0: public function setInputStream($stream) Chris@0: { Chris@14: @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::setStream() instead.', __METHOD__, StreamableInputInterface::class), E_USER_DEPRECATED); Chris@0: Chris@17: if (!\is_resource($stream)) { Chris@0: throw new InvalidArgumentException('Input stream must be a valid resource.'); Chris@0: } Chris@0: Chris@0: $this->inputStream = $stream; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the helper's input stream. Chris@0: * Chris@0: * @deprecated since version 3.2, to be removed in 4.0. Use Chris@0: * StreamableInputInterface::getStream() instead. Chris@0: * Chris@0: * @return resource Chris@0: */ Chris@0: public function getInputStream() Chris@0: { Chris@17: if (0 === \func_num_args() || func_get_arg(0)) { Chris@14: @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::getStream() instead.', __METHOD__, StreamableInputInterface::class), E_USER_DEPRECATED); Chris@0: } Chris@0: Chris@0: return $this->inputStream; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getName() Chris@0: { Chris@0: return 'question'; Chris@0: } Chris@0: Chris@0: /** Chris@14: * Prevents usage of stty. Chris@14: */ Chris@14: public static function disableStty() Chris@14: { Chris@14: self::$stty = false; Chris@14: } Chris@14: Chris@14: /** Chris@0: * Asks the question to the user. Chris@0: * Chris@17: * @return bool|mixed|string|null Chris@0: * Chris@12: * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden Chris@0: */ Chris@0: private function doAsk(OutputInterface $output, Question $question) Chris@0: { Chris@0: $this->writePrompt($output, $question); Chris@0: Chris@0: $inputStream = $this->inputStream ?: STDIN; Chris@0: $autocomplete = $question->getAutocompleterValues(); Chris@0: Chris@0: if (null === $autocomplete || !$this->hasSttyAvailable()) { Chris@0: $ret = false; Chris@0: if ($question->isHidden()) { Chris@0: try { Chris@0: $ret = trim($this->getHiddenResponse($output, $inputStream)); Chris@12: } catch (RuntimeException $e) { Chris@0: if (!$question->isHiddenFallback()) { Chris@0: throw $e; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if (false === $ret) { Chris@0: $ret = fgets($inputStream, 4096); Chris@0: if (false === $ret) { Chris@18: throw new RuntimeException('Aborted.'); Chris@0: } Chris@0: $ret = trim($ret); Chris@0: } Chris@0: } else { Chris@17: $ret = trim($this->autocomplete($output, $question, $inputStream, \is_array($autocomplete) ? $autocomplete : iterator_to_array($autocomplete, false))); Chris@0: } Chris@0: Chris@17: $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); Chris@0: Chris@0: if ($normalizer = $question->getNormalizer()) { Chris@0: return $normalizer($ret); Chris@0: } Chris@0: Chris@0: return $ret; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Outputs the question prompt. Chris@0: */ Chris@0: protected function writePrompt(OutputInterface $output, Question $question) Chris@0: { Chris@0: $message = $question->getQuestion(); Chris@0: Chris@0: if ($question instanceof ChoiceQuestion) { Chris@17: $maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices()))); Chris@0: Chris@0: $messages = (array) $question->getQuestion(); Chris@0: foreach ($question->getChoices() as $key => $value) { Chris@0: $width = $maxWidth - $this->strlen($key); Chris@0: $messages[] = ' ['.$key.str_repeat(' ', $width).'] '.$value; Chris@0: } Chris@0: Chris@0: $output->writeln($messages); Chris@0: Chris@0: $message = $question->getPrompt(); Chris@0: } Chris@0: Chris@0: $output->write($message); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Outputs an error message. Chris@0: */ Chris@0: protected function writeError(OutputInterface $output, \Exception $error) Chris@0: { Chris@0: if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { Chris@0: $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); Chris@0: } else { Chris@0: $message = ''.$error->getMessage().''; Chris@0: } Chris@0: Chris@0: $output->writeln($message); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Autocompletes a question. Chris@0: * Chris@0: * @param OutputInterface $output Chris@0: * @param Question $question Chris@0: * @param resource $inputStream Chris@14: * @param array $autocomplete Chris@0: * Chris@0: * @return string Chris@0: */ Chris@14: private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete) Chris@0: { Chris@0: $ret = ''; Chris@0: Chris@0: $i = 0; Chris@0: $ofs = -1; Chris@0: $matches = $autocomplete; Chris@17: $numMatches = \count($matches); Chris@0: Chris@0: $sttyMode = shell_exec('stty -g'); Chris@0: Chris@0: // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) Chris@0: shell_exec('stty -icanon -echo'); Chris@0: Chris@0: // Add highlighted text style Chris@0: $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); Chris@0: Chris@0: // Read a keypress Chris@0: while (!feof($inputStream)) { Chris@0: $c = fread($inputStream, 1); Chris@0: Chris@18: // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. Chris@18: if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { Chris@18: shell_exec(sprintf('stty %s', $sttyMode)); Chris@18: throw new RuntimeException('Aborted.'); Chris@18: } elseif ("\177" === $c) { // Backspace Character Chris@0: if (0 === $numMatches && 0 !== $i) { Chris@0: --$i; Chris@0: // Move cursor backwards Chris@0: $output->write("\033[1D"); Chris@0: } Chris@0: Chris@14: if (0 === $i) { Chris@0: $ofs = -1; Chris@0: $matches = $autocomplete; Chris@17: $numMatches = \count($matches); Chris@0: } else { Chris@0: $numMatches = 0; Chris@0: } Chris@0: Chris@0: // Pop the last character off the end of our string Chris@0: $ret = substr($ret, 0, $i); Chris@0: } elseif ("\033" === $c) { Chris@0: // Did we read an escape sequence? Chris@0: $c .= fread($inputStream, 2); Chris@0: Chris@0: // A = Up Arrow. B = Down Arrow Chris@0: if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { Chris@0: if ('A' === $c[2] && -1 === $ofs) { Chris@0: $ofs = 0; Chris@0: } Chris@0: Chris@0: if (0 === $numMatches) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $ofs += ('A' === $c[2]) ? -1 : 1; Chris@0: $ofs = ($numMatches + $ofs) % $numMatches; Chris@0: } Chris@17: } elseif (\ord($c) < 32) { Chris@0: if ("\t" === $c || "\n" === $c) { Chris@0: if ($numMatches > 0 && -1 !== $ofs) { Chris@0: $ret = $matches[$ofs]; Chris@0: // Echo out remaining chars for current match Chris@0: $output->write(substr($ret, $i)); Chris@17: $i = \strlen($ret); Chris@0: } Chris@0: Chris@0: if ("\n" === $c) { Chris@0: $output->write($c); Chris@0: break; Chris@0: } Chris@0: Chris@0: $numMatches = 0; Chris@0: } Chris@0: Chris@0: continue; Chris@0: } else { Chris@18: if ("\x80" <= $c) { Chris@18: $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); Chris@18: } Chris@18: Chris@0: $output->write($c); Chris@0: $ret .= $c; Chris@0: ++$i; Chris@0: Chris@0: $numMatches = 0; Chris@0: $ofs = 0; Chris@0: Chris@0: foreach ($autocomplete as $value) { Chris@0: // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) Chris@16: if (0 === strpos($value, $ret)) { Chris@0: $matches[$numMatches++] = $value; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Erase characters from cursor to end of line Chris@0: $output->write("\033[K"); Chris@0: Chris@0: if ($numMatches > 0 && -1 !== $ofs) { Chris@0: // Save cursor position Chris@0: $output->write("\0337"); Chris@0: // Write highlighted text Chris@14: $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $i)).''); Chris@0: // Restore cursor position Chris@0: $output->write("\0338"); Chris@0: } Chris@0: } Chris@0: Chris@0: // Reset stty so it behaves normally again Chris@0: shell_exec(sprintf('stty %s', $sttyMode)); Chris@0: Chris@0: return $ret; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets a hidden response from user. Chris@0: * Chris@0: * @param OutputInterface $output An Output instance Chris@0: * @param resource $inputStream The handler resource Chris@0: * Chris@0: * @return string The answer Chris@0: * Chris@0: * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden Chris@0: */ Chris@0: private function getHiddenResponse(OutputInterface $output, $inputStream) Chris@0: { Chris@17: if ('\\' === \DIRECTORY_SEPARATOR) { Chris@0: $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; Chris@0: Chris@0: // handle code running from a phar Chris@0: if ('phar:' === substr(__FILE__, 0, 5)) { Chris@0: $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; Chris@0: copy($exe, $tmpExe); Chris@0: $exe = $tmpExe; Chris@0: } Chris@0: Chris@0: $value = rtrim(shell_exec($exe)); Chris@0: $output->writeln(''); Chris@0: Chris@0: if (isset($tmpExe)) { Chris@0: unlink($tmpExe); Chris@0: } Chris@0: Chris@0: return $value; Chris@0: } Chris@0: Chris@0: if ($this->hasSttyAvailable()) { Chris@0: $sttyMode = shell_exec('stty -g'); Chris@0: Chris@0: shell_exec('stty -echo'); Chris@0: $value = fgets($inputStream, 4096); Chris@0: shell_exec(sprintf('stty %s', $sttyMode)); Chris@0: Chris@0: if (false === $value) { Chris@18: throw new RuntimeException('Aborted.'); Chris@0: } Chris@0: Chris@0: $value = trim($value); Chris@0: $output->writeln(''); Chris@0: Chris@0: return $value; Chris@0: } Chris@0: Chris@0: if (false !== $shell = $this->getShell()) { Chris@14: $readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword'; Chris@0: $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); Chris@0: $value = rtrim(shell_exec($command)); Chris@0: $output->writeln(''); Chris@0: Chris@0: return $value; Chris@0: } Chris@0: Chris@0: throw new RuntimeException('Unable to hide the response.'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates an attempt. Chris@0: * Chris@0: * @param callable $interviewer A callable that will ask for a question and return the result Chris@0: * @param OutputInterface $output An Output instance Chris@0: * @param Question $question A Question instance Chris@0: * Chris@0: * @return mixed The validated response Chris@0: * Chris@0: * @throws \Exception In case the max number of attempts has been reached and no valid response has been given Chris@0: */ Chris@0: private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) Chris@0: { Chris@0: $error = null; Chris@0: $attempts = $question->getMaxAttempts(); Chris@0: while (null === $attempts || $attempts--) { Chris@0: if (null !== $error) { Chris@0: $this->writeError($output, $error); Chris@0: } Chris@0: Chris@0: try { Chris@17: return \call_user_func($question->getValidator(), $interviewer()); Chris@0: } catch (RuntimeException $e) { Chris@0: throw $e; Chris@0: } catch (\Exception $error) { Chris@0: } Chris@0: } Chris@0: Chris@0: throw $error; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a valid unix shell. Chris@0: * Chris@0: * @return string|bool The valid shell name, false in case no valid shell is found Chris@0: */ Chris@0: private function getShell() Chris@0: { Chris@0: if (null !== self::$shell) { Chris@0: return self::$shell; Chris@0: } Chris@0: Chris@0: self::$shell = false; Chris@0: Chris@0: if (file_exists('/usr/bin/env')) { Chris@0: // handle other OSs with bash/zsh/ksh/csh if available to hide the answer Chris@0: $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; Chris@17: foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { Chris@0: if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { Chris@0: self::$shell = $sh; Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return self::$shell; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether Stty is available or not. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function hasSttyAvailable() Chris@0: { Chris@0: if (null !== self::$stty) { Chris@0: return self::$stty; Chris@0: } Chris@0: Chris@0: exec('stty 2>&1', $output, $exitcode); Chris@0: Chris@14: return self::$stty = 0 === $exitcode; Chris@0: } Chris@0: }