【问题标题】:Command line progress bar in PHPPHP中的命令行进度条
【发布时间】:2021-05-26 03:28:04
【问题描述】:

我目前正在尝试向命令行脚本添加进度条,并且尝试了各种解决方案(包括 Zend 和 Console_ProgressBar)。他们共同的问题是进度条没有粘在窗口底部,因为在脚本执行过程中,会输出新行和其他信息。

有什么办法可以让进度条一直在终端底部,但在脚本运行的时候还能输出其他信息吗?

[编辑]

我想通了:

我实际上不是直接输出到 STDOUT,而是在变量中获取输出,而是使用 echo chr(27) . '[2J' 擦除屏幕,然后将变量的内容输出到 STDOUT,然后附加我的进度条。

希望这是有道理的:)

【问题讨论】:

标签: php


【解决方案1】:

这是一个不错的 cli 进度条:

http://snipplr.com/view/29548/

<?php

/*

Copyright (c) 2010, dealnews.com, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
 * Neither the name of dealnews.com, Inc. nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

 */

/**
 * show a status bar in the console
 * 
 * <code>
 * for($x=1;$x<=100;$x++){
 * 
 *     show_status($x, 100);
 * 
 *     usleep(100000);
 *                           
 * }
 * </code>
 *
 * @param   int     $done   how many items are completed
 * @param   int     $total  how many items are to be done total
 * @param   int     $size   optional size of the status bar
 * @return  void
 *
 */

function show_status($done, $total, $size=30) {

    static $start_time;

    // if we go over our bound, just ignore it
    if($done > $total) return;

    if(empty($start_time)) $start_time=time();
    $now = time();

    $perc=(double)($done/$total);

    $bar=floor($perc*$size);

    $status_bar="\r[";
    $status_bar.=str_repeat("=", $bar);
    if($bar<$size){
        $status_bar.=">";
        $status_bar.=str_repeat(" ", $size-$bar);
    } else {
        $status_bar.="=";
    }

    $disp=number_format($perc*100, 0);

    $status_bar.="] $disp%  $done/$total";

    $rate = ($now-$start_time)/$done;
    $left = $total - $done;
    $eta = round($rate * $left, 2);

    $elapsed = $now - $start_time;

    $status_bar.= " remaining: ".number_format($eta)." sec.  elapsed: ".number_format($elapsed)." sec.";

    echo "$status_bar  ";

    flush();

    // when done, send a newline
    if($done == $total) {
        echo "\n";
    }

}

?>

【讨论】:

  • 这是最好的答案
  • 非常如此。总而言之,该代码 sn-p 的重要部分是在写出新行之前回显"\r"(需要双引号)。
  • 该函数更糟糕的是,您通常以 $c=0 而不是 $c=1 开始 for 循环。因为你从 $c=0 开始,你会得到一个除以零的函数异常。
  • 非常重要:如果您使用宽度太“小”的 tty,这将不起作用。事实上,在这种情况下,如果长行导致换行,则 \r 只是移动到 new 行的开头,而不是第一行。结果一团糟。
  • 25 年的编程经验,我才意识到我可以用\r 做进度条
【解决方案2】:

