8000 [Console] Fix bug with utf-8 bug, change writePrompt to use one function prepareChoices by proggga · Pull Request #33911 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] Fix bug with utf-8 bug, change writePrompt to use one function prepareChoices #33911

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

Closed
wants to merge 6 commits into from
Closed
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
30 changes: 23 additions & 7 deletions src/Symfony/Component/Console/Helper/QuestionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,8 @@ protected function writePrompt(OutputInterface $output, Question $question)
$message = $question->getQuestion();

if ($question instanceof ChoiceQuestion) {
$maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices())));

$messages = (array) $question->getQuestion();
foreach ($question->getChoices() as $key => $value) {
$width = $maxWidth - $this->strlen($key);
$messages[] = ' [<info>'.$key.str_repeat(' ', $width).'</info>] '.$value;
}

$messages = array_merge($m 8000 essages, $this->prepareChoices('info', $question));
$output->writeln($messages);

$message = $question->getPrompt();
Expand All @@ -214,6 +208,28 @@ protected function writePrompt(OutputInterface $output, Question $question)
$output->write($message);
}

/**
* @param string $tag
*
* @return string[]
*/
protected function prepareChoices($tag, ChoiceQuestion $question)
{
$choices = $question->getChoices();

$maxWidth = max(array_map([$this, 'strlen'], array_keys($choices)));
Copy link
Contributor
@fancyweb fancyweb Dec 4, 2019

Choose a reason for hiding this comment

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

self::strlen?

Copy link
Member

Choose a reason for hiding this comment

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

[static::class, 'strlen'] (but honestly I've no issue with using $this)

Copy link
Contributor

Choose a reason for hiding this comment

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

We currently use self:: everywhere, we don't expect those methods to be overidden.


$messages = [];

foreach ($choices as $key => $value) {
$width = $maxWidth - $this->strlen($key);
Copy link
Contributor

Choose a reason for hiding this comment

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

self::strlen

// We using strlen + str_repeat to fix sprintf whitespace padding problem with UTF-8)
$messages[] = sprintf(' [<%s>%s%s</%s>] %s', $tag, $key, str_repeat(' ', $width), $tag, $value);
Copy link
Contributor

Choose a reason for hiding this comment

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

@nicolas-grekas sprintf left justify does not work with multibytes. Shouldn't we create a sprintf method in the abstract Helper class like we did for strlen, substr, etc.? Or using str_repeat for all strings is ok?

Copy link
Member

Choose a reason for hiding this comment

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

So, you want to implement sprintf in pure PHP.
Yes, sure... in theory :)
in practice, I don't know :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean add a sprintf helper method to handle the left justify. If the string is not utf-8 we use the normal sprintf. If it is, we use the str_repeat strategy.

Copy link
Member

Choose a reason for hiding this comment

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

OK, sure!

Copy link
Contributor
@fancyweb fancyweb Dec 4, 2019

Choose a reason for hiding this comment

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

@proggga Would you have time to take care of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi, thanks for reviewing this,
Yea, Maybe I can, but I'm not true symphonist, so I don't know rules for creating new helper/naming/best place for it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@proggga Would you have time to take care of this?

May be you can give instructions, and after may be I can do it

Copy link
Contributor

Choose a reason for hiding this comment

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

In your proposed changes, you replaces the sprintf left justify feat by strlen + str_repeat everytime. Firstly, this alternative should only be used for multibytes strings. Secondly, it should be done in a new helper method in the abstract Helper.php class (like we did for strlen and sbustr).

}

return $messages;
}

/**
* Outputs an error message.
*/
Expand Down
12 changes: 6 additions & 6 deletions src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ protected function writePrompt(OutputInterface $output, Question $question)
$output->writeln($text);

