8000 [Console] Add DialogHelper::askHiddenResponse method by romainneutron · Pull Request #5731 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] Add DialogHelper::askHiddenResponse method #5731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 16, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* added support for colorization on Windows via ConEmu
* add a method to Dialog Helper to ask for a question and hide the response

2.1.0
-----
Expand Down
187 changes: 174 additions & 13 deletions src/Symfony/Component/Console/Helper/DialogHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
class DialogHelper extends Helper
{
private $inputStream;
private static $shell;
private static $stty;

/**
* Asks a question to the user.
Expand Down Expand Up @@ -71,6 +73,76 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
return !$answer || 'y' == strtolower($answer[0]);
}

/**
* Asks a question to the user, the response is hidden
*
* @param OutputInterface $output An Output instance
* @param string|array $question The question
* @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
*
* @return string The answer
*
* @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
*/
public function askHiddenResponse(OutputInterface $output, $question, $fallback = true)
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$exe = __DIR__ . '/../../Resources/bin/hiddeninput.exe';

// handle code running from a phar
if ('phar:' === substr(__FILE__, 0, 5)) {
$tmpExe = sys_get_temp_dir() . '/../../Resources/bin/hiddeninput.exe';
copy($exe, $tmpExe);
$exe = $tmpExe;
}

$output->write($question);
$value = rtrim(shell_exec($exe));
$output->writeln('');

if (isset($tmpExe)) {
unlink($tmpExe);
}

return $value;
}

if ($this->hasSttyAvailable()) {
$output->write($question);

$sttyMode = shell_exec('/usr/bin/env stty -g');

shell_exec('/usr/bin/env stty -echo');
$value = fgets($this->inputStream ?: STDIN, 4096);
shell_exec(sprintf('/usr/bin/env stty %s', $sttyMode));

if (false === $value) {
throw new \RuntimeException('Aborted');
}

$value = trim($value);
$output->writeln('');

return $value;
}

if (false !== $shell = $this->getShell()) {
$output->write($question);
$readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read 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;
}

if ($fallback) {
return $this->ask($output, $question);
}

throw new \RuntimeException('Unable to hide the response');
}

/**
* Asks for a value and validates the response.
*
Expand All @@ -80,7 +152,7 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
*
* @param OutputInterface $output An Output instance
* @param string|array $question The question to ask
* @param callback $validator A PHP callback
* @param callable $validator A PHP callback
* @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
* @param string $default The default answer if none is given by the user
*
Expand All @@ -90,21 +162,43 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
*/
public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null)
{
$error = null;
while (false === $attempts || $attempts--) {
if (null !== $error) {
$output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
}
$that = $this;

$value = $this->ask($output, $question, $default);
$interviewer = function() use ($output, $question, $default, $that) {
return $that->ask($output, $question, $default);
};

try {
return call_user_func($validator, $value);
} catch (\Exception $error) {
}
}
return $this->validateAttempts($interviewer, $output, $validator, $attempts);
}

throw $error;
/**
* Asks for a value, hide and validates the response.
*
* The validator receives the data to validate. It must return the
* validated data when the data is valid and throw an exception
* otherwise.
*
* @param OutputInterface $output An Output instance
* @param string|array $question The question to ask
* @param callable $validator A PHP callback
* @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
* @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
*
* @return string The response
*
* @throws \Exception When any of the validators return an error
* @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
*
*/
public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true)
{
$that = $this;

$interviewer = function() use ($output, $question, $fallback, $that) {
return $that->askHiddenResponse($output, $question, $fallback);
};

return $this->validateAttempts($interviewer, $output, $validator, $attempts);
}

/**
Expand Down Expand Up @@ -136,4 +230,71 @@ public function getName()
{
return 'dialog';
}

/**
* Return a valid unix shell
*
* @return string|false 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;
}

private function hasSttyAvailable()
{
if (null !== self::$stty) {
return self::$stty;
}

exec('/usr/bin/env stty', $output, $exicode);

return self::$stty = $exicode === 0;
}

/**
* Validate an attempt
*
* @param callable $interviewer A callable that will ask for a question and return the result
* @param OutputInterface $output An Output instance
* @param callable $validator A PHP callback
* @param integer $attempts Max number of times to ask before giving up ; false will ask infinitely
*
* @return string The validated response
*
* @throws \Exception In case the max number of attempts has been reached and no valid response has been given
*/
private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts)
{
$error = null;
while (false === $attempts || $attempts--) {
if (null !== $error) {
$output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
}

try {
return call_user_func($validator, $interviewer());
} catch (\Exception $error) {
}
}

throw $error;
}
}
685C 8 changes: 7 additions & 1 deletion src/Symfony/Component/Console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,18 @@ output abstractions (so that you can easily unit-test your commands),
validation, automatic help messages, ...

Tests
---------
-----

You can run the unit tests with the following command:

phpunit

Third Party
-----------

`Resources/bin/hiddeninput.exe` is a third party binary provided within this
component. Find sources and license at https://github.com/Seldaek/hidden-input.

Resources
---------

Expand Down
Binary file not shown.
13 changes: 13 additions & 0 deletions src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ public function testAsk()
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
}

public function testAskHiddenResponse()
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$this->markTestSkipped('This test is not supported on Windows');
}

$dialog = new DialogHelper();

$dialog->setInputStream($this->getInputStream("8AM\n"));

$this->assertEquals('8AM', $dialog->askHiddenResponse($this->getOutputStream(), 'What time is it?'));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be skipped on Windows as it would trigger the binary, which will not work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fixed


public function testAskConfirmation()
{
$dialog = new DialogHelper();
Expand Down
0