Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a5dbc68

Browse filesBrowse files
committed
feature #24363 [Console] Modify console output and print multiple modifyable sections (pierredup)
This PR was squashed before being merged into the 4.1-dev branch (closes #24363). Discussion ---------- [Console] Modify console output and print multiple modifyable sections | Q | A | ------------- | --- | Branch? | 4.1 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | TBD | Fixed tickets | | License | MIT | Doc PR | symfony/symfony-docs#9304 Add support to create different output sections for the console output. Each section is it's own 'stream' of output, where the output can be modified (even if there are other output after it). This allows you to modify previous output in the console, either by appending new lines, modifying previous lines or clearing the output. Modifying a sections output doesn't affect the output after that or in other sections. Some examples of what can be done: **Overwriting content in a previous section:** Code: ```php $section1 = $output->section(); $section2 = $output->section(); $section1->writeln("<comment>Doing something</comment>\n"); usleep(500000); $section2->writeln('<info>Result of first operation</info>'); usleep(500000); $section1->overwrite("<comment>Doing something else</comment>\n"); usleep(500000); $section2->writeln('<info>Result of second operation</info>'); usleep(500000); $section1->overwrite("<comment>Finishing</comment>\n"); usleep(500000); $section2->writeln('<info>Last Result</info>'); ``` Result: ![overwrite-append](https://user-images.githubusercontent.com/144858/30975030-769f2c46-a471-11e7-819f-c3698b43f0af.gif) **Multiple Progress Bars:** Code: ```php $section1 = $output->section(); $section2 = $output->section(); $progress = new ProgressBar($section1); $progress2 = new ProgressBar($section2); $progress->start(100); $progress2->start(100); $c = 0; while (++$c < 100) { $progress->advance(); if ($c % 2 === 0) { $progress2->advance(4); } usleep(500000); } ``` Result: ![multiple-progress](https://user-images.githubusercontent.com/144858/30975119-b63222be-a471-11e7-89aa-a555cdf3d2e0.gif) **Modifying content of a table & updating a progress bar:** Code: ```php $section1 = $output->section(); $section2 = $output->section(); $progress = new ProgressBar($section1); $table = new Table($section2); $table->addRow(['Row 1']); $table->render(); $progress->start(5); $c = 0; while (++$c < 5) { $table->appendRow(['Row '.($c + 1)]); $progress->advance(); usleep(500000); } $progress->finish(); $section1->clear(); ``` Result: ![progress-table](https://user-images.githubusercontent.com/144858/30975176-e332499c-a471-11e7-9d4f-f58b464a53c2.gif) **Example with Symfony Installer:*** Before: ![sf-installer-old](https://user-images.githubusercontent.com/144858/30975291-40f22106-a472-11e7-8836-bc39139c2d30.gif) After: ![sf-installer](https://user-images.githubusercontent.com/144858/30975302-4a00acf4-a472-11e7-83ba-88ea9d0f0f3f.gif) TODO: - [x] Add unit tests Commits ------- 9ec51a1 [Console] Modify console output and print multiple modifyable sections
2 parents 1fffb85 + 9ec51a1 commit a5dbc68
Copy full SHA for a5dbc68

File tree

9 files changed

+531
-8
lines changed
Filter options

9 files changed

+531
-8
lines changed

‎src/Symfony/Component/Console/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added option to run suggested command if command is not found and only 1 alternative is available
8+
* added option to modify console output and print multiple modifiable sections
89

910
4.0.0
1011
-----

‎src/Symfony/Component/Console/Helper/ProgressBar.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Helper/ProgressBar.php
+13-7Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Console\Helper;
1313

1414
use Symfony\Component\Console\Output\ConsoleOutputInterface;
15+
use Symfony\Component\Console\Output\ConsoleSectionOutput;
1516
use Symfony\Component\Console\Output\OutputInterface;
1617
use Symfony\Component\Console\Exception\LogicException;
1718
use Symfony\Component\Console\Terminal;
@@ -376,15 +377,20 @@ private function overwrite(string $message): void
376377
{
377378
if ($this->overwrite) {
378379
if (!$this->firstRun) {
379-
// Move the cursor to the beginning of the line
380-
$this->output->write("\x0D");
380+
if ($this->output instanceof ConsoleSectionOutput) {
381+
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
382+
$this->output->clear($lines);
383+
} else {
384+
// Move the cursor to the beginning of the line
385+
$this->output->write("\x0D");
381386

382-
// Erase the line
383-
$this->output->write("\x1B[2K");
387+
// Erase the line
388+
$this->output->write("\x1B[2K");
384389

385-
// Erase previous lines
386-
if ($this->formatLineCount > 0) {
387-
$this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount));
390+
// Erase previous lines
391+
if ($this->formatLineCount > 0) {
392+
$this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount));
393+
}
388394
}
389395
}
390396
} elseif ($this->step > 0) {

‎src/Symfony/Component/Console/Helper/Table.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Helper/Table.php
+37Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace Symfony\Component\Console\Helper;
1313

14+
use Symfony\Component\Console\Output\ConsoleSectionOutput;
1415
use Symfony\Component\Console\Output\OutputInterface;
1516
use Symfony\Component\Console\Exception\InvalidArgumentException;
17+
use Symfony\Component\Console\Exception\RuntimeException;
1618

1719
/**
1820
* Provides helpers to display a table.
@@ -75,6 +77,8 @@ class Table
7577

7678
private static $styles;
7779

80+
private $rendered = false;
81+
7882
public function __construct(OutputInterface $output)
7983
{
8084
$this->output = $output;
@@ -257,6 +261,25 @@ public function addRow($row)
257261
return $this;
258262
}
259263

264+
/**
265+
* Adds a row to the table, and re-renders the table.
266+
*/
267+
public function appendRow($row): self
268+
{
269+
if (!$this->output instanceof ConsoleSectionOutput) {
270+
throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
271+
}
272+
273+
if ($this->rendered) {
274+
$this->output->clear($this->calculateRowCount());
275+
}
276+
277+
$this->addRow($row);
278+
$this->render();
279+
280+
return $this;
281+
}
282+
260283
public function setRow($column, array $row)
261284
{
262285
$this->rows[$column] = $row;
@@ -316,6 +339,7 @@ public function render()
316339
$this->renderRowSeparator(self::SEPARATOR_BOTTOM);
317340

318341
$this->cleanup();
342+
$this->rendered = true;
319343
}
320344

321345
/**
@@ -460,6 +484,19 @@ private function buildTableRows($rows)
460484
});
461485
}
462486

487+
private function calculateRowCount(): int
488+
{
489+
$numberOfRows = count(iterator_to_array($this->buildTableRows(array_merge($this->headers, array(new TableSeparator()), $this->rows))));
490+
491+
if ($this->headers) {
492+
++$numberOfRows; // Add row for header separator
493+
}
494+
495+
++$numberOfRows; // Add row for footer separator
496+
497+
return $numberOfRows;
498+
}
499+
463500
/**
464501
* fill rows that contains rowspan > 1.
465502
*

‎src/Symfony/Component/Console/Output/ConsoleOutput.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Output/ConsoleOutput.php
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
3131
{
3232
private $stderr;
33+
private $consoleSectionOutputs = array();
3334

3435
/**
3536
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
@@ -48,6 +49,14 @@ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decor
4849
}
4950
}
5051

52+
/**
53+
* Creates a new output section.
54+
*/
55+
public function section(): ConsoleSectionOutput
56+
{
57+
return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
58+
}
59+
5160
/**
5261
* {@inheritdoc}
5362
*/

‎src/Symfony/Component/Console/Output/ConsoleOutputInterface.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Output/ConsoleOutputInterface.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
/**
1515
* ConsoleOutputInterface is the interface implemented by ConsoleOutput class.
16-
* This adds information about stderr output stream.
16+
* This adds information about stderr and section output stream.
1717
*
1818
* @author Dariusz Górecki <darek.krk@gmail.com>
19+
*
20+
* @method ConsoleSectionOutput section() Creates a new output section
1921
*/
2022
interface ConsoleOutputInterface extends OutputInterface
2123
{
+133Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Output;
13+
14+
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
15+
use Symfony\Component\Console\Helper\Helper;
16+
use Symfony\Component\Console\Terminal;
17+
18+
/**
19+
* @author Pierre du Plessis <pdples@gmail.com>
20+
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
21+
*/
22+
class ConsoleSectionOutput extends StreamOutput
23+
{
24+
private $content = array();
25+
private $lines = 0;
26+
private $sections;
27+
private $terminal;
28+
29+
/**
30+
* @param resource $stream
31+
* @param ConsoleSectionOutput[] $sections
32+
*/
33+
public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
34+
{
35+
parent::__construct($stream, $verbosity, $decorated, $formatter);
36+
array_unshift($sections, $this);
37+
$this->sections = &$sections;
38+
$this->terminal = new Terminal();
39+
}
40+
41+
/**
42+
* Clears previous output for this section.
43+
*
44+
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
45+
*/
46+
public function clear(int $lines = null)
47+
{
48+
if (empty($this->content) || !$this->isDecorated()) {
49+
return;
50+
}
51+
52+
if ($lines) {
53+
\array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
54+
} else {
55+
$lines = $this->lines;
56+
$this->content = array();
57+
}
58+
59+
$this->lines -= $lines;
60+
61+
parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
62+
}
63+
64+
/**
65+
* Overwrites the previous output with a new message.
66+
*
67+
* @param array|string $message
68+
*/
69+
public function overwrite($message)
70+
{
71+
$this->clear();
72+
$this->writeln($message);
73+
}
74+
75+
public function getContent(): string
76+
{
77+
return implode('', $this->content);
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
protected function doWrite($message, $newline)
84+
{
85+
if (!$this->isDecorated()) {
86+
return parent::doWrite($message, $newline);
87+
}
88+
89+
$erasedContent = $this->popStreamContentUntilCurrentSection();
90+
91+
foreach (explode(PHP_EOL, $message) as $lineContent) {
92+
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
93+
$this->content[] = $lineContent;
94+
$this->content[] = PHP_EOL;
95+
}
96+
97+
parent::doWrite($message, true);
98+
parent::doWrite($erasedContent, false);
99+
}
100+
101+
/**
102+
* At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
103+
* current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
104+
*/
105+
private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
106+
{
107+
$numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
108+
$erasedContent = array();
109+
110+
foreach ($this->sections as $section) {
111+
if ($section === $this) {
112+
break;
113+
}
114+
115+
$numberOfLinesToClear += $section->lines;
116+
$erasedContent[] = $section->getContent();
117+
}
118+
119+
if ($numberOfLinesToClear > 0) {
120+
// move cursor up n lines
121+
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
122+
// erase to end of screen
123+
parent::doWrite("\x1b[0J", false);
124+
}
125+
126+
return implode('', array_reverse($erasedContent));
127+
}
128+
129+
private function getDisplayLength(string $text): string
130+
{
131+
return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
132+
}
133+
}

‎src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
+84Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\Console\Tests\Helper;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Formatter\OutputFormatter;
1516
use Symfony\Component\Console\Helper\ProgressBar;
1617
use Symfony\Component\Console\Helper\Helper;
18+
use Symfony\Component\Console\Output\ConsoleSectionOutput;
1719
use Symfony\Component\Console\Output\StreamOutput;
1820

1921
/**
@@ -310,6 +312,88 @@ public function testOverwriteWithShorterLine()
310312
);
311313
}
312314

315+
public function testOverwriteWithSectionOutput()
316+
{
317+
$sections = array();
318+
$stream = $this->getOutputStream(true);
319+
$output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
320+
321+
$bar = new ProgressBar($output, 50);
322+
$bar->start();
323+
$bar->display();
324+
$bar->advance();
325+
$bar->advance();
326+
327+
rewind($output->getStream());
328+
$this->assertEquals(
329+
' 0/50 [>---------------------------] 0%'.PHP_EOL.
330+
"\x1b[1A\x1b[0J".' 0/50 [>---------------------------] 0%'.PHP_EOL.
331+
"\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL.
332+
"\x1b[1A\x1b[0J".' 2/50 [=>--------------------------] 4%'.PHP_EOL,
333+
stream_get_contents($output->getStream())
334+
);
335+
}
336+
337+
public function testOverwriteMultipleProgressBarsWithSectionOutputs()
338+
{
339+
$sections = array();
340+
$stream = $this->getOutputStream(true);
341+
$output1 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
342+
$output2 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
343+
344+
$progress = new ProgressBar($output1, 50);
345+
$progress2 = new ProgressBar($output2, 50);
346+
347+
$progress->start();
348+
$progress2->start();
349+
350+
$progress2->advance();
351+
$progress->advance();
352+
353+
rewind($stream->getStream());
354+
355+
$this->assertEquals(
356+
' 0/50 [>---------------------------] 0%'.PHP_EOL.
357+
' 0/50 [>---------------------------] 0%'.PHP_EOL.
358+
"\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL.
359+
"\x1b[2A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL.
360+
"\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL.
361+
' 1/50 [>---------------------------] 2%'.PHP_EOL,
362+
stream_get_contents($stream->getStream())
363+
);
364+
}
365+
366+
public function testMultipleSectionsWithCustomFormat()
367+
{
368+
$sections = array();
369+
$stream = $this->getOutputStream(true);
370+
$output1 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
371+
$output2 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
372+
373+
ProgressBar::setFormatDefinition('test', '%current%/%max% [%bar%] %percent:3s%% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.');
374+
375+
$progress = new ProgressBar($output1, 50);
376+
$progress2 = new ProgressBar($output2, 50);
377+
$progress2->setFormat('test');
378+
379+
$progress->start();
380+
$progress2->start();
381+
382+
$progress->advance();
383+
$progress2->advance();
384+
385+
rewind($stream->getStream());
386+
387+
$this->assertEquals(' 0/50 [>---------------------------] 0%'.PHP_EOL.
388+
' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL.
389+
"\x1b[4A\x1b[0J".' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL.
390+
"\x1b[3A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL.
391+
' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL.
392+
"\x1b[3A\x1b[0J".' 1/50 [>] 2% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL,
393+
stream_get_contents($stream->getStream())
394+
);
395+
}
396+
313397
public function testStartWithMax()
314398
{
315399
$bar = new ProgressBar($output = $this->getOutputStream());

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.