如何在命令行中输出进度条

之前就比较好奇,命令行一般输出都是往后追加,好像不知道怎么做覆盖更新,但是很多比较厉害的shell脚本都有那种进度条,甚至整个重绘
所以就来看下,怎么在命令行里输出个会变的进度条,
比如我在命令行中输出两行

1
2
3
echo "111\n";
echo "222\n";
exit;

最简单就是这样子,那如果想把111改成222呢
我们发现”\n”是换行,那”\r”其实是原地移动到行首
可以再看下

1
2
3
4
echo "111\r";
usleep(1000*1000);
echo "222\n";
exit;

加了sleep是因为闪太快了看不到,这样就能看到这一行就重新刷新了
然后就可以看下进度条可以怎么输出了

1
2
3
4
5
6
for ($i = 0; $i <= 50; $i++) {
printf("progress: [%-50s] %d%%\r", str_repeat('#',$i), $i * 2);
usleep(1000 * 100);
}
echo "\n";
echo "Done.\n";

首先是解释下这段的含义

1
2
3
4
5
"progress: [%-50s] %d%%\r"
└──┬──┘ │ │ └─ 回到当前行开头
│ │ └──── 输出一个真正的 %
│ └────── 输出整数
└──────────── 输出宽度为 50 的字符串

感觉又变成了一个视觉游戏,以为是逐渐的在往后画
其实是一直在刷新这个进度条
首先是定义一个长度是50个字符的输出,用空格补全,然后会输出对应百分比,比如2%
就对应的是一个井号,然后用”\r”回到行首重绘
实际的就是第一次画一个#号,第二次画两个#号
这里最大的重点就是要让部分字符看着在变,部分看着不变,才能形成这种视觉假象
比如这里的50个字符长度,还有对应的”%”符号
否则就会变成一直在跳的了
对应的动的部分就是#号的增加,对应百分比数值也在往上涨
不过这里有点小问题,不知道有没有发现,它的数字没有定义位数
那么在达到100%的时候其实是有一点跳跃的

那如果想要改多行怎么办呢
就需要用ANSI 控制序列让光标向上移动若干行

1
2
3
任务 A:[########          ] 40%
任务 B:[############ ] 60%
总进度:[########## ] 50%

光标此时位于这三行下面。下一次刷新前,先让光标向上移动三行:

1
ESC [ 3 A

在php中可以写成

1
echo "\033[3A";

最简单的就是

1
2
3
4
5
6
echo "111\n";
echo "222\n";
echo "333\n";
echo "\033[3A";
usleep(1000*1000);
echo "444\n";

这样就会把前面的111,222,333给干掉,重新输出444
来个像样点的脚本就是

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
<?php

function progressBar(int $percent, int $width = 30): string
{
$percent = max(0, min(100, $percent));
$filled = (int) floor($percent * $width / 100);

return sprintf(
"[%-{$width}s] %3d%%",
str_repeat('#', $filled),
$percent
);
}

$lineCount = 3;

for ($i = 0; $i <= 100; $i++) {
$taskA = $i;
$taskB = min(100, $i * 2);
$total = (int) (($taskA + $taskB) / 2);

// 第一次输出前不需要移动光标
if ($i > 0) {
printf("\033[%dA", $lineCount);
}

$lines = [
"任务 A:" . progressBar($taskA),
"任务 B:" . progressBar($taskB),
"总进度:" . progressBar($total),
];

foreach ($lines as $line) {
// 回到行首并清除当前整行
echo "\r\033[2K";
echo $line;
echo PHP_EOL;
}

fflush(STDOUT);
usleep(100000);
}

这里的符号代表的含义

控制序列作用
\r回到当前行行首
\033[2K清除当前整行
\033[3A光标向上移动三行
\033[1A光标向上移动一行
\033[1B光标向下移动一行
\033 表示 ESC 字符,所以:
1
echo "\033[3A";

发送给终端的实际含义是:

1
ESC + [3A

也就是“光标向上移动三行”。
还是比较神奇的东西
以前一直没搞明白,刚好这次想到了
现在很多想claude code,包括opencode,pi等都是搞成了命令行的
这个大概就是这些命令行工具的基础
像opencode是使用的

1
2
3
@opentui/core
@opentui/solid
solid-js

pi则是使用

1
2
@earendil-works/pi-tui
其他的框架还有
语言开源库定位
TypeScriptOpenTUIOpenCode 使用的完整 TUI
TypeScriptpi-tuiPi 使用的轻量差量渲染
JavaScriptInk使用 React 编写 CLI
JavaScriptcli-progress单个或多个进度条
GoBubble TeaElm 风格 TUI 框架
RustRatatuiRust 生态常用 TUI
PHPSymfony Console进度条、表格和多区域刷新
PHP 里最省事的是 Symfony Console 的 section()
1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Console\Output\ConsoleOutput;

$output = new ConsoleOutput();
$section = $output->section();

$section->writeln("任务 A:10%\n任务 B:20%\n任务 C:30%");

sleep(1);

$section->overwrite(
"任务 A:40%\n任务 B:50%\n任务 C:60%"
);