diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 097fd99..e1f3899 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -5,6 +5,7 @@ Yii Framework 2 Change Log ---------------------------- - Bug #1446: Logging while logs are processed causes infinite loop (qiangxue) +- Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) - New extension #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul) diff --git a/framework/yii/helpers/BaseConsole.php b/framework/yii/helpers/BaseConsole.php index 92f7c46..0b8e6be 100644 --- a/framework/yii/helpers/BaseConsole.php +++ b/framework/yii/helpers/BaseConsole.php @@ -618,7 +618,7 @@ class BaseConsole * Gets input from STDIN and returns a string right-trimmed for EOLs. * * @param bool $raw If set to true, returns the raw string without trimming - * @return string + * @return string the string read from stdin */ public static function stdin($raw = false) { @@ -651,7 +651,7 @@ class BaseConsole * Asks the user for input. Ends when the user types a carriage return (PHP_EOL). Optionally, It also provides a * prompt. * - * @param string $prompt the prompt (optional) + * @param string $prompt the prompt to display before waiting for input (optional) * @return string the user's input */ public static function input($prompt = null) @@ -776,60 +776,147 @@ class BaseConsole return $input; } + private static $_progressStart; + private static $_progressWidth; + private static $_progressPrefix; + /** - * Displays and updates a simple progress bar on screen. + * Starts display of a progress bar on screen. + * + * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]]. + * + * The following example shows a simple usage of a progress bar: + * + * ```php + * Console::startProgress(0, 1000); + * for ($n = 1; $n <= 1000; $n++) { + * usleep(1000); + * Console::updateProgress($n, 1000); + * } + * Console::endProgress(); + * ``` + * + * Git clone like progress (showing only status information): + * ```php + * Console::startProgress(0, 1000, 'Counting objects: ', false); + * for ($n = 1; $n <= 1000; $n++) { + * usleep(1000); + * Console::updateProgress($n, 1000); + * } + * Console::endProgress("done." . PHP_EOL); + * ``` * - * @param integer $done the number of items that are completed - * @param integer $total the total value of items that are to be done - * @param integer $size the size of the status bar (optional) - * @see http://snipplr.com/view/29548/ + * @param integer $done the number of items that are completed. + * @param integer $total the total value of items that are to be done. + * @param string $prefix an optional string to display before the progress bar. + * Default to empty string which results in no prefix to be displayed. + * @param integer|boolean $width optional width of the progressbar. This can be an integer representing + * the number of characters to display for the progress bar or a float between 0 and 1 representing the + * percentage of screen with the progress bar may take. It can also be set to false to disable the + * bar and only show progress information like percent, number of items and ETA. + * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]]. + * @see startProgress + * @see updateProgress + * @see endProgress */ - public static function showProgress($done, $total, $size = 30) + public static function startProgress($done, $total, $prefix = '', $width = null) { - static $start; + self::$_progressStart = time(); + self::$_progressWidth = $width; + self::$_progressPrefix = $prefix; - // if we go over our bound, just ignore it - if ($done > $total) { - return; - } + static::updateProgress($done, $total); + } - if (empty($start)) { - $start = time(); + /** + * Updates a progress bar that has been started by [[startProgress()]]. + * + * @param integer $done the number of items that are completed. + * @param integer $total the total value of items that are to be done. + * @param string $prefix an optional string to display before the progress bar. + * Defaults to null meaning the prefix specified by [[startProgress()]] will be used. + * If prefix is specified it will update the prefix that will be used by later calls. + * @see startProgress + * @see endProgress + */ + public static function updateProgress($done, $total, $prefix = null) + { + $width = self::$_progressWidth; + if ($width === false) { + $width = 0; + } else { + $screenSize = static::getScreenSize(true); + if ($screenSize === false && $width < 1) { + $width = 0; + } elseif ($width === null) { + $width = $screenSize[0]; + } elseif ($width > 0 && $width < 1) { + $width = floor($screenSize[0] * $width); + } } - - $now = time(); - - $percent = (double)($done / $total); - $bar = floor($percent * $size); - - $status = "\r["; - $status .= str_repeat("=", $bar); - if ($bar < $size) { - $status .= ">"; - $status .= str_repeat(" ", $size - $bar); + if ($prefix === null) { + $prefix = self::$_progressPrefix; } else { - $status .= "="; + self::$_progressPrefix = $prefix; } + $width -= mb_strlen($prefix); - $display = number_format($percent * 100, 0); - - $status .= "] $display% $done/$total"; - - $rate = ($now - $start) / $done; - $left = $total - $done; - $eta = round($rate * $left, 2); - - $elapsed = $now - $start; - - $status .= " remaining: " . number_format($eta) . " sec. elapsed: " . number_format($elapsed) . " sec."; + $percent = $done / $total; + $info = sprintf("%d%% (%d/%d)", $percent * 100, $done, $total); - static::stdout("$status "); + if ($done > $total || $done == 0) { + $info .= ' ETA: n/a'; + } elseif ($done < $total) { + $rate = (time() - self::$_progressStart) / $done; + $info .= sprintf(' ETA: %d sec.', $rate * ($total - $done)); + } + $width -= 3 + mb_strlen($info); + // skipping progress bar on very small display or if forced to skip + if ($width < 5) { + static::stdout("\r$prefix$info "); + } else { + if ($percent < 0) { + $percent = 0; + } elseif ($percent > 1) { + $percent = 1; + } + $bar = floor($percent * $width); + $status = str_repeat("=", $bar); + if ($bar < $width) { + $status .= ">"; + $status .= str_repeat(" ", $width - $bar - 1); + } + static::stdout("\r$prefix" . "[$status] $info"); + } flush(); + } - // when done, send a newline - if ($done == $total) { - echo "\n"; + /** + * Ends a progress bar that has been started by [[startProgress()]]. + * + * @param string|boolean $remove This can be `false` to leave the progress bar on screen and just print a newline. + * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead + * of the progress bar. + * @param bool $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar + * gets removed. Defaults to true. + * @see startProgress + * @see updateProgress + */ + public static function endProgress($remove = false, $keepPrefix = true) + { + if ($remove === false) { + static::stdout(PHP_EOL); + } else { + if (static::streamSupportsAnsiColors(STDOUT)) { + static::clearLine(); + } + static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : '')); } + flush(); + + self::$_progressStart = null; + self::$_progressWidth = null; + self::$_progressPrefix = ''; } }