PHP中的命令行进度条


Command line progress bar in PHP

我目前正在尝试将进度条添加到命令行脚本中,并尝试了各种解决方案(包括Zend和控制台进度条)。它们都有一个共同的问题,那就是进度条不会停留在窗口的底部,因为在脚本期间,会输出新的行和其他信息。

是否有任何方法可以将进度条保持在终端的底部,但在脚本运行时仍然能够输出其他信息?

[编辑]

我想出来了:

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

希望这是有道理的:)


这是一个很好的CLI进度条:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?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
 *
 * <wyn>
 * for($x=1;$x<=100;$x++){
 *
 *     show_status($x, 100);
 *
 *     usleep(100000);
 *                          
 * }
 * </wyn>
 *
 * @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="
["
;
    $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"
"
;
    }

}

?>


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

1
2
3
4
5
6
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);
}

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

编辑:我已经将echoting
改为escape sequence 33[0g,现在应该可以在OSX和Linux/Unix上使用了。

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

编辑3:更新了打印到stderr的新版本,现在也在github上:https://github.com/macroman/phpterminalprogressbar


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<?
/*

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
 *
 * <wyn>
 * for($x=1;$x<=100;$x++){
 *
 *     show_status($x, 100);
 *
 *     usleep(100000);
 *
 * }
 * </wyn>
 *
 * @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"
"
;
    // 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
$status_bar"
;

    flush();

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

}

我不知道为什么上面的代码有许可证,我只是为了安全而复制它。以下代码没有许可证。免费用于任何目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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"
"
;
    }

    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);
}


以下是针对UNIX机器的。

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

这是一个基地,随时可以扩大。

1
2
3
4
5
6
7
8
9
10
 #!/usr/bin/php
<?php

 @ob_start();

 $shell = system("tput cols");

 @ob_end_clean();

 for( $i= 0 ; $i < $shell ; $i++ ){ echo"█"; usleep(100000); }

enter image description here

ob行动是为了隐藏tputstdout

假设您希望创建一个与任务进度相匹配的进度条。

只需将剩余时间(秒)除以列数。

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

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

在纯bash中做同样的事情:

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

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

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

下面使用strtoupper和ansi代码反向视频。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/php
<?php
$iloop ="0"; /* Outside the loop */
while (true){  
  $warn ="Program running hold on!!
"
;
  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);
}

输出:

enter image description here

有些人可能喜欢通过迭代ANSI代码获得的Party版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/php
<?php
$iloop ="0"; /* Outside the loop */
while (true){    
for ($i=0;$i<=109;$i++){
  $warn ="Program running hold on!!
"
;
  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);
}}

enter image description here

有关ANSI代码的更多信息,请参阅:https://stackoverflow.com/a/48365998/2494754