Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Console\Helper; Chris@0: Chris@17: use Symfony\Component\Console\Exception\LogicException; Chris@0: use Symfony\Component\Console\Output\ConsoleOutputInterface; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@0: use Symfony\Component\Console\Terminal; Chris@0: Chris@0: /** Chris@0: * The ProgressBar provides helpers to display progress output. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: * @author Chris Jones Chris@0: */ Chris@14: final class ProgressBar Chris@0: { Chris@0: private $barWidth = 28; Chris@0: private $barChar; Chris@0: private $emptyBarChar = '-'; Chris@0: private $progressChar = '>'; Chris@0: private $format; Chris@0: private $internalFormat; Chris@0: private $redrawFreq = 1; Chris@0: private $output; Chris@0: private $step = 0; Chris@0: private $max; Chris@0: private $startTime; Chris@0: private $stepWidth; Chris@0: private $percent = 0.0; Chris@0: private $formatLineCount; Chris@17: private $messages = []; Chris@0: private $overwrite = true; Chris@0: private $terminal; Chris@0: private $firstRun = true; Chris@0: Chris@0: private static $formatters; Chris@0: private static $formats; Chris@0: Chris@0: /** Chris@0: * @param OutputInterface $output An OutputInterface instance Chris@0: * @param int $max Maximum steps (0 if unknown) Chris@0: */ Chris@0: public function __construct(OutputInterface $output, $max = 0) Chris@0: { Chris@0: if ($output instanceof ConsoleOutputInterface) { Chris@0: $output = $output->getErrorOutput(); Chris@0: } Chris@0: Chris@0: $this->output = $output; Chris@0: $this->setMaxSteps($max); Chris@0: $this->terminal = new Terminal(); Chris@0: Chris@0: if (!$this->output->isDecorated()) { Chris@0: // disable overwrite when output does not support ANSI codes. Chris@0: $this->overwrite = false; Chris@0: Chris@0: // set a reasonable redraw frequency so output isn't flooded Chris@0: $this->setRedrawFrequency($max / 10); Chris@0: } Chris@0: Chris@0: $this->startTime = time(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a placeholder formatter for a given name. Chris@0: * Chris@0: * This method also allow you to override an existing placeholder. Chris@0: * Chris@0: * @param string $name The placeholder name (including the delimiter char like %) Chris@0: * @param callable $callable A PHP callable Chris@0: */ Chris@0: public static function setPlaceholderFormatterDefinition($name, callable $callable) Chris@0: { Chris@0: if (!self::$formatters) { Chris@0: self::$formatters = self::initPlaceholderFormatters(); Chris@0: } Chris@0: Chris@0: self::$formatters[$name] = $callable; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the placeholder formatter for a given name. Chris@0: * Chris@0: * @param string $name The placeholder name (including the delimiter char like %) Chris@0: * Chris@0: * @return callable|null A PHP callable Chris@0: */ Chris@0: public static function getPlaceholderFormatterDefinition($name) Chris@0: { Chris@0: if (!self::$formatters) { Chris@0: self::$formatters = self::initPlaceholderFormatters(); Chris@0: } Chris@0: Chris@0: return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a format for a given name. Chris@0: * Chris@0: * This method also allow you to override an existing format. Chris@0: * Chris@0: * @param string $name The format name Chris@0: * @param string $format A format string Chris@0: */ Chris@0: public static function setFormatDefinition($name, $format) Chris@0: { Chris@0: if (!self::$formats) { Chris@0: self::$formats = self::initFormats(); Chris@0: } Chris@0: Chris@0: self::$formats[$name] = $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the format for a given name. Chris@0: * Chris@0: * @param string $name The format name Chris@0: * Chris@0: * @return string|null A format string Chris@0: */ Chris@0: public static function getFormatDefinition($name) Chris@0: { Chris@0: if (!self::$formats) { Chris@0: self::$formats = self::initFormats(); Chris@0: } Chris@0: Chris@0: return isset(self::$formats[$name]) ? self::$formats[$name] : null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Associates a text with a named placeholder. Chris@0: * Chris@0: * The text is displayed when the progress bar is rendered but only Chris@0: * when the corresponding placeholder is part of the custom format line Chris@0: * (by wrapping the name with %). Chris@0: * Chris@0: * @param string $message The text to associate with the placeholder Chris@0: * @param string $name The name of the placeholder Chris@0: */ Chris@0: public function setMessage($message, $name = 'message') Chris@0: { Chris@0: $this->messages[$name] = $message; Chris@0: } Chris@0: Chris@0: public function getMessage($name = 'message') Chris@0: { Chris@0: return $this->messages[$name]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the progress bar start time. Chris@0: * Chris@0: * @return int The progress bar start time Chris@0: */ Chris@0: public function getStartTime() Chris@0: { Chris@0: return $this->startTime; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the progress bar maximal steps. Chris@0: * Chris@0: * @return int The progress bar max steps Chris@0: */ Chris@0: public function getMaxSteps() Chris@0: { Chris@0: return $this->max; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the current step position. Chris@0: * Chris@0: * @return int The progress bar step Chris@0: */ Chris@0: public function getProgress() Chris@0: { Chris@0: return $this->step; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the progress bar step width. Chris@0: * Chris@0: * @return int The progress bar step width Chris@0: */ Chris@0: private function getStepWidth() Chris@0: { Chris@0: return $this->stepWidth; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the current progress bar percent. Chris@0: * Chris@0: * @return float The current progress bar percent Chris@0: */ Chris@0: public function getProgressPercent() Chris@0: { Chris@0: return $this->percent; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the progress bar width. Chris@0: * Chris@0: * @param int $size The progress bar size Chris@0: */ Chris@0: public function setBarWidth($size) Chris@0: { Chris@0: $this->barWidth = max(1, (int) $size); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the progress bar width. Chris@0: * Chris@0: * @return int The progress bar size Chris@0: */ Chris@0: public function getBarWidth() Chris@0: { Chris@0: return $this->barWidth; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the bar character. Chris@0: * Chris@0: * @param string $char A character Chris@0: */ Chris@0: public function setBarCharacter($char) Chris@0: { Chris@0: $this->barChar = $char; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the bar character. Chris@0: * Chris@0: * @return string A character Chris@0: */ Chris@0: public function getBarCharacter() Chris@0: { Chris@0: if (null === $this->barChar) { Chris@0: return $this->max ? '=' : $this->emptyBarChar; Chris@0: } Chris@0: Chris@0: return $this->barChar; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the empty bar character. Chris@0: * Chris@0: * @param string $char A character Chris@0: */ Chris@0: public function setEmptyBarCharacter($char) Chris@0: { Chris@0: $this->emptyBarChar = $char; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the empty bar character. Chris@0: * Chris@0: * @return string A character Chris@0: */ Chris@0: public function getEmptyBarCharacter() Chris@0: { Chris@0: return $this->emptyBarChar; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the progress bar character. Chris@0: * Chris@0: * @param string $char A character Chris@0: */ Chris@0: public function setProgressCharacter($char) Chris@0: { Chris@0: $this->progressChar = $char; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the progress bar character. Chris@0: * Chris@0: * @return string A character Chris@0: */ Chris@0: public function getProgressCharacter() Chris@0: { Chris@0: return $this->progressChar; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the progress bar format. Chris@0: * Chris@0: * @param string $format The format Chris@0: */ Chris@0: public function setFormat($format) Chris@0: { Chris@0: $this->format = null; Chris@0: $this->internalFormat = $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the redraw frequency. Chris@0: * Chris@0: * @param int|float $freq The frequency in steps Chris@0: */ Chris@0: public function setRedrawFrequency($freq) Chris@0: { Chris@0: $this->redrawFreq = max((int) $freq, 1); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Starts the progress output. Chris@0: * Chris@0: * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged Chris@0: */ Chris@0: public function start($max = null) Chris@0: { Chris@0: $this->startTime = time(); Chris@0: $this->step = 0; Chris@0: $this->percent = 0.0; Chris@0: Chris@0: if (null !== $max) { Chris@0: $this->setMaxSteps($max); Chris@0: } Chris@0: Chris@0: $this->display(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Advances the progress output X steps. Chris@0: * Chris@0: * @param int $step Number of steps to advance Chris@0: */ Chris@0: public function advance($step = 1) Chris@0: { Chris@0: $this->setProgress($this->step + $step); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets whether to overwrite the progressbar, false for new line. Chris@0: * Chris@0: * @param bool $overwrite Chris@0: */ Chris@0: public function setOverwrite($overwrite) Chris@0: { Chris@0: $this->overwrite = (bool) $overwrite; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the current progress. Chris@0: * Chris@0: * @param int $step The current progress Chris@0: */ Chris@0: public function setProgress($step) Chris@0: { Chris@0: $step = (int) $step; Chris@0: Chris@0: if ($this->max && $step > $this->max) { Chris@0: $this->max = $step; Chris@0: } elseif ($step < 0) { Chris@0: $step = 0; Chris@0: } Chris@0: Chris@0: $prevPeriod = (int) ($this->step / $this->redrawFreq); Chris@0: $currPeriod = (int) ($step / $this->redrawFreq); Chris@0: $this->step = $step; Chris@0: $this->percent = $this->max ? (float) $this->step / $this->max : 0; Chris@0: if ($prevPeriod !== $currPeriod || $this->max === $step) { Chris@0: $this->display(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Finishes the progress output. Chris@0: */ Chris@0: public function finish() Chris@0: { Chris@0: if (!$this->max) { Chris@0: $this->max = $this->step; Chris@0: } Chris@0: Chris@0: if ($this->step === $this->max && !$this->overwrite) { Chris@0: // prevent double 100% output Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->setProgress($this->max); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Outputs the current progress string. Chris@0: */ Chris@0: public function display() Chris@0: { Chris@0: if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if (null === $this->format) { Chris@0: $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); Chris@0: } Chris@0: Chris@0: $this->overwrite($this->buildLine()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Removes the progress bar from the current line. Chris@0: * Chris@0: * This is useful if you wish to write some output Chris@0: * while a progress bar is running. Chris@0: * Call display() to show the progress bar again. Chris@0: */ Chris@0: public function clear() Chris@0: { Chris@0: if (!$this->overwrite) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if (null === $this->format) { Chris@0: $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); Chris@0: } Chris@0: Chris@0: $this->overwrite(''); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the progress bar format. Chris@0: * Chris@0: * @param string $format The format Chris@0: */ Chris@0: private function setRealFormat($format) Chris@0: { Chris@0: // try to use the _nomax variant if available Chris@0: if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { Chris@0: $this->format = self::getFormatDefinition($format.'_nomax'); Chris@0: } elseif (null !== self::getFormatDefinition($format)) { Chris@0: $this->format = self::getFormatDefinition($format); Chris@0: } else { Chris@0: $this->format = $format; Chris@0: } Chris@0: Chris@0: $this->formatLineCount = substr_count($this->format, "\n"); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the progress bar maximal steps. Chris@0: * Chris@0: * @param int $max The progress bar max steps Chris@0: */ Chris@0: private function setMaxSteps($max) Chris@0: { Chris@0: $this->max = max(0, (int) $max); Chris@0: $this->stepWidth = $this->max ? Helper::strlen($this->max) : 4; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Overwrites a previous message to the output. Chris@0: * Chris@0: * @param string $message The message Chris@0: */ Chris@0: private function overwrite($message) Chris@0: { Chris@0: if ($this->overwrite) { Chris@0: if (!$this->firstRun) { Chris@0: // Erase previous lines Chris@0: if ($this->formatLineCount > 0) { Chris@18: $message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message; Chris@0: } Chris@18: Chris@18: // Move the cursor to the beginning of the line and erase the line Chris@18: $message = "\x0D\x1B[2K$message"; Chris@0: } Chris@0: } elseif ($this->step > 0) { Chris@18: $message = PHP_EOL.$message; Chris@0: } Chris@0: Chris@0: $this->firstRun = false; Chris@0: Chris@0: $this->output->write($message); Chris@0: } Chris@0: Chris@0: private function determineBestFormat() Chris@0: { Chris@0: switch ($this->output->getVerbosity()) { Chris@0: // OutputInterface::VERBOSITY_QUIET: display is disabled anyway Chris@0: case OutputInterface::VERBOSITY_VERBOSE: Chris@0: return $this->max ? 'verbose' : 'verbose_nomax'; Chris@0: case OutputInterface::VERBOSITY_VERY_VERBOSE: Chris@0: return $this->max ? 'very_verbose' : 'very_verbose_nomax'; Chris@0: case OutputInterface::VERBOSITY_DEBUG: Chris@0: return $this->max ? 'debug' : 'debug_nomax'; Chris@0: default: Chris@0: return $this->max ? 'normal' : 'normal_nomax'; Chris@0: } Chris@0: } Chris@0: Chris@0: private static function initPlaceholderFormatters() Chris@0: { Chris@17: return [ Chris@17: 'bar' => function (self $bar, OutputInterface $output) { Chris@0: $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); Chris@0: $display = str_repeat($bar->getBarCharacter(), $completeBars); Chris@0: if ($completeBars < $bar->getBarWidth()) { Chris@0: $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); Chris@0: $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); Chris@0: } Chris@0: Chris@0: return $display; Chris@0: }, Chris@17: 'elapsed' => function (self $bar) { Chris@0: return Helper::formatTime(time() - $bar->getStartTime()); Chris@0: }, Chris@17: 'remaining' => function (self $bar) { Chris@0: if (!$bar->getMaxSteps()) { Chris@0: throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); Chris@0: } Chris@0: Chris@0: if (!$bar->getProgress()) { Chris@0: $remaining = 0; Chris@0: } else { Chris@0: $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); Chris@0: } Chris@0: Chris@0: return Helper::formatTime($remaining); Chris@0: }, Chris@17: 'estimated' => function (self $bar) { Chris@0: if (!$bar->getMaxSteps()) { Chris@0: throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); Chris@0: } Chris@0: Chris@0: if (!$bar->getProgress()) { Chris@0: $estimated = 0; Chris@0: } else { Chris@0: $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); Chris@0: } Chris@0: Chris@0: return Helper::formatTime($estimated); Chris@0: }, Chris@17: 'memory' => function (self $bar) { Chris@0: return Helper::formatMemory(memory_get_usage(true)); Chris@0: }, Chris@17: 'current' => function (self $bar) { Chris@0: return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); Chris@0: }, Chris@17: 'max' => function (self $bar) { Chris@0: return $bar->getMaxSteps(); Chris@0: }, Chris@17: 'percent' => function (self $bar) { Chris@0: return floor($bar->getProgressPercent() * 100); Chris@0: }, Chris@17: ]; Chris@0: } Chris@0: Chris@0: private static function initFormats() Chris@0: { Chris@17: return [ Chris@0: 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', Chris@0: 'normal_nomax' => ' %current% [%bar%]', Chris@0: Chris@0: 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', Chris@0: 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', Chris@0: Chris@0: 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', Chris@0: 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', Chris@0: Chris@0: 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', Chris@0: 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', Chris@17: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return string Chris@0: */ Chris@0: private function buildLine() Chris@0: { Chris@0: $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; Chris@0: $callback = function ($matches) { Chris@0: if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { Chris@17: $text = \call_user_func($formatter, $this, $this->output); Chris@0: } elseif (isset($this->messages[$matches[1]])) { Chris@0: $text = $this->messages[$matches[1]]; Chris@0: } else { Chris@0: return $matches[0]; Chris@0: } Chris@0: Chris@0: if (isset($matches[2])) { Chris@0: $text = sprintf('%'.$matches[2], $text); Chris@0: } Chris@0: Chris@0: return $text; Chris@0: }; Chris@0: $line = preg_replace_callback($regex, $callback, $this->format); Chris@0: Chris@0: // gets string length for each sub line with multiline format Chris@0: $linesLength = array_map(function ($subLine) { Chris@0: return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r")); Chris@0: }, explode("\n", $line)); Chris@0: Chris@0: $linesWidth = max($linesLength); Chris@0: Chris@0: $terminalWidth = $this->terminal->getWidth(); Chris@0: if ($linesWidth <= $terminalWidth) { Chris@0: return $line; Chris@0: } Chris@0: Chris@0: $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); Chris@0: Chris@0: return preg_replace_callback($regex, $callback, $this->format); Chris@0: } Chris@0: }