10000 [Console] A better progress bar by fabpot · Pull Request #10356 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] A better progress bar #10356

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 9 commits into from
Mar 3, 2014
Prev Previous commit
Next Next commit
[Console] refactored the progress bar to allow placeholder to be exte…
…nsible
  • Loading branch information
fabpot committed Mar 1, 2014
commit 2a78a09f1b3c68ba734e1edeb68fa93a6c474e15
27 changes: 27 additions & 0 deletions src/Symfony/Component/Console/Helper/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,31 @@ public static function strlen($string)

return mb_strlen($string, $encoding);
}

public static function formatTime($secs)
{
static $timeFormats = array(
array(0, '< 1 sec'),
array(2, '1 sec'),
array(59, 'secs', 1),
array(60, '1 min'),
array(3600, 'mins', 60),
array(5400, '1 hr'),
array(86400, 'hrs', 3600),
array(129600, '1 day'),
array(604800, 'days', 86400),
);

foreach ($timeFormats as $format) {
if ($secs >= $format[0]) {
continue;
}

if (2 == count($format)) {
return $format[1];
}

return ceil($secs / $format[2]).' '.$format[1];
}
}
}
256 changes: 143 additions & 113 deletions src/Symfony/Component/Console/Helper/ProgressBar.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,61 +44,96 @@ class Pro 8000 gressBar
private $step;
private $max;
private $startTime;
private $stepWidth;
private $percent;
private $lastMessagesLength;
private $barCharOriginal;

static private $formatters;

/**
* List of formatting variables
* Constructor.
*
* @var array
* @param OutputInterface $output An OutputInterface instance
* @param integer $max Maximum steps (0 if unknown)
*/
private $defaultFormatVars = array(
'current',
'max',
'bar',
'percent',
'elapsed',
);
public function __construct(OutputInterface $output, $max = 0)
{
// Disabling output when it does not support ANSI codes as it would result in a broken display anyway.
$this->output = $output->isDecorated() ? $output : new NullOutput();
$this->max = (int) $max;
$this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4;

if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
}
}

/**
* Available formatting variables
* Sets a placeholder formatter for a given name.
*
* This method also allow you to override an existing placeholder.
*
* @var array
* @param string $name The placeholder name (including the delimiter char like %)
* @param callable $callable A PHP callable
*/
private $formatVars;
public static function setPlaceholderFormatter($name, $callable)
{
if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
}

self::$formatters[$name] = $callable;
Copy link
Member

Choose a reason for hiding this comment

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

The method initPlaceholderFormatters should be called here if the class has not been instantiated yet.

Use case:

ProgressBar::setPlaceholderFormatter('%cpu%', function (ProgressBar $bar) {});
$bar = new ProgressBar($output, 100);

}

/**
* Various time formats
* Gets the progress bar start time.
*
* @var array
* @return int The progress bar start time
*/
private $timeFormats = array(
array(0, '???'),
array(2, '1 sec'),
array(59, 'secs', 1),
array(60, '1 min'),
array(3600, 'mins', 60),
array(5400, '1 hr'),
array(86400, 'hrs', 3600),
array(129600, '1 day'),
array(604800, 'days', 86400),
);
public function getStartTime()
{
return $this->startTime;
}

private $stepWidth;
private $percent;
/**
* Gets the progress bar maximal steps.
*
* @return int The progress bar max steps
*/
public function getMaxSteps()
{
return $this->max;
}

/**
* Constructor.
* Gets the progress bar step.
*
* @param OutputInterface $output An OutputInterface instance
* @param integer $max Maximum steps (0 if unknown)
* @return int The progress bar step
*/
public function __construct(OutputInterface $output, $max = 0)
public function getStep()
{
// Disabling output when it does not support ANSI codes as it would result in a broken display anyway.
$this->output = $output->isDecorated() ? $output : new NullOutput();
$this->max = (int) $max;
$this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4;
return $this->step;
}

/**
* Gets the progress bar step width.
*
* @return int The progress bar step width
*/
public function getStepWidth()
{
return $this->stepWidth;
}

/**
* Gets the current progress bar percent.
*
* @return int The current progress bar percent
*/
public function getProgressPercent()
{
return $this->percent;
}

/**
Expand All @@ -111,6 +146,16 @@ public function setBarWidth($size)
$this->barWidth = (int) $size;
}

/**
* Gets the progress bar width.
*
* @return int The progress bar size
*/
public function getBarWidth()
{
return $this->barWidth;
}

/**
* Sets the bar character.
*
Expand All @@ -121,6 +166,16 @@ public function setBarCharacter($char)
$this->barChar = $char;
}

/**
* Gets the bar character.
*
* @return string A character
*/
public function getBarCharacter()
{
return $this->barChar;
}

/**
* Sets the empty bar character.
*
Expand All @@ -131,6 +186,16 @@ public function setEmptyBarCharacter($char)
$this->emptyBarChar = $char;
}

/**
* Gets the empty bar character.
*
* @return string A character
*/
public function getEmptyBarCharacter()
{
return $this->emptyBarChar;
}

