bug #14623 [Console] SymfonyStyle : fix & automate block gaps. (ogizanagi)
This PR was merged into the 2.7 branch. Discussion ---------- [Console] SymfonyStyle : fix & automate block gaps. | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - --- Depends on https://github.com/symfony/symfony/pull/14741 --- ## What it does - autoprepend appropriate blocks (like cautions, titles, sections, ...) by the correct number of blank lines considering history. - handle automatically most of the SymfonyStyle guide line breaks and gaps. Fix things such as unwanted double blank lines between titles and admonitions. - test outputs - fix an issue using questions with SymfonyStyle, which should not output extra blank lines when using with a non-interactive input. ## Description `SymfonyStyle` is great, but there are some issues, mostly when using blocks (text blocks, titles and admonitions): some extra blank lines might be generated. Plus, on the contrary, some line breaks or blank lines around blocks are missing, and the developer need to handle this himself by polluting his code with ugly `if` and `newLine()` statements. ### Before / After : ![screenshot 2015-05-13 a 00 11 59](https://cloud.githubusercontent.com/assets/2211145/7600572/ccfa8904-f90c-11e4-999f-d89612360424.PNG) As you can see, it's still up to the developper to end his command by a blank line (unless using a block like `SymfonyStyle::success()`) in order to distinct different commands outputs more efficiently. Everything else is now handled properly, and automatically, according to the rules exposed in the symfony console style guide published some time ago by @javiereguiluz . Questions (not exposed in the above output) are considered as blocks, and follow, for instance, the same conditions than admonitions: 1 blank line before and after, no more (although, you'll still be able to output more blank lines yourself, using `newLine`). Commits -------fc598ff
[Console] SymfonyStyle : fix & automate block gaps.260702e
[Console] SymfonyStyle : Improve EOL consistency by relying on output instance
This commit is contained in:
commit
c62069bc52
@ -18,6 +18,7 @@ use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
@ -36,6 +37,7 @@ class SymfonyStyle extends OutputStyle
|
||||
private $questionHelper;
|
||||
private $progressBar;
|
||||
private $lineLength;
|
||||
private $bufferedOutput;
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
@ -45,6 +47,7 @@ class SymfonyStyle extends OutputStyle
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->lineLength = min($this->getTerminalWidth(), self::MAX_LINE_LENGTH);
|
||||
$this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
|
||||
|
||||
parent::__construct($output);
|
||||
}
|
||||
@ -60,6 +63,7 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false)
|
||||
{
|
||||
$this->autoPrependBlock();
|
||||
$messages = is_array($messages) ? array_values($messages) : array($messages);
|
||||
$lines = array();
|
||||
|
||||
@ -71,7 +75,7 @@ class SymfonyStyle extends OutputStyle
|
||||
// wrap and add newlines for each element
|
||||
foreach ($messages as $key => $message) {
|
||||
$message = OutputFormatter::escape($message);
|
||||
$lines = array_merge($lines, explode("\n", wordwrap($message, $this->lineLength - Helper::strlen($prefix))));
|
||||
$lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - Helper::strlen($prefix), PHP_EOL)));
|
||||
|
||||
if (count($messages) > 1 && $key < count($messages) - 1) {
|
||||
$lines[] = '';
|
||||
@ -92,7 +96,8 @@ class SymfonyStyle extends OutputStyle
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeln(implode("\n", $lines)."\n");
|
||||
$this->writeln($lines);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,7 +105,12 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function title($message)
|
||||
{
|
||||
$this->writeln(sprintf("\n<comment>%s</>\n<comment>%s</>\n", $message, str_repeat('=', strlen($message))));
|
||||
$this->autoPrependBlock();
|
||||
$this->writeln(array(
|
||||
sprintf('<comment>%s</>', $message),
|
||||
sprintf('<comment>%s</>', str_repeat('=', strlen($message))),
|
||||
));
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,7 +118,12 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function section($message)
|
||||
{
|
||||
$this->writeln(sprintf("<comment>%s</>\n<comment>%s</>\n", $message, str_repeat('-', strlen($message))));
|
||||
$this->autoPrependBlock();
|
||||
$this->writeln(array(
|
||||
sprintf('<comment>%s</>', $message),
|
||||
sprintf('<comment>%s</>', str_repeat('-', strlen($message))),
|
||||
));
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,13 +131,13 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function listing(array $elements)
|
||||
{
|
||||
$this->autoPrependText();
|
||||
$elements = array_map(function ($element) {
|
||||
return sprintf(' * %s', $element);
|
||||
},
|
||||
$elements
|
||||
);
|
||||
return sprintf(' * %s', $element);
|
||||
}, $elements);
|
||||
|
||||
$this->writeln(implode("\n", $elements)."\n");
|
||||
$this->writeln($elements);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,6 +145,8 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function text($message)
|
||||
{
|
||||
$this->autoPrependText();
|
||||
|
||||
if (!is_array($message)) {
|
||||
$this->writeln(sprintf(' // %s', $message));
|
||||
|
||||
@ -292,17 +309,51 @@ class SymfonyStyle extends OutputStyle
|
||||
*/
|
||||
public function askQuestion(Question $question)
|
||||
{
|
||||
if ($this->input->isInteractive()) {
|
||||
$this->autoPrependBlock();
|
||||
}
|
||||
|
||||
if (!$this->questionHelper) {
|
||||
$this->questionHelper = new SymfonyQuestionHelper();
|
||||
}
|
||||
|
||||
$answer = $this->questionHelper->ask($this->input, $this, $question);
|
||||
|
||||
$this->newLine();
|
||||
if ($this->input->isInteractive()) {
|
||||
$this->newLine();
|
||||
$this->bufferedOutput->write("\n");
|
||||
}
|
||||
|
||||
return $answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeln($messages, $type = self::OUTPUT_NORMAL)
|
||||
{
|
||||
parent::writeln($messages, $type);
|
||||
$this->bufferedOutput->writeln($this->reduceBuffer($messages), $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
|
||||
{
|
||||
parent::write($messages, $newline, $type);
|
||||
$this->bufferedOutput->write($this->reduceBuffer($messages), $newline, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function newLine($count = 1)
|
||||
{
|
||||
parent::newLine($count);
|
||||
$this->bufferedOutput->write(str_repeat("\n", $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProgressBar
|
||||
*/
|
||||
@ -322,4 +373,33 @@ class SymfonyStyle extends OutputStyle
|
||||
|
||||
return $dimensions[0] ?: self::MAX_LINE_LENGTH;
|
||||
}
|
||||
|
||||
private function autoPrependBlock()
|
||||
{
|
||||
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
|
||||
|
||||
if (false === $chars) {
|
||||
return $this->newLine(); //empty history, so we should start with a new line.
|
||||
}
|
||||
//Prepend new line for each non LF chars (This means no blank line was output before)
|
||||
$this->newLine(2 - substr_count($chars, "\n"));
|
||||
}
|
||||
|
||||
private function autoPrependText()
|
||||
{
|
||||
$fetched = $this->bufferedOutput->fetch();
|
||||
//Prepend new line if last char isn't EOL:
|
||||
if ("\n" !== substr($fetched, -1)) {
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
private function reduceBuffer($messages)
|
||||
{
|
||||
// We need to know if the two last chars are PHP_EOL
|
||||
// Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer
|
||||
return array_map(function ($value) {
|
||||
return substr($value, -4);
|
||||
}, array_merge(array($this->bufferedOutput->fetch()), (array) $messages));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has single blank line at start when using block element
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
$output->caution('Lorem ipsum dolor sit amet');
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has single blank line between titles and blocks
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
$output->title('Title');
|
||||
$output->warning('Lorem ipsum dolor sit amet');
|
||||
$output->title('Title');
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has single blank line between blocks
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
$output->warning('Warning');
|
||||
$output->caution('Caution');
|
||||
$output->error('Error');
|
||||
$output->success('Success');
|
||||
$output->note('Note');
|
||||
$output->block('Custom block', 'CUSTOM', 'fg=white;bg=green', 'X ', true);
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has single blank line between two titles
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
$output->title('First title');
|
||||
$output->title('Second title');
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has single blank line after any text and a title
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
|
||||
$output->write('Lorem ipsum dolor sit amet');
|
||||
$output->title('First title');
|
||||
|
||||
$output->writeln('Lorem ipsum dolor sit amet');
|
||||
$output->title('Second title');
|
||||
|
||||
$output->write('Lorem ipsum dolor sit amet');
|
||||
$output->write('');
|
||||
$output->title('Third title');
|
||||
|
||||
//Ensure edge case by appending empty strings to history:
|
||||
$output->write('Lorem ipsum dolor sit amet');
|
||||
$output->write(array('', '', ''));
|
||||
$output->title('Fourth title');
|
||||
|
||||
//Ensure have manual control over number of blank lines:
|
||||
$output->writeln('Lorem ipsum dolor sit amet');
|
||||
$output->writeln(array('', '')); //Should append an extra blank line
|
||||
$output->title('Fifth title');
|
||||
|
||||
$output->writeln('Lorem ipsum dolor sit amet');
|
||||
$output->newLine(2); //Should append an extra blank line
|
||||
$output->title('Fifth title');
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has proper line ending before outputing a text block like with SymfonyStyle::listing() or SymfonyStyle::text()
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
|
||||
$output->writeln('Lorem ipsum dolor sit amet');
|
||||
$output->listing(array(
|
||||
'Lorem ipsum dolor sit amet',
|
||||
'consectetur adipiscing elit',
|
||||
));
|
||||
|
||||
//Even using write:
|
||||
$output->write('Lorem ipsum dolor sit amet');
|
||||
$output->listing(array(
|
||||
'Lorem ipsum dolor sit amet',
|
||||
'consectetur adipiscing elit',
|
||||
));
|
||||
|
||||
$output->write('Lorem ipsum dolor sit amet');
|
||||
$output->text(array(
|
||||
'Lorem ipsum dolor sit amet',
|
||||
'consectetur adipiscing elit',
|
||||
));
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure has proper blank line after text block when using a block like with SymfonyStyle::success
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
|
||||
$output->listing(array(
|
||||
'Lorem ipsum dolor sit amet',
|
||||
'consectetur adipiscing elit',
|
||||
));
|
||||
$output->success('Lorem ipsum dolor sit amet');
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
//Ensure questions do not output anything when input is non-interactive
|
||||
return function (InputInterface $input, OutputInterface $output) {
|
||||
$output = new SymfonyStyle($input, $output);
|
||||
$output->title('Title');
|
||||
$output->askHidden('Hidden question');
|
||||
$output->choice('Choice question with default', array('choice1', 'choice2'), 'choice1');
|
||||
$output->confirm('Confirmation with yes default', true);
|
||||
$output->text('Duis aute irure dolor in reprehenderit in voluptate velit esse');
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
|
||||
! [CAUTION] Lorem ipsum dolor sit amet
|
||||
|
@ -0,0 +1,9 @@
|
||||
|
||||
Title
|
||||
=====
|
||||
|
||||
[WARNING] Lorem ipsum dolor sit amet
|
||||
|
||||
Title
|
||||
=====
|
||||
|
@ -0,0 +1,13 @@
|
||||
|
||||
[WARNING] Warning
|
||||
|
||||
! [CAUTION] Caution
|
||||
|
||||
[ERROR] Error
|
||||
|
||||
[OK] Success
|
||||
|
||||
! [NOTE] Note
|
||||
|
||||
X [CUSTOM] Custom block
|
||||
|
@ -0,0 +1,7 @@
|
||||
|
||||
First title
|
||||
===========
|
||||
|
||||
Second title
|
||||
============
|
||||
|
@ -0,0 +1,32 @@
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
First title
|
||||
===========
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
Second title
|
||||
============
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
Third title
|
||||
===========
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
Fourth title
|
||||
============
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
|
||||
Fifth title
|
||||
===========
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
|
||||
|
||||
Fifth title
|
||||
===========
|
||||
|
@ -0,0 +1,11 @@
|
||||
Lorem ipsum dolor sit amet
|
||||
* Lorem ipsum dolor sit amet
|
||||
* consectetur adipiscing elit
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
* Lorem ipsum dolor sit amet
|
||||
* consectetur adipiscing elit
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
// Lorem ipsum dolor sit amet
|
||||
// consectetur adipiscing elit
|
@ -0,0 +1,6 @@
|
||||
|
||||
* Lorem ipsum dolor sit amet
|
||||
* consectetur adipiscing elit
|
||||
|
||||
[OK] Lorem ipsum dolor sit amet
|
||||
|
@ -0,0 +1,5 @@
|
||||
|
||||
Title
|
||||
=====
|
||||
|
||||
// Duis aute irure dolor in reprehenderit in voluptate velit esse
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Console\Tests\Style;
|
||||
|
||||
use PHPUnit_Framework_TestCase;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class SymfonyStyleTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/** @var Command */
|
||||
protected $command;
|
||||
/** @var CommandTester */
|
||||
protected $tester;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->command = new Command('sfstyle');
|
||||
$this->tester = new CommandTester($this->command);
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
$this->command = null;
|
||||
$this->tester = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider inputCommandToOutputFilesProvider
|
||||
*/
|
||||
public function testOutputs($inputCommandFilepath, $outputFilepath)
|
||||
{
|
||||
$code = require $inputCommandFilepath;
|
||||
$this->command->setCode($code);
|
||||
$this->tester->execute(array(), array('interactive' => false, 'decorated' => false));
|
||||
$this->assertStringEqualsFile($outputFilepath, $this->tester->getDisplay(true));
|
||||
}
|
||||
|
||||
public function inputCommandToOutputFilesProvider()
|
||||
{
|
||||
$baseDir = __DIR__.'/../Fixtures/Style/SymfonyStyle';
|
||||
|
||||
return array_map(null, glob($baseDir.'/command/command_*.php'), glob($baseDir.'/output/output_*.txt'));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user