From 4b89aae98c89877fee427670443fcd3c3df34ecb Mon Sep 17 00:00:00 2001 From: leek Date: Sun, 4 Mar 2012 19:02:59 -0500 Subject: [PATCH 1/3] [2.2][Console] Add ProgressHelper --- .../Console/Helper/ProgressHelper.php | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 src/Symfony/Component/Console/Helper/ProgressHelper.php diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php new file mode 100644 index 0000000000..43dc9fd287 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -0,0 +1,354 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * The Progress class providers helpers to display progress output. + * + * @author Chris Jones + */ +class ProgressHelper extends Helper +{ + const FORMAT_QUIET = ' %percent%%'; + const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%'; + const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%'; + const FORMAT_QUIET_NOMAX = ' %current%'; + const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; + const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; + + /** + * @var array + */ + protected $options = array( + 'barWidth' => null, + 'barChar' => null, + 'emptyBarChar' => null, + 'progressChar' => null, + 'format' => null, + 'redrawFreq' => null, + ); + + /** + * @var array + */ + private $defaultOptions = array( + 'barWidth' => 28, + 'barChar' => '=', + 'emptyBarChar' => '-', + 'progressChar' => '>', + 'format' => self::FORMAT_NORMAL_NOMAX, + 'redrawFreq' => 1, + ); + + /** + * @var OutputInterface + */ + private $output; + + /** + * Current step + * + * @var integer + */ + private $current; + + /** + * Maximum number of steps + * + * @var integer + */ + private $max; + + /** + * Start time of the progress bar + * + * @var integer + */ + private $startTime; + + /** + * List of formatting variables + * + * @var array + */ + private $defaultFormatVars = array( + 'current', + 'max', + 'bar', + 'percent', + 'elapsed', + ); + + /** + * Available formatting variables + * + * @var array + */ + private $formatVars; + + /** + * Stored format part widths (used for padding) + * + * @var array + */ + private $widths = array( + 'current' => 4, + 'max' => 4, + 'percent' => 3, + 'elapsed' => 6, + ); + + /** + * Various time formats + * + * @var array + */ + 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), + ); + + /** + * Starts the progress output. + * + * @param OutputInterface $output An Output instance + * @param integer $max Maximum steps + * @param array $options Options for progress helper + */ + public function start(OutputInterface $output, $max = null, array $options = array()) + { + $this->startTime = time(); + $this->current = 0; + $this->max = (int) $max; + $this->output = $output; + + switch ($output->getVerbosity()) { + case OutputInterface::VERBOSITY_QUIET: + $this->options['format'] = self::FORMAT_QUIET_NOMAX; + if ($this->max > 0) { + $this->options['format'] = self::FORMAT_QUIET; + } + break; + case OutputInterface::VERBOSITY_VERBOSE: + $this->options['format'] = self::FORMAT_VERBOSE_NOMAX; + if ($this->max > 0) { + $this->options['format'] = self::FORMAT_VERBOSE; + } + break; + default: + if ($this->max > 0) { + $this->options['format'] = self::FORMAT_NORMAL; + } + break; + } + + $this->options = array_merge($this->defaultOptions, $options); + $this->inititalize(); + } + + /** + * Advances the progress output X steps. + * + * @param integer $step Number of steps to advance + * @param Boolean $redraw Whether to redraw or not + */ + public function advance($step = 1, $redraw = false) + { + if ($this->current === 0) { + $redraw = true; + } + $this->current += $step; + if ($redraw || $this->current % $this->options['redrawFreq'] === 0) { + $this->display(); + } + } + + /** + * Outputs the current progress string. + * + * @param Boolean $finish Forces the end result + */ + public function display($finish = false) + { + $message = $this->options['format']; + foreach ($this->generate($finish) as $name => $value) { + $message = str_replace("%{$name}%", $value, $message); + } + $this->overwrite($this->output, $message); + } + + /** + * Finish the progress output + */ + public function finish() + { + if ($this->startTime !== null) { + if (!$this->max) { + $this->options['barChar'] = $this->options['barCharOriginal']; + $this->display(true); + } + $this->startTime = null; + $this->output->writeln(''); + $this->output = null; + } + } + + /** + * Initialize the progress helper. + */ + protected function inititalize() + { + $this->formatVars = array(); + foreach ($this->defaultFormatVars as $var) { + if (strpos($this->options['format'], "%{$var}%") !== false) { + $this->formatVars[$var] = true; + } + } + + if ($this->max > 0) { + $this->widths['max'] = strlen($this->max); + $this->widths['current'] = $this->widths['max']; + } else { + $this->options['barCharOriginal'] = $this->options['barChar']; + $this->options['barChar'] = $this->options['emptyBarChar']; + } + } + + /** + * Generates the array map of format variables to values. + * + * @param Boolean $finish Forces the end result + * @return array Array of format vars and values + */ + protected function generate($finish = false) + { + $vars = array(); + $percent = 0; + if ($this->max > 0) { + $percent = (double) round($this->current / $this->max, 1); + } + + if (isset($this->formatVars['bar'])) { + $completeBars = 0; + $emptyBars = 0; + if ($this->max > 0) { + $completeBars = floor($percent * $this->options['barWidth']); + } else { + if (!$finish) { + $completeBars = floor($this->current % $this->options['barWidth']); + } else { + $completeBars = $this->options['barWidth']; + } + } + + $emptyBars = $this->options['barWidth'] - $completeBars - strlen($this->options['progressChar']); + $bar = str_repeat($this->options['barChar'], $completeBars); + if ($completeBars < $this->options['barWidth']) { + $bar .= $this->options['progressChar']; + $bar .= str_repeat($this->options['emptyBarChar'], $emptyBars); + } + + $vars['bar'] = $bar; + } + + if (isset($this->formatVars['elapsed'])) { + $elapsed = time() - $this->startTime; + $vars['elapsed'] = str_pad($this->humaneTime($elapsed), $this->widths['elapsed'], ' ', STR_PAD_LEFT); + } + + if (isset($this->formatVars['current'])) { + $vars['current'] = str_pad($this->current, $this->widths['current'], ' ', STR_PAD_LEFT); + } + + if (isset($this->formatVars['max'])) { + $vars['max'] = $this->max; + } + + if (isset($this->formatVars['percent'])) { + $vars['percent'] = str_pad($percent * 100, $this->widths['percent'], ' ', 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. + * + * @param OutputInterface $output An Output instance + * @param string|array $messages The message as an array of lines or a single string + * @param Boolean $newline Whether to add a newline or not + * @param integer $size The size of line + */ + private function overwrite(OutputInterface $output, $messages, $newline = true, $size = 80) + { + for ($place = $size; $place > 0; $place--) { + $output->write("\x08", false); + } + + $output->write($messages, false); + + for ($place = ($size - strlen($messages)); $place > 0; $place--) { + $output->write(' ', false); + } + + // clean up the end line + for ($place = ($size - strlen($messages)); $place > 0; $place--) { + $output->write("\x08", false); + } + + if ($newline) { + $output->write(''); + } + } + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + */ + public function getName() + { + return 'progress'; + } +} From 729e3bfcafa3965051fef2b75ce7c743e36859b6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 29 Sep 2012 22:11:59 +0200 Subject: [PATCH 2/3] [Console] converted options to proper setters in ProgressHelper --- src/Symfony/Component/Console/Application.php | 2 + .../Console/Helper/ProgressHelper.php | 153 ++++++++++++------ 2 files changed, 107 insertions(+), 48 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 63f16e87d7..f8c3ecadc4 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\DialogHelper; +use Symfony\Component\Console\Helper\ProgressHelper; /** * An Application is the container for a collection of commands. @@ -934,6 +935,7 @@ class Application return new HelperSet(array( new FormatterHelper(), new DialogHelper(), + new ProgressHelper(), )); } diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 43dc9fd287..5411c7a276 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; * The Progress class providers helpers to display progress output. * * @author Chris Jones + * @author Fabien Potencier */ class ProgressHelper extends Helper { @@ -27,29 +28,15 @@ class ProgressHelper extends Helper const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; - /** - * @var array - */ - protected $options = array( - 'barWidth' => null, - 'barChar' => null, - 'emptyBarChar' => null, - 'progressChar' => null, - 'format' => null, - 'redrawFreq' => null, - ); + // options + private $barWidth = 28; + private $barChar = '='; + private $emptyBarChar = '-'; + private $progressChar = '>'; + private $format = self::FORMAT_NORMAL_NOMAX; + private $redrawFreq = 1; - /** - * @var array - */ - private $defaultOptions = array( - 'barWidth' => 28, - 'barChar' => '=', - 'emptyBarChar' => '-', - 'progressChar' => '>', - 'format' => self::FORMAT_NORMAL_NOMAX, - 'redrawFreq' => 1, - ); + private $barCharOriginal; /** * @var OutputInterface @@ -126,14 +113,73 @@ class ProgressHelper extends Helper array(604800, 'days', 86400), ); + /** + * Sets the progress bar width. + * + * @param int $size The progress bar size + */ + public function setBarWidth($size) + { + $this->barWidth = (int) $size; + } + + /** + * Sets the bar character. + * + * @param string $char A character + */ + public function setBarCharacter($char) + { + $this->barChar = $char; + } + + /** + * Sets the empty bar character. + * + * @param string $char A character + */ + public function setEmptyBarCharacter($char) + { + $this->emptyBarChar = $char; + } + + /** + * Sets the progress bar character. + * + * @param string $char A character + */ + public function setProgressChar($char) + { + $this->progressChar = $char; + } + + /** + * Sets the progress bar format. + * + * @param string $format The format + */ + public function setFormat($format) + { + $this->format = $format; + } + + /** + * Sets the redraw frequency. + * + * @param int $freq The frequency in seconds + */ + public function setRedrawFrequency($freq) + { + $this->redrawFreq = (int) $freq; + } + /** * Starts the progress output. * * @param OutputInterface $output An Output instance * @param integer $max Maximum steps - * @param array $options Options for progress helper */ - public function start(OutputInterface $output, $max = null, array $options = array()) + public function start(OutputInterface $output, $max = null) { $this->startTime = time(); $this->current = 0; @@ -142,25 +188,24 @@ class ProgressHelper extends Helper switch ($output->getVerbosity()) { case OutputInterface::VERBOSITY_QUIET: - $this->options['format'] = self::FORMAT_QUIET_NOMAX; + $this->format = self::FORMAT_QUIET_NOMAX; if ($this->max > 0) { - $this->options['format'] = self::FORMAT_QUIET; + $this->format = self::FORMAT_QUIET; } break; case OutputInterface::VERBOSITY_VERBOSE: - $this->options['format'] = self::FORMAT_VERBOSE_NOMAX; + $this->format = self::FORMAT_VERBOSE_NOMAX; if ($this->max > 0) { - $this->options['format'] = self::FORMAT_VERBOSE; + $this->format = self::FORMAT_VERBOSE; } break; default: if ($this->max > 0) { - $this->options['format'] = self::FORMAT_NORMAL; + $this->format = self::FORMAT_NORMAL; } break; } - $this->options = array_merge($this->defaultOptions, $options); $this->inititalize(); } @@ -172,11 +217,15 @@ class ProgressHelper extends Helper */ public function advance($step = 1, $redraw = false) { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling advance().'); + } + if ($this->current === 0) { $redraw = true; } $this->current += $step; - if ($redraw || $this->current % $this->options['redrawFreq'] === 0) { + if ($redraw || $this->current % $this->redrawFreq === 0) { $this->display(); } } @@ -188,7 +237,11 @@ class ProgressHelper extends Helper */ public function display($finish = false) { - $message = $this->options['format']; + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling display().'); + } + + $message = $this->format; foreach ($this->generate($finish) as $name => $value) { $message = str_replace("%{$name}%", $value, $message); } @@ -196,13 +249,17 @@ class ProgressHelper extends Helper } /** - * Finish the progress output + * Finishes the progress output. */ public function finish() { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling finish().'); + } + if ($this->startTime !== null) { if (!$this->max) { - $this->options['barChar'] = $this->options['barCharOriginal']; + $this->barChar = $this->barCharOriginal; $this->display(true); } $this->startTime = null; @@ -212,13 +269,13 @@ class ProgressHelper extends Helper } /** - * Initialize the progress helper. + * Initializes the progress helper. */ - protected function inititalize() + private function inititalize() { $this->formatVars = array(); foreach ($this->defaultFormatVars as $var) { - if (strpos($this->options['format'], "%{$var}%") !== false) { + if (strpos($this->format, "%{$var}%") !== false) { $this->formatVars[$var] = true; } } @@ -227,8 +284,8 @@ class ProgressHelper extends Helper $this->widths['max'] = strlen($this->max); $this->widths['current'] = $this->widths['max']; } else { - $this->options['barCharOriginal'] = $this->options['barChar']; - $this->options['barChar'] = $this->options['emptyBarChar']; + $this->barCharOriginal = $this->barChar; + $this->barChar = $this->emptyBarChar; } } @@ -238,7 +295,7 @@ class ProgressHelper extends Helper * @param Boolean $finish Forces the end result * @return array Array of format vars and values */ - protected function generate($finish = false) + private function generate($finish = false) { $vars = array(); $percent = 0; @@ -250,20 +307,20 @@ class ProgressHelper extends Helper $completeBars = 0; $emptyBars = 0; if ($this->max > 0) { - $completeBars = floor($percent * $this->options['barWidth']); + $completeBars = floor($percent * $this->barWidth); } else { if (!$finish) { - $completeBars = floor($this->current % $this->options['barWidth']); + $completeBars = floor($this->current % $this->barWidth); } else { - $completeBars = $this->options['barWidth']; + $completeBars = $this->barWidth; } } - $emptyBars = $this->options['barWidth'] - $completeBars - strlen($this->options['progressChar']); - $bar = str_repeat($this->options['barChar'], $completeBars); - if ($completeBars < $this->options['barWidth']) { - $bar .= $this->options['progressChar']; - $bar .= str_repeat($this->options['emptyBarChar'], $emptyBars); + $emptyBars = $this->barWidth - $completeBars - 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; From 7b3297621a8632143fbe15c9bcfae9b0a7d59a83 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 29 Sep 2012 23:20:12 +0200 Subject: [PATCH 3/3] [Console] added some basic tests for the ProgressHelper class --- .../Console/Helper/ProgressHelper.php | 46 ++++++------ .../Tests/Helper/ProgressHelperTest.php | 75 +++++++++++++++++++ 2 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 5411c7a276..0b2d6c0aa6 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -33,7 +33,7 @@ class ProgressHelper extends Helper private $barChar = '='; private $emptyBarChar = '-'; private $progressChar = '>'; - private $format = self::FORMAT_NORMAL_NOMAX; + private $format = null; private $redrawFreq = 1; private $barCharOriginal; @@ -148,7 +148,7 @@ class ProgressHelper extends Helper * * @param string $char A character */ - public function setProgressChar($char) + public function setProgressCharacter($char) { $this->progressChar = $char; } @@ -186,27 +186,29 @@ class ProgressHelper extends Helper $this->max = (int) $max; $this->output = $output; - switch ($output->getVerbosity()) { - case OutputInterface::VERBOSITY_QUIET: - $this->format = self::FORMAT_QUIET_NOMAX; - if ($this->max > 0) { - $this->format = self::FORMAT_QUIET; - } - break; - case OutputInterface::VERBOSITY_VERBOSE: - $this->format = self::FORMAT_VERBOSE_NOMAX; - if ($this->max > 0) { - $this->format = self::FORMAT_VERBOSE; - } - break; - default: - if ($this->max > 0) { - $this->format = self::FORMAT_NORMAL; - } - break; + if (null === $this->format) { + switch ($output->getVerbosity()) { + case OutputInterface::VERBOSITY_QUIET: + $this->format = self::FORMAT_QUIET_NOMAX; + if ($this->max > 0) { + $this->format = self::FORMAT_QUIET; + } + break; + case OutputInterface::VERBOSITY_VERBOSE: + $this->format = self::FORMAT_VERBOSE_NOMAX; + if ($this->max > 0) { + $this->format = self::FORMAT_VERBOSE; + } + break; + default: + if ($this->max > 0) { + $this->format = self::FORMAT_NORMAL; + } + break; + } } - $this->inititalize(); + $this->initialize(); } /** @@ -271,7 +273,7 @@ class ProgressHelper extends Helper /** * Initializes the progress helper. */ - private function inititalize() + private function initialize() { $this->formatVars = array(); foreach ($this->defaultFormatVars as $var) { diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php new file mode 100644 index 0000000000..e855146888 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\ProgressHelper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Output\StreamOutput; + +class ProgressHelperTest extends \PHPUnit_Framework_TestCase +{ + public function testAdvance() + { + $progress = new ProgressHelper(); + $progress->start($output = $this->getOutputStream()); + $progress->advance(); + + rewind($output->getStream()); + $this->assertEquals($this->generateOutput(' 1 [->--------------------------]'), stream_get_contents($output->getStream())); + } + + public function testAdvanceWithStep() + { + $progress = new ProgressHelper(); + $progress->start($output = $this->getOutputStream()); + $progress->advance(5); + + rewind($output->getStream()); + $this->assertEquals($this->generateOutput(' 5 [----->----------------------]'), stream_get_contents($output->getStream())); + } + + public function testAdvanceMultipleTimes() + { + $progress = new ProgressHelper(); + $progress->start($output = $this->getOutputStream()); + $progress->advance(3); + $progress->advance(2); + + rewind($output->getStream()); + $this->assertEquals($this->generateOutput(' 3 [--->------------------------]').$this->generateOutput(' 5 [----->----------------------]'), stream_get_contents($output->getStream())); + } + + public function testCustomizations() + { + $progress = new ProgressHelper(); + $progress->setBarWidth(10); + $progress->setBarCharacter('_'); + $progress->setEmptyBarCharacter(' '); + $progress->setProgressCharacter('/'); + $progress->setFormat(' %current%/%max% [%bar%] %percent%%'); + $progress->start($output = $this->getOutputStream(), 10); + $progress->advance(); + + rewind($output->getStream()); + $this->assertEquals($this->generateOutput(' 1/10 [_/ ] 10%'), stream_get_contents($output->getStream())); + } + + protected function getOutputStream() + { + return new StreamOutput(fopen('php://memory', 'r+', false)); + } + + protected function generateOutput($expected) + { + return str_repeat("\x08", 80).$expected.str_repeat(' ', 80 - strlen($expected)).str_repeat("\x08", 80 - strlen($expected)); + } +}