if ($question instanceof ChoiceQuestion) {
$width = max(array_map('strlen', array_keys($question->getChoices())));
$messages = $this->prepareChoices('comment', $question);

foreach ($question->getChoices() as $key => $value) {
$output->writeln(sprintf(" [<comment>%-${width}s</comment>] %s", $key, $value));
}
$output->writeln($messages);
// ChoiceQuestion can have any prompt
$output->write($question->getPrompt() ?: Question::DEFAULT_PROMPT);
Copy link
Contributor

Choose a reason for hiding this comment

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

getPrompt() will always return a string. The constant is also useless. Just write the prompt.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's another bug fixed by this PR btw. Unrelated to the linked issue.

} else {
$output->write(Question::DEFAULT_PROMPT);
Copy link
Contributor

Choose a reason for hiding this comment

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

And here keep the old behavior?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should actually move the prompt from the ChoiceQuestion to the Question class? 🤔

}

$output->write(' > ');
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/Question/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
class Question
{
const DEFAULT_PROMPT = ' > ';
private $question;
private $attempts;
private $hidden = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Symfony\Component\Console\Tests\Helper;

use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
Expand Down Expand Up @@ -108,9 +109,54 @@ public function testAskEscapeAndFormatLabel()
{
$helper = new SymfonyQuestionHelper();
$input = $this->createStreamableInputInterfaceMock($this->getInputStream('Foo\\Bar'));
$helper->ask($input, $output = $this->createOutputInterface(), new Question('Do you want to use Foo\\Bar <comment>or</comment> Foo\\Baz\\?', 'Foo\\Baz'));
$helper->ask($input, $output = $this->createOutputInterface(), new Question('Do you want to use Foo\\Bar <comment>or</comment> Foo\\Baz?', 'Foo\\Baz'));

$this->assertOutputContains('Do you want to use Foo\\Bar or Foo\\Baz\\? [Foo\\Baz]:', $output);
$this->assertOutputContains('Do you want to use Foo\\Bar or Foo\\Baz? [Foo\\Baz]:', $output);
}

public function testForUtf8Keys()
{
$question = 'Lorem ipsum?';
$possibleChoices = [
'foo' => 'foo',
'żółw' => 'bar',
'łabądź' => 'baz',
];
$question_result = ' <info>Lorem ipsum?</info> [<comment>foo</comment>]:';
$outputShown = [
' [<comment>foo </comment>] foo',
' [<comment>żółw </comment>] bar',
' [<comment>łabądź</comment>] baz',
];

$dialog = new SymfonyQuestionHelper();

$question = new ChoiceQuestion($question, $possibleChoices, 'foo');

$output = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')->getMock();
$output->method('getFormatter')->willReturn(new OutputFormatter());
$question->setValidator(null);

$input = $this->createStreamableInputInterfaceMock($this->getInputStream('foo'));
$result = [];
$output->method('writeln')->willReturnCallback(function ($params) use (&$result) {
if (\is_array($params)) {
$result = array_merge($result, $params);
} else {
$result[] = $params;
}
});
$dialog->ask($input, $output, $question);
$this->assertEquals(array_merge([$question_result], $outputShown), $result);
}

public function testAskDefaultPrompt()
{
$helper = new SymfonyQuestionHelper();
$input = $this->createStreamableInputInterfaceMock($this->getInputStream('Foo\\Bar'));
$helper->ask($input, $output = $this->createOutputInterface(), new Question('Do you want to use Foo\\Bar <comment>or</comment> Foo\\Baz?', 'Foo\\Baz'));

$this->assertOutputEndsWith("\n > ", $output);
}

public function testLabelTrailingBackslash()
Expand Down Expand Up @@ -159,8 +205,20 @@ protected function createInputInterfaceMock($interactive = true)

private function assertOutputContains($expected, StreamOutput $output)
{
rewind($output->getStream());
$stream = stream_get_contents($output->getStream());
$stream = $this->getOutputString($output);
$this->assertStringContainsString($expected, $stream);
}

private function assertOutputEndsWith($suffix, StreamOutput $output)
{
$stream = $this->getOutputString($output);
$this->assertStringEndsWith($suffix, $stream);
}

private function getOutputString(StreamOutput $output)
{
rewind($output->getStream());

return stream_get_contents($output->getStream()) ?: '';
}
}
0