ProgressBar.php 17 KB

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