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