ProgressBar.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  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. /**
  14. * The ProgressBar provides helpers to display progress output.
  15. *
  16. * @author Fabien Potencier <fabien@symfony.com>
  17. * @author Chris Jones <leeked@gmail.com>
  18. */
  19. class ProgressBar
  20. {
  21. // options
  22. private $barWidth = 28;
  23. private $barChar;
  24. private $emptyBarChar = '-';
  25. private $progressChar = '>';
  26. private $format = null;
  27. private $redrawFreq = 1;
  28. /**
  29. * @var OutputInterface
  30. */
  31. private $output;
  32. private $step = 0;
  33. private $max;
  34. private $startTime;
  35. private $stepWidth;
  36. private $percent = 0.0;
  37. private $lastMessagesLength = 0;
  38. private $formatLineCount;
  39. private $messages;
  40. private $overwrite = true;
  41. private static $formatters;
  42. private static $formats;
  43. /**
  44. * Constructor.
  45. *
  46. * @param OutputInterface $output An OutputInterface instance
  47. * @param int $max Maximum steps (0 if unknown)
  48. */
  49. public function __construct(OutputInterface $output, $max = 0)
  50. {
  51. if ($output instanceof ConsoleOutputInterface) {
  52. $output = $output->getErrorOutput();
  53. }
  54. $this->output = $output;
  55. $this->setMaxSteps($max);
  56. if (!$this->output->isDecorated()) {
  57. // disable overwrite when output does not support ANSI codes.
  58. $this->overwrite = false;
  59. if ($this->max > 10) {
  60. // set a reasonable redraw frequency so output isn't flooded
  61. $this->setRedrawFrequency($max / 10);
  62. }
  63. }
  64. $this->setFormat($this->determineBestFormat());
  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)
  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. public function setMessage($message, $name = 'message')
  126. {
  127. $this->messages[$name] = $message;
  128. }
  129. public function getMessage($name = 'message')
  130. {
  131. return $this->messages[$name];
  132. }
  133. /**
  134. * Gets the progress bar start time.
  135. *
  136. * @return int The progress bar start time
  137. */
  138. public function getStartTime()
  139. {
  140. return $this->startTime;
  141. }
  142. /**
  143. * Gets the progress bar maximal steps.
  144. *
  145. * @return int The progress bar max steps
  146. */
  147. public function getMaxSteps()
  148. {
  149. return $this->max;
  150. }
  151. /**
  152. * Gets the progress bar step.
  153. *
  154. * @deprecated since version 2.6, to be removed in 3.0. Use {@link getProgress()} instead.
  155. *
  156. * @return int The progress bar step
  157. */
  158. public function getStep()
  159. {
  160. @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);
  161. return $this->getProgress();
  162. }
  163. /**
  164. * Gets the current step position.
  165. *
  166. * @return int The progress bar step
  167. */
  168. public function getProgress()
  169. {
  170. return $this->step;
  171. }
  172. /**
  173. * Gets the progress bar step width.
  174. *
  175. * @internal This method is public for PHP 5.3 compatibility, it should not be used.
  176. *
  177. * @return int The progress bar step width
  178. */
  179. public function getStepWidth()
  180. {
  181. return $this->stepWidth;
  182. }
  183. /**
  184. * Gets the current progress bar percent.
  185. *
  186. * @return float The current progress bar percent
  187. */
  188. public function getProgressPercent()
  189. {
  190. return $this->percent;
  191. }
  192. /**
  193. * Sets the progress bar width.
  194. *
  195. * @param int $size The progress bar size
  196. */
  197. public function setBarWidth($size)
  198. {
  199. $this->barWidth = (int) $size;
  200. }
  201. /**
  202. * Gets the progress bar width.
  203. *
  204. * @return int The progress bar size
  205. */
  206. public function getBarWidth()
  207. {
  208. return $this->barWidth;
  209. }
  210. /**
  211. * Sets the bar character.
  212. *
  213. * @param string $char A character
  214. */
  215. public function setBarCharacter($char)
  216. {
  217. $this->barChar = $char;
  218. }
  219. /**
  220. * Gets the bar character.
  221. *
  222. * @return string A character
  223. */
  224. public function getBarCharacter()
  225. {
  226. if (null === $this->barChar) {
  227. return $this->max ? '=' : $this->emptyBarChar;
  228. }
  229. return $this->barChar;
  230. }
  231. /**
  232. * Sets the empty bar character.
  233. *
  234. * @param string $char A character
  235. */
  236. public function setEmptyBarCharacter($char)
  237. {
  238. $this->emptyBarChar = $char;
  239. }
  240. /**
  241. * Gets the empty bar character.
  242. *
  243. * @return string A character
  244. */
  245. public function getEmptyBarCharacter()
  246. {
  247. return $this->emptyBarChar;
  248. }
  249. /**
  250. * Sets the progress bar character.
  251. *
  252. * @param string $char A character
  253. */
  254. public function setProgressCharacter($char)
  255. {
  256. $this->progressChar = $char;
  257. }
  258. /**
  259. * Gets the progress bar character.
  260. *
  261. * @return string A character
  262. */
  263. public function getProgressCharacter()
  264. {
  265. return $this->progressChar;
  266. }
  267. /**
  268. * Sets the progress bar format.
  269. *
  270. * @param string $format The format
  271. */
  272. public function setFormat($format)
  273. {
  274. // try to use the _nomax variant if available
  275. if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
  276. $this->format = self::getFormatDefinition($format.'_nomax');
  277. } elseif (null !== self::getFormatDefinition($format)) {
  278. $this->format = self::getFormatDefinition($format);
  279. } else {
  280. $this->format = $format;
  281. }
  282. $this->formatLineCount = substr_count($this->format, "\n");
  283. }
  284. /**
  285. * Sets the redraw frequency.
  286. *
  287. * @param int $freq The frequency in steps
  288. */
  289. public function setRedrawFrequency($freq)
  290. {
  291. $this->redrawFreq = (int) $freq;
  292. }
  293. /**
  294. * Starts the progress output.
  295. *
  296. * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
  297. */
  298. public function start($max = null)
  299. {
  300. $this->startTime = time();
  301. $this->step = 0;
  302. $this->percent = 0.0;
  303. if (null !== $max) {
  304. $this->setMaxSteps($max);
  305. }
  306. $this->display();
  307. }
  308. /**
  309. * Advances the progress output X steps.
  310. *
  311. * @param int $step Number of steps to advance
  312. *
  313. * @throws \LogicException
  314. */
  315. public function advance($step = 1)
  316. {
  317. $this->setProgress($this->step + $step);
  318. }
  319. /**
  320. * Sets the current progress.
  321. *
  322. * @deprecated since version 2.6, to be removed in 3.0. Use {@link setProgress()} instead.
  323. *
  324. * @param int $step The current progress
  325. *
  326. * @throws \LogicException
  327. */
  328. public function setCurrent($step)
  329. {
  330. @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);
  331. $this->setProgress($step);
  332. }
  333. /**
  334. * Sets whether to overwrite the progressbar, false for new line.
  335. *
  336. * @param bool $overwrite
  337. */
  338. public function setOverwrite($overwrite)
  339. {
  340. $this->overwrite = (bool) $overwrite;
  341. }
  342. /**
  343. * Sets the current progress.
  344. *
  345. * @param int $step The current progress
  346. *
  347. * @throws \LogicException
  348. */
  349. public function setProgress($step)
  350. {
  351. $step = (int) $step;
  352. if ($step < $this->step) {
  353. throw new \LogicException('You can\'t regress the progress bar.');
  354. }
  355. if ($this->max && $step > $this->max) {
  356. $this->max = $step;
  357. }
  358. $prevPeriod = (int) ($this->step / $this->redrawFreq);
  359. $currPeriod = (int) ($step / $this->redrawFreq);
  360. $this->step = $step;
  361. $this->percent = $this->max ? (float) $this->step / $this->max : 0;
  362. if ($prevPeriod !== $currPeriod || $this->max === $step) {
  363. $this->display();
  364. }
  365. }
  366. /**
  367. * Finishes the progress output.
  368. */
  369. public function finish()
  370. {
  371. if (!$this->max) {
  372. $this->max = $this->step;
  373. }
  374. if ($this->step === $this->max && !$this->overwrite) {
  375. // prevent double 100% output
  376. return;
  377. }
  378. $this->setProgress($this->max);
  379. }
  380. /**
  381. * Outputs the current progress string.
  382. */
  383. public function display()
  384. {
  385. if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
  386. return;
  387. }
  388. // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped.
  389. $self = $this;
  390. $output = $this->output;
  391. $messages = $this->messages;
  392. $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) {
  393. if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
  394. $text = call_user_func($formatter, $self, $output);
  395. } elseif (isset($messages[$matches[1]])) {
  396. $text = $messages[$matches[1]];
  397. } else {
  398. return $matches[0];
  399. }
  400. if (isset($matches[2])) {
  401. $text = sprintf('%'.$matches[2], $text);
  402. }
  403. return $text;
  404. }, $this->format));
  405. }
  406. /**
  407. * Removes the progress bar from the current line.
  408. *
  409. * This is useful if you wish to write some output
  410. * while a progress bar is running.
  411. * Call display() to show the progress bar again.
  412. */
  413. public function clear()
  414. {
  415. if (!$this->overwrite) {
  416. return;
  417. }
  418. $this->overwrite(str_repeat("\n", $this->formatLineCount));
  419. }
  420. /**
  421. * Sets the progress bar maximal steps.
  422. *
  423. * @param int $max The progress bar max steps
  424. */
  425. private function setMaxSteps($max)
  426. {
  427. $this->max = max(0, (int) $max);
  428. $this->stepWidth = $this->max ? Helper::strlen($this->max) : 4;
  429. }
  430. /**
  431. * Overwrites a previous message to the output.
  432. *
  433. * @param string $message The message
  434. */
  435. private function overwrite($message)
  436. {
  437. $lines = explode("\n", $message);
  438. // append whitespace to match the line's length
  439. if (null !== $this->lastMessagesLength) {
  440. foreach ($lines as $i => $line) {
  441. if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) {
  442. $lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
  443. }
  444. }
  445. }
  446. if ($this->overwrite) {
  447. // move back to the beginning of the progress bar before redrawing it
  448. $this->output->write("\x0D");
  449. } elseif ($this->step > 0) {
  450. // move to new line
  451. $this->output->writeln('');
  452. }
  453. if ($this->formatLineCount) {
  454. $this->output->write(sprintf("\033[%dA", $this->formatLineCount));
  455. }
  456. $this->output->write(implode("\n", $lines));
  457. $this->lastMessagesLength = 0;
  458. foreach ($lines as $line) {
  459. $len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line);
  460. if ($len > $this->lastMessagesLength) {
  461. $this->lastMessagesLength = $len;
  462. }
  463. }
  464. }
  465. private function determineBestFormat()
  466. {
  467. switch ($this->output->getVerbosity()) {
  468. // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
  469. case OutputInterface::VERBOSITY_VERBOSE:
  470. return $this->max ? 'verbose' : 'verbose_nomax';
  471. case OutputInterface::VERBOSITY_VERY_VERBOSE:
  472. return $this->max ? 'very_verbose' : 'very_verbose_nomax';
  473. case OutputInterface::VERBOSITY_DEBUG:
  474. return $this->max ? 'debug' : 'debug_nomax';
  475. default:
  476. return $this->max ? 'normal' : 'normal_nomax';
  477. }
  478. }
  479. private static function initPlaceholderFormatters()
  480. {
  481. return array(
  482. 'bar' => function (ProgressBar $bar, OutputInterface $output) {
  483. $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth());
  484. $display = str_repeat($bar->getBarCharacter(), $completeBars);
  485. if ($completeBars < $bar->getBarWidth()) {
  486. $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
  487. $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
  488. }
  489. return $display;
  490. },
  491. 'elapsed' => function (ProgressBar $bar) {
  492. return Helper::formatTime(time() - $bar->getStartTime());
  493. },
  494. 'remaining' => function (ProgressBar $bar) {
  495. if (!$bar->getMaxSteps()) {
  496. throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
  497. }
  498. if (!$bar->getProgress()) {
  499. $remaining = 0;
  500. } else {
  501. $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress()));
  502. }
  503. return Helper::formatTime($remaining);
  504. },
  505. 'estimated' => function (ProgressBar $bar) {
  506. if (!$bar->getMaxSteps()) {
  507. throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
  508. }
  509. if (!$bar->getProgress()) {
  510. $estimated = 0;
  511. } else {
  512. $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps());
  513. }
  514. return Helper::formatTime($estimated);
  515. },
  516. 'memory' => function (ProgressBar $bar) {
  517. return Helper::formatMemory(memory_get_usage(true));
  518. },
  519. 'current' => function (ProgressBar $bar) {
  520. return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
  521. },
  522. 'max' => function (ProgressBar $bar) {
  523. return $bar->getMaxSteps();
  524. },
  525. 'percent' => function (ProgressBar $bar) {
  526. return floor($bar->getProgressPercent() * 100);
  527. },
  528. );
  529. }
  530. private static function initFormats()
  531. {
  532. return array(
  533. 'normal' => ' %current%/%max% [%bar%] %percent:3s%%',
  534. 'normal_nomax' => ' %current% [%bar%]',
  535. 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
  536. 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
  537. 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
  538. 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
  539. 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
  540. 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
  541. );
  542. }
  543. }