/**
* Sets the progress bar character.
*
Expand All @@ -141,6 +206,16 @@ public function setProgressCharacter($char)
$this->progressChar = $char;
}

/**
* Gets the progress bar character.
*
* @return string A character
*/
public function getProgressCharacter()
{
return $this->progressChar;
}

/**
* Sets the progress bar format.
*
Expand Down Expand Up @@ -176,13 +251,6 @@ public function start()
$this->format = $this->determineBestFormat();
}

$this->formatVars = array();
foreach ($this->defaultFormatVars as $var) {
if (false !== strpos($this->format, "%{$var}%")) {
$this->formatVars[$var] = true;
}
}

if (!$this->max) {
$this->barCharOriginal = $this->barChar;
$this->barChar = $this->emptyBarChar;
Expand Down Expand Up @@ -267,11 +335,11 @@ public function display()
throw new \LogicException('You must start the progress bar before calling display().');
}

$message = $this->format;
foreach ($this->generate() as $name => $value) {
$message = str_replace("%{$name}%", $value, $message);
}
$this->overwrite($message);
$regex = implode('|', array_keys(self::$formatters));
$self = $this;
$this->overwrite(preg_replace_callback("{($regex)}", function ($matches) use ($self) {
return call_user_func(self::$formatters[$matches[1]], $self);
}, $this->format));
}

/**
Expand All @@ -286,72 +354,6 @@ public function clear()
$this->overwrite('');
}

/**
* Generates the array map of format variables to values.
*
* @return array Array of format vars and values
*/
private function generate()
{
$vars = array();

if (isset($this->formatVars['bar'])) {
$completeBars = floor($this->max > 0 ? $this->percent * $this->barWidth : $this->step % $this->barWidth);
$emptyBars = $this->barWidth - $completeBars - Helper::strlen($this->progressChar);
$bar = str_repeat($this->barChar, $completeBars);
if ($completeBars < $this->barWidth) {
$bar .= $this->progressChar;
$bar .= str_repeat($this->emptyBarChar, $emptyBars);
}

$vars['bar'] = $bar;
}

if (isset($this->formatVars['elapsed'])) {
$elapsed = time() - $this->startTime;
$vars['elapsed'] = str_pad($this->humaneTime($elapsed), 6, ' ', STR_PAD_LEFT);
}

if (isset($this->formatVars['current'])) {
$vars['current'] = str_pad($this->step, $this->stepWidth, ' ', STR_PAD_LEFT);
}

if (isset($this->formatVars['max'])) {
$vars['max'] = $this->max;
}

if (isset($this->formatVars['percent'])) {
$vars['percent'] = str_pad(floor($this->percent * 100), 3, ' ', STR_PAD_LEFT);
}

return $vars;
}

/**
* Converts seconds into human-readable format.
*
* @param integer $secs Number of seconds
*
* @return string Time in readable format
*/
private function humaneTime($secs)
{
$text = '';
foreach ($this->timeFormats as $format) {
if ($secs < $format[0]) {
if (count($format) == 2) {
$text = $format[1];
break;
} else {
$text = ceil($secs / $format[2]).' '.$format[1];
break;
}
}
}

return $text;
}

/**
* Overwrites a previous message to the output.
*
Expand Down Expand Up @@ -400,4 +402,32 @@ private function determineBestFormat()

return $format;
}

static private function initPlaceholderFormatters()
{
return array(
'%bar%' => function (ProgressBar $bar) {
$completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth());
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter());
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
}

return $display;
},
'%elapsed%' => function (ProgressBar $bar) {
return str_pad(Helper::formatTime(time() - $bar->getStartTime()), 6, ' ', STR_PAD_LEFT);
},
'%current%' => function (ProgressBar $bar) {
return str_pad($bar->getStep(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
},
'%max%' => function (ProgressBar $bar) {
return $bar->getMaxSteps();
},
'%percent%' => function (ProgressBar $bar) {
return str_pad(floor($bar->getProgressPercent() * 100), 3, ' ', STR_PAD_LEFT);
},
);
}
}
21 changes: 21 additions & 0 deletions src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,27 @@ public function testParallelBars()
);
}

public function testAddingPlaceholderFormatter()
{
ProgressBar::setPlaceholderFormatter('%remaining_steps%', function (ProgressBar $bar) {
return $bar->getMaxSteps() - $bar->getStep();
});
$bar = new ProgressBar($output = $this->getOutputStream(), 3);
$bar->setFormat(' %remaining_steps% [%bar%]');

$bar->start();
$bar->advance();
$bar->finish();

rewind($output->getStream());
$this->assertEquals(
$this->generateOutput(' 3 [>---------------------------]').
$this->generateOutput(' 2 [=========>------------------]').
$this->generateOutput(' 0 [============================]'),
stream_get_contents($output->getStream())
);
}

protected function getOutputStream($decorated = true)
{
return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated);
Expand Down
2A4E
0