ProgressBar.php 17 KB

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