* * 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\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * The ProgressBar provides helpers to display progress output. * * @author Fabien Potencier * @author Chris Jones */ class ProgressBar { // options private $barWidth = 28; private $barChar; private $emptyBarChar = '-'; private $progressChar = '>'; private $format = null; private $redrawFreq = 1; /** * @var OutputInterface */ private $output; private $step = 0; private $max; private $startTime; private $stepWidth; private $percent = 0.0; private $lastMessagesLength = 0; private $formatLineCount; private $messages; private $overwrite = true; private static $formatters; private static $formats; /** * Constructor. * * @param OutputInterface $output An OutputInterface instance * @param int $max Maximum steps (0 if unknown) */ public function __construct(OutputInterface $output, $max = 0) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->output = $output; $this->setMaxSteps($max); if (!$this->output->isDecorated()) { // disable overwrite when output does not support ANSI codes. $this->overwrite = false; if ($this->max > 10) { // set a reasonable redraw frequency so output isn't flooded $this->setRedrawFrequency($max / 10); } } $this->setFormat($this->determineBestFormat()); $this->startTime = time(); } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition($name, $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) * * @return callable|null A PHP callable */ public static function getPlaceholderFormatterDefinition($name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; } /** * Sets a format for a given name. * * This method also allow you to override an existing format. * * @param string $name The format name * @param string $format A format string */ public static function setFormatDefinition($name, $format) { if (!self::$formats) { self::$formats = self::initFormats(); } self::$formats[$name] = $format; } /** * Gets the format for a given name. * * @param string $name The format name * * @return string|null A format string */ public static function getFormatDefinition($name) { if (!self::$formats) { self::$formats = self::initFormats(); } return isset(self::$formats[$name]) ? self::$formats[$name] : null; } public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; } public function getMessage($name = 'message') { return $this->messages[$name]; } /** * Gets the progress bar start time. * * @return int The progress bar start time */ public function getStartTime() { return $this->startTime; } /** * Gets the progress bar maximal steps. * * @return int The progress bar max steps */ public function getMaxSteps() { return $this->max; } /** * Gets the progress bar step. * * @deprecated since version 2.6, to be removed in 3.0. Use {@link getProgress()} instead. * * @return int The progress bar step */ public function getStep() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the getProgress() method instead.', E_USER_DEPRECATED); return $this->getProgress(); } /** * Gets the current step position. * * @return int The progress bar step */ public function getProgress() { return $this->step; } /** * Gets the progress bar step width. * * @internal This method is public for PHP 5.3 compatibility, it should not be used. * * @return int The progress bar step width */ public function getStepWidth() { return $this->stepWidth; } /** * Gets the current progress bar percent. * * @return float The current progress bar percent */ public function getProgressPercent() { return $this->percent; } /** * Sets the progress bar width. * * @param int $size The progress bar size */ 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. * * @param string $char A character */ public function setBarCharacter($char) { $this->barChar = $char; } /** * Gets the bar character. * * @return string A character */ public function getBarCharacter() { if (null === $this->barChar) { return $this->max ? '=' : $this->emptyBarChar; } return $this->barChar; } /** * Sets the empty bar character. * * @param string $char A character */ 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. * * @param string $char A character */ 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. * * @param string $format The format */ public function setFormat($format) { // try to use the _nomax variant if available if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { $this->format = self::getFormatDefinition($format.'_nomax'); } elseif (null !== self::getFormatDefinition($format)) { $this->format = self::getFormatDefinition($format); } else { $this->format = $format; } $this->formatLineCount = substr_count($this->format, "\n"); } /** * Sets the redraw frequency. * * @param int $freq The frequency in steps */ public function setRedrawFrequency($freq) { $this->redrawFreq = (int) $freq; } /** * Starts the progress output. * * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged */ public function start($max = null) { $this->startTime = time(); $this->step = 0; $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); } $this->display(); } /** * Advances the progress output X steps. * * @param int $step Number of steps to advance * * @throws \LogicException */ public function advance($step = 1) { $this->setProgress($this->step + $step); } /** * Sets the current progress. * * @deprecated since version 2.6, to be removed in 3.0. Use {@link setProgress()} instead. * * @param int $step The current progress * * @throws \LogicException */ public function setCurrent($step) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setProgress() method instead.', E_USER_DEPRECATED); $this->setProgress($step); } /** * Sets whether to overwrite the progressbar, false for new line. * * @param bool $overwrite */ public function setOverwrite($overwrite) { $this->overwrite = (bool) $overwrite; } /** * Sets the current progress. * * @param int $step The current progress * * @throws \LogicException */ public function setProgress($step) { $step = (int) $step; if ($step < $this->step) { throw new \LogicException('You can\'t regress the progress bar.'); } if ($this->max && $step > $this->max) { $this->max = $step; } $prevPeriod = (int) ($this->step / $this->redrawFreq); $currPeriod = (int) ($step / $this->redrawFreq); $this->step = $step; $this->percent = $this->max ? (float) $this->step / $this->max : 0; if ($prevPeriod !== $currPeriod || $this->max === $step) { $this->display(); } } /** * Finishes the progress output. */ public function finish() { if (!$this->max) { $this->max = $this->step; } if ($this->step === $this->max && !$this->overwrite) { // prevent double 100% output return; } $this->setProgress($this->max); } /** * Outputs the current progress string. */ public function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped. $self = $this; $output = $this->output; $messages = $this->messages; $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) { if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { $text = call_user_func($formatter, $self, $output); } elseif (isset($messages[$matches[1]])) { $text = $messages[$matches[1]]; } else { return $matches[0]; } if (isset($matches[2])) { $text = sprintf('%'.$matches[2], $text); } return $text; }, $this->format)); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. */ public function clear() { if (!$this->overwrite) { return; } $this->overwrite(str_repeat("\n", $this->formatLineCount)); } /** * Sets the progress bar maximal steps. * * @param int $max The progress bar max steps */ private function setMaxSteps($max) { $this->max = max(0, (int) $max); $this->stepWidth = $this->max ? Helper::strlen($this->max) : 4; } /** * Overwrites a previous message to the output. * * @param string $message The message */ private function overwrite($message) { $lines = explode("\n", $message); // append whitespace to match the line's length if (null !== $this->lastMessagesLength) { foreach ($lines as $i => $line) { if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) { $lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); } } } if ($this->overwrite) { // move back to the beginning of the progress bar before redrawing it $this->output->write("\x0D"); } elseif ($this->step > 0) { // move to new line $this->output->writeln(''); } if ($this->formatLineCount) { $this->output->write(sprintf("\033[%dA", $this->formatLineCount)); } $this->output->write(implode("\n", $lines)); $this->lastMessagesLength = 0; foreach ($lines as $line) { $len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line); if ($len > $this->lastMessagesLength) { $this->lastMessagesLength = $len; } } } private function determineBestFormat() { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->max ? 'verbose' : 'verbose_nomax'; case OutputInterface::VERBOSITY_VERY_VERBOSE: return $this->max ? 'very_verbose' : 'very_verbose_nomax'; case OutputInterface::VERBOSITY_DEBUG: return $this->max ? 'debug' : 'debug_nomax'; default: return $this->max ? 'normal' : 'normal_nomax'; } } private static function initPlaceholderFormatters() { return array( 'bar' => function (ProgressBar $bar, OutputInterface $output) { $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); } return $display; }, 'elapsed' => function (ProgressBar $bar) { return Helper::formatTime(time() - $bar->getStartTime()); }, 'remaining' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $remaining = 0; } else { $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); } return Helper::formatTime($remaining); }, 'estimated' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $estimated = 0; } else { $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); } return Helper::formatTime($estimated); }, 'memory' => function (ProgressBar $bar) { return Helper::formatMemory(memory_get_usage(true)); }, 'current' => function (ProgressBar $bar) { return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); }, 'max' => function (ProgressBar $bar) { return $bar->getMaxSteps(); }, 'percent' => function (ProgressBar $bar) { return floor($bar->getProgressPercent() * 100); }, ); } private static function initFormats() { return array( 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', 'normal_nomax' => ' %current% [%bar%]', 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', ); } }