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 12namespace Symfony\Component\Console\Helper; 13 14use Symfony\Component\Console\Cursor; 15use Symfony\Component\Console\Exception\LogicException; 16use Symfony\Component\Console\Output\ConsoleOutputInterface; 17use Symfony\Component\Console\Output\ConsoleSectionOutput; 18use Symfony\Component\Console\Output\OutputInterface; 19use Symfony\Component\Console\Terminal; 20 21/** 22 * The ProgressBar provides helpers to display progress output. 23 * 24 * @author Fabien Potencier <fabien@symfony.com> 25 * @author Chris Jones <leeked@gmail.com> 26 */ 27final class ProgressBar 28{ 29 private $barWidth = 28; 30 private $barChar; 31 private $emptyBarChar = '-'; 32 private $progressChar = '>'; 33 private $format; 34 private $internalFormat; 35 private $redrawFreq = 1; 36 private $writeCount; 37 private $lastWriteTime; 38 private $minSecondsBetweenRedraws = 0; 39 private $maxSecondsBetweenRedraws = 1; 40 private $output; 41 private $step = 0; 42 private $max; 43 private $startTime; 44 private $stepWidth; 45 private $percent = 0.0; 46 private $formatLineCount; 47 private $messages = []; 48 private $overwrite = true; 49 private $terminal; 50 private $previousMessage; 51 private $cursor; 52 53 private static $formatters; 54 private static $formats; 55 56 /** 57 * @param int $max Maximum steps (0 if unknown) 58 */ 59 public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25) 60 { 61 if ($output instanceof ConsoleOutputInterface) { 62 $output = $output->getErrorOutput(); 63 } 64 65 $this->output = $output; 66 $this->setMaxSteps($max); 67 $this->terminal = new Terminal(); 68 69 if (0 < $minSecondsBetweenRedraws) { 70 $this->redrawFreq = null; 71 $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws; 72 } 73 74 if (!$this->output->isDecorated()) { 75 // disable overwrite when output does not support ANSI codes. 76 $this->overwrite = false; 77 78 // set a reasonable redraw frequency so output isn't flooded 79 $this->redrawFreq = null; 80 } 81 82 $this->startTime = time(); 83 $this->cursor = new Cursor($output); 84 } 85 86 /** 87 * Sets a placeholder formatter for a given name. 88 * 89 * This method also allow you to override an existing placeholder. 90 * 91 * @param string $name The placeholder name (including the delimiter char like %) 92 * @param callable $callable A PHP callable 93 */ 94 public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void 95 { 96 if (!self::$formatters) { 97 self::$formatters = self::initPlaceholderFormatters(); 98 } 99 100 self::$formatters[$name] = $callable; 101 } 102 103 /** 104 * Gets the placeholder formatter for a given name. 105 * 106 * @param string $name The placeholder name (including the delimiter char like %) 107 * 108 * @return callable|null A PHP callable 109 */ 110 public static function getPlaceholderFormatterDefinition(string $name): ?callable 111 { 112 if (!self::$formatters) { 113 self::$formatters = self::initPlaceholderFormatters(); 114 } 115 116 return self::$formatters[$name] ?? null; 117 } 118 119 /** 120 * Sets a format for a given name. 121 * 122 * This method also allow you to override an existing format. 123 * 124 * @param string $name The format name 125 * @param string $format A format string 126 */ 127 public static function setFormatDefinition(string $name, string $format): void 128 { 129 if (!self::$formats) { 130 self::$formats = self::initFormats(); 131 } 132 133 self::$formats[$name] = $format; 134 } 135 136 /** 137 * Gets the format for a given name. 138 * 139 * @param string $name The format name 140 * 141 * @return string|null A format string 142 */ 143 public static function getFormatDefinition(string $name): ?string 144 { 145 if (!self::$formats) { 146 self::$formats = self::initFormats(); 147 } 148 149 return self::$formats[$name] ?? null; 150 } 151 152 /** 153 * Associates a text with a named placeholder. 154 * 155 * The text is displayed when the progress bar is rendered but only 156 * when the corresponding placeholder is part of the custom format line 157 * (by wrapping the name with %). 158 * 159 * @param string $message The text to associate with the placeholder 160 * @param string $name The name of the placeholder 161 */ 162 public function setMessage(string $message, string $name = 'message') 163 { 164 $this->messages[$name] = $message; 165 } 166 167 public function getMessage(string $name = 'message') 168 { 169 return $this->messages[$name]; 170 } 171 172 public function getStartTime(): int 173 { 174 return $this->startTime; 175 } 176 177 public function getMaxSteps(): int 178 { 179 return $this->max; 180 } 181 182 public function getProgress(): int 183 { 184 return $this->step; 185 } 186 187 private function getStepWidth(): int 188 { 189 return $this->stepWidth; 190 } 191 192 public function getProgressPercent(): float 193 { 194 return $this->percent; 195 } 196 197 public function getBarOffset(): float 198 { 199 return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth); 200 } 201 202 public function getEstimated(): float 203 { 204 if (!$this->step) { 205 return 0; 206 } 207 208 return round((time() - $this->startTime) / $this->step * $this->max); 209 } 210 211 public function getRemaining(): float 212 { 213 if (!$this->step) { 214 return 0; 215 } 216 217 return round((time() - $this->startTime) / $this->step * ($this->max - $this->step)); 218 } 219 220 public function setBarWidth(int $size) 221 { 222 $this->barWidth = max(1, $size); 223 } 224 225 public function getBarWidth(): int 226 { 227 return $this->barWidth; 228 } 229 230 public function setBarCharacter(string $char) 231 { 232 $this->barChar = $char; 233 } 234 235 public function getBarCharacter(): string 236 { 237 if (null === $this->barChar) { 238 return $this->max ? '=' : $this->emptyBarChar; 239 } 240 241 return $this->barChar; 242 } 243 244 public function setEmptyBarCharacter(string $char) 245 { 246 $this->emptyBarChar = $char; 247 } 248 249 public function getEmptyBarCharacter(): string 250 { 251 return $this->emptyBarChar; 252 } 253 254 public function setProgressCharacter(string $char) 255 { 256 $this->progressChar = $char; 257 } 258 259 public function getProgressCharacter(): string 260 { 261 return $this->progressChar; 262 } 263 264 public function setFormat(string $format) 265 { 266 $this->format = null; 267 $this->internalFormat = $format; 268 } 269 270 /** 271 * Sets the redraw frequency. 272 * 273 * @param int|float $freq The frequency in steps 274 */ 275 public function setRedrawFrequency(?int $freq) 276 { 277 $this->redrawFreq = null !== $freq ? max(1, $freq) : null; 278 } 279 280 public function minSecondsBetweenRedraws(float $seconds): void 281 { 282 $this->minSecondsBetweenRedraws = $seconds; 283 } 284 285 public function maxSecondsBetweenRedraws(float $seconds): void 286 { 287 $this->maxSecondsBetweenRedraws = $seconds; 288 } 289 290 /** 291 * Returns an iterator that will automatically update the progress bar when iterated. 292 * 293 * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable 294 */ 295 public function iterate(iterable $iterable, int $max = null): iterable 296 { 297 $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0)); 298 299 foreach ($iterable as $key => $value) { 300 yield $key => $value; 301 302 $this->advance(); 303 } 304 305 $this->finish(); 306 } 307 308 /** 309 * Starts the progress output. 310 * 311 * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged 312 */ 313 public function start(int $max = null) 314 { 315 $this->startTime = time(); 316 $this->step = 0; 317 $this->percent = 0.0; 318 319 if (null !== $max) { 320 $this->setMaxSteps($max); 321 } 322 323 $this->display(); 324 } 325 326 /** 327 * Advances the progress output X steps. 328 * 329 * @param int $step Number of steps to advance 330 */ 331 public function advance(int $step = 1) 332 { 333 $this->setProgress($this->step + $step); 334 } 335 336 /** 337 * Sets whether to overwrite the progressbar, false for new line. 338 */ 339 public function setOverwrite(bool $overwrite) 340 { 341 $this->overwrite = $overwrite; 342 } 343 344 public function setProgress(int $step) 345 { 346 if ($this->max && $step > $this->max) { 347 $this->max = $step; 348 } elseif ($step < 0) { 349 $step = 0; 350 } 351 352 $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10); 353 $prevPeriod = (int) ($this->step / $redrawFreq); 354 $currPeriod = (int) ($step / $redrawFreq); 355 $this->step = $step; 356 $this->percent = $this->max ? (float) $this->step / $this->max : 0; 357 $timeInterval = microtime(true) - $this->lastWriteTime; 358 359 // Draw regardless of other limits 360 if ($this->max === $step) { 361 $this->display(); 362 363 return; 364 } 365 366 // Throttling 367 if ($timeInterval < $this->minSecondsBetweenRedraws) { 368 return; 369 } 370 371 // Draw each step period, but not too late 372 if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) { 373 $this->display(); 374 } 375 } 376 377 public function setMaxSteps(int $max) 378 { 379 $this->format = null; 380 $this->max = max(0, $max); 381 $this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4; 382 } 383 384 /** 385 * Finishes the progress output. 386 */ 387 public function finish(): void 388 { 389 if (!$this->max) { 390 $this->max = $this->step; 391 } 392 393 if ($this->step === $this->max && !$this->overwrite) { 394 // prevent double 100% output 395 return; 396 } 397 398 $this->setProgress($this->max); 399 } 400 401 /** 402 * Outputs the current progress string. 403 */ 404 public function display(): void 405 { 406 if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { 407 return; 408 } 409 410 if (null === $this->format) { 411 $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); 412 } 413 414 $this->overwrite($this->buildLine()); 415 } 416 417 /** 418 * Removes the progress bar from the current line. 419 * 420 * This is useful if you wish to write some output 421 * while a progress bar is running. 422 * Call display() to show the progress bar again. 423 */ 424 public function clear(): void 425 { 426 if (!$this->overwrite) { 427 return; 428 } 429 430 if (null === $this->format) { 431 $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); 432 } 433 434 $this->overwrite(''); 435 } 436 437 private function setRealFormat(string $format) 438 { 439 // try to use the _nomax variant if available 440 if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { 441 $this->format = self::getFormatDefinition($format.'_nomax'); 442 } elseif (null !== self::getFormatDefinition($format)) { 443 $this->format = self::getFormatDefinition($format); 444 } else { 445 $this->format = $format; 446 } 447 448 $this->formatLineCount = substr_count($this->format, "\n"); 449 } 450 451 /** 452 * Overwrites a previous message to the output. 453 */ 454 private function overwrite(string $message): void 455 { 456 if ($this->previousMessage === $message) { 457 return; 458 } 459 460 $originalMessage = $message; 461 462 if ($this->overwrite) { 463 if (null !== $this->previousMessage) { 464 if ($this->output instanceof ConsoleSectionOutput) { 465 $messageLines = explode("\n", $message); 466 $lineCount = \count($messageLines); 467 foreach ($messageLines as $messageLine) { 468 $messageLineLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $messageLine); 469 if ($messageLineLength > $this->terminal->getWidth()) { 470 $lineCount += floor($messageLineLength / $this->terminal->getWidth()); 471 } 472 } 473 $this->output->clear($lineCount); 474 } else { 475 if ($this->formatLineCount > 0) { 476 $this->cursor->moveUp($this->formatLineCount); 477 } 478 479 $this->cursor->moveToColumn(1); 480 $this->cursor->clearLine(); 481 } 482 } 483 } elseif ($this->step > 0) { 484 $message = \PHP_EOL.$message; 485 } 486 487 $this->previousMessage = $originalMessage; 488 $this->lastWriteTime = microtime(true); 489 490 $this->output->write($message); 491 ++$this->writeCount; 492 } 493 494 private function determineBestFormat(): string 495 { 496 switch ($this->output->getVerbosity()) { 497 // OutputInterface::VERBOSITY_QUIET: display is disabled anyway 498 case OutputInterface::VERBOSITY_VERBOSE: 499 return $this->max ? 'verbose' : 'verbose_nomax'; 500 case OutputInterface::VERBOSITY_VERY_VERBOSE: 501 return $this->max ? 'very_verbose' : 'very_verbose_nomax'; 502 case OutputInterface::VERBOSITY_DEBUG: 503 return $this->max ? 'debug' : 'debug_nomax'; 504 default: 505 return $this->max ? 'normal' : 'normal_nomax'; 506 } 507 } 508 509 private static function initPlaceholderFormatters(): array 510 { 511 return [ 512 'bar' => function (self $bar, OutputInterface $output) { 513 $completeBars = $bar->getBarOffset(); 514 $display = str_repeat($bar->getBarCharacter(), $completeBars); 515 if ($completeBars < $bar->getBarWidth()) { 516 $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); 517 $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); 518 } 519 520 return $display; 521 }, 522 'elapsed' => function (self $bar) { 523 return Helper::formatTime(time() - $bar->getStartTime()); 524 }, 525 'remaining' => function (self $bar) { 526 if (!$bar->getMaxSteps()) { 527 throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); 528 } 529 530 return Helper::formatTime($bar->getRemaining()); 531 }, 532 'estimated' => function (self $bar) { 533 if (!$bar->getMaxSteps()) { 534 throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); 535 } 536 537 return Helper::formatTime($bar->getEstimated()); 538 }, 539 'memory' => function (self $bar) { 540 return Helper::formatMemory(memory_get_usage(true)); 541 }, 542 'current' => function (self $bar) { 543 return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT); 544 }, 545 'max' => function (self $bar) { 546 return $bar->getMaxSteps(); 547 }, 548 'percent' => function (self $bar) { 549 return floor($bar->getProgressPercent() * 100); 550 }, 551 ]; 552 } 553 554 private static function initFormats(): array 555 { 556 return [ 557 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', 558 'normal_nomax' => ' %current% [%bar%]', 559 560 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', 561 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 562 563 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', 564 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 565 566 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', 567 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', 568 ]; 569 } 570 571 private function buildLine(): string 572 { 573 $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; 574 $callback = function ($matches) { 575 if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { 576 $text = $formatter($this, $this->output); 577 } elseif (isset($this->messages[$matches[1]])) { 578 $text = $this->messages[$matches[1]]; 579 } else { 580 return $matches[0]; 581 } 582 583 if (isset($matches[2])) { 584 $text = sprintf('%'.$matches[2], $text); 585 } 586 587 return $text; 588 }; 589 $line = preg_replace_callback($regex, $callback, $this->format); 590 591 // gets string length for each sub line with multiline format 592 $linesLength = array_map(function ($subLine) { 593 return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r")); 594 }, explode("\n", $line)); 595 596 $linesWidth = max($linesLength); 597 598 $terminalWidth = $this->terminal->getWidth(); 599 if ($linesWidth <= $terminalWidth) { 600 return $line; 601 } 602 603 $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); 604 605 return preg_replace_callback($regex, $callback, $this->format); 606 } 607} 608