其他答案似乎过于复杂。我的解决方案是在下次更新之前简单地回显 \033[0G 转义序列,并将光标移回开头。

function progressBar($done, $total) {
    $perc = floor(($done / $total) * 100);
    $left = 100 - $perc;
    $write = sprintf("\033[0G\033[2K[%'={$perc}s>%-{$left}s] - $perc%% - $done/$total", "", "");
    fwrite(STDERR, $write);
}

第一次调用函数会输出进度条,随后的每次调用都会用新的进度条覆盖最后一行。

编辑: 我已将回显 \r 更改为转义序列 \033[0G,这现在应该可以在 OSX 以及 Linux/Unix 上运行。

编辑 2: 根据@tbjers 的建议修复了第 3 行可能出现的错误。

编辑 3: 更新了打印到 STDERR 的新版本,现在也在 GitHub 上:https://github.com/MacroMan/PHPTerminalProgressBar

编辑 4: 现在与作曲家:composer require macroman/terminal-progress-bar

use TerminalProgress\Bar;

$pg = new Bar(1000);

for ($i = 0; $i < 1000; $i++) {
    usleep(10000);
    $pg->tick();
}

【讨论】:

  • 我试过这个,但新的行不断被添加。在 Mac PHP CLI 上测试
  • osx 使用 \r 作为回车和换行,所以这在 osx 上不起作用。
  • 第 3 行应更改为以下内容以避免来自str_repeat:$bar = "[" . ($perc &gt; 0 ? str_repeat("=", $perc - 1) : '') . "&gt;"; 的警告
  • 感谢@tbjers 我已添加您的代码以检查低于 0 的输入。
  • 我会使用 ceil() 函数来确保你达到 100% function progressBar($done, $total) { $perc = ceil(($done / $total) * 100); etc...
【解决方案3】:

以下内容适用于 unix 机器

目标是检索当前终端总计列。 (使用tput

这是一个可以扩展的基地。

 #!/usr/bin/php
 <?php

 @ob_start();
 
 $shell = system("tput cols");
 
 @ob_end_clean();
  
 for( $i= 0 ; $i < $shell ; $i++ ){ echo "█"; usleep(100000); } 

ob 操作在这里隐藏tputstdout

假设您想制作一个与您的任务进度相匹配的进度条。

只需剩余时间(以秒为单位)除以列数。

你可能会以微秒结束,所以最好使用usleep

这样进度条将始终与用户外壳宽度匹配,包括调整大小时。

在纯 bash 中做同样的事情:

for ((i=0; i<$(tput cols); i++)); do echo -e "█\c"; sleep 0.1; done

这显示了 php echo 的主要奇点:它不会追加新行,而 bash echo 会。

当使用循环时,比如说每隔一段时间检查一个条件, 另一种警告正在运行的活动的好方法是文本效果

下面使用strtoupper和ansi代码reverse video

#!/usr/bin/php
<?php
$iloop = "0"; /* Outside the loop */
while (true){  
  $warn = "Program running hold on!!\r";
  if (strlen($warn) === $iloop+1){
    $iloop = "0";
  }
  $warn = str_split($warn);
  $iloop++;
  $warn[$iloop] = "\033[35;2m\e[0m".strtoupper($warn[$iloop]);
  echo " \033[7m".implode($warn);
  usleep(90000);
}

输出:

可能有些人喜欢party版,通过ansi代码迭代获得。

#!/usr/bin/php
<?php
$iloop = "0"; /* Outside the loop */
while (true){    
for ($i=0;$i<=109;$i++){
  $warn = "Program running hold on!!\r";
  if (strlen($warn) === $iloop+1){
    $iloop = "0";
  }
  $warn = str_split($warn);
  $iloop++;
  $warn[$iloop] = "\033[$i;7m".strtoupper($warn[$iloop]);
  echo " \033[7m".implode($warn);
  usleep(90000);
}}

详细了解 ANSI 代码:https://stackoverflow.com/a/48365998/2494754

【讨论】:

    【解决方案4】:

    这是对上一个答案的改进,它处理终端调整大小并使用 2 行而不是 1 行。第一行是时间/百分比等信息,第二行是进度条。

    <?
    /*
    
    Copyright (c) 2010, dealnews.com, Inc.
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
     * Redistributions of source code must retain the above copyright notice,
       this list of conditions and the following disclaimer.
     * Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.
     * Neither the name of dealnews.com, Inc. nor the names of its contributors
       may be used to endorse or promote products derived from this software
       without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
    
     */
    
    /**
     * show a status bar in the console
     *
     * <code>
     * for($x=1;$x<=100;$x++){
     *
     *     show_status($x, 100);
     *
     *     usleep(100000);
     *
     * }
     * </code>
     *
     * @param   int     $done   how many items are completed
     * @param   int     $total  how many items are to be done total
     * @param   int     $size   optional size of the status bar
     * @return  void
     *
     */
    
    function show_status($done, $total, $size=30, $lineWidth=-1) {
        if($lineWidth <= 0){
            $lineWidth = $_ENV['COLUMNS'];
        }
    
        static $start_time;
    
        // to take account for [ and ]
        $size -= 3;
        // if we go over our bound, just ignore it
        if($done > $total) return;
    
        if(empty($start_time)) $start_time=time();
        $now = time();
    
        $perc=(double)($done/$total);
    
        $bar=floor($perc*$size);
    
        // jump to the begining
        echo "\r";
        // jump a line up
        echo "\x1b[A";
    
        $status_bar="[";
        $status_bar.=str_repeat("=", $bar);
        if($bar<$size){
            $status_bar.=">";
            $status_bar.=str_repeat(" ", $size-$bar);
        } else {
            $status_bar.="=";
        }
    
        $disp=number_format($perc*100, 0);
    
        $status_bar.="]";
        $details = "$disp%  $done/$total";
    
        $rate = ($now-$start_time)/$done;
        $left = $total - $done;
        $eta = round($rate * $left, 2);
    
        $elapsed = $now - $start_time;
    
    
        $details .= " " . formatTime($eta)." ". formatTime($elapsed);
    
        $lineWidth--;
        if(strlen($details) >= $lineWidth){
            $details = substr($details, 0, $lineWidth-1);
        }
        echo "$details\n$status_bar";
    
        flush();
    
        // when done, send a newline
        if($done == $total) {
            echo "\n";
        }
    
    }
    

    我不知道为什么上面的代码有许可证,为了安全起见,我只是复制它。波纹管代码未经许可。免费用于任何目的。

    function formatTime($sec){
        if($sec > 100){
            $sec /= 60;
            if($sec > 100){
                $sec /= 60;
                return number_format($sec) . " hr";
            }
            return number_format($sec) . " min";
        }
        return number_format($sec) . " sec";
    }
    
    
    class Timer {
        public $time;
        function __construct(){
            $this->start();
        }
        function start($offset=0){
            $this->time = microtime(true) + $offset;
        }
        function seconds(){
            return microtime(true) - $this->time;
        }
    };
    
    
    // We need this to limit the frequency of the progress bar. Or else it
    // hugely slows down the app.
    class FPSLimit {
        public $frequency;
        public $maxDt;
        public $timer;
        function __construct($freq){
            $this->setFrequency($freq);
            $this->timer = new Timer();
            $this->timer->start();
        }
        function setFrequency($freq){
            $this->frequency = $freq;
            $this->maxDt = 1.0/$freq;
        }
        function frame(){
            $dt = $this->timer->seconds();
            if($dt > $this->maxDt){
                $this->timer->start($dt - $this->maxDt);
                return true;
            }
            return false;
        }
    };
    
    class Progress {
        // generic progress class to update different things
        function update($units, $total){}
    }
    
    class SimpleProgress extends Progress {
        private $cols;
        private $limiter;
        private $units;
        private $total;
    
        function __construct(){
            // change the fps limit as needed
            $this->limiter = new FPSLimit(10);
            echo "\n";
        }
    
        function __destruct(){
            $this->draw();
        }
    
        function updateSize(){
            // get the number of columns
            $this->cols = exec("tput cols");
        }
    
        function draw(){
            $this->updateSize();
            show_status($this->units, $this->total, $this->cols, $this->cols);
        }
    
        function update($units, $total){
            $this->units = $units;
            $this->total = $total;
            if(!$this->limiter->frame())
                return;
            $this->draw();
        }
    }
    
    
    // example
    
    $tasks = rand() % 700 + 600;
    $done = 0;
    
    $progress = new SimpleProgress();
    
    for($done = 0; $done <= $tasks; $done++){
        usleep((rand() % 127)*100);
        $progress->update($done, $tasks);
    }
    

    【讨论】:

    • 太棒了! \x1b[A 位正是我需要拥有多行进度条 :)
    【解决方案5】:

    如果您想使用可以在不同终端中使用的东西而不必担心兼容性,Ncurses 是您的选择。

    检查:https://www.whoishostingthis.com/resources/ncurses/

    The ncurses library provides a robust framework which allows programmers to create visually appealing user interfaces in text mode

    关于此的主要 PHP 文档要点是:https://www.php.net/manual/en/book.ncurses.php

    【讨论】:

      猜你喜欢
      • 2010-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多