之前就比较好奇,命令行一般输出都是往后追加,好像不知道怎么做覆盖更新,但是很多比较厉害的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%
|
光标此时位于这三行下面。下一次刷新前,先让光标向上移动三行:
在php中可以写成
最简单的就是
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 字符,所以: | |
发送给终端的实际含义是:
也就是“光标向上移动三行”。
还是比较神奇的东西
以前一直没搞明白,刚好这次想到了
现在很多想claude code,包括opencode,pi等都是搞成了命令行的
这个大概就是这些命令行工具的基础
像opencode是使用的
1 2 3
| @opentui/core @opentui/solid solid-js
|
pi则是使用
1 2
| @earendil-works/pi-tui 其他的框架还有
|
| 语言 | 开源库 | 定位 |
|---|
| TypeScript | OpenTUI | OpenCode 使用的完整 TUI |
| TypeScript | pi-tui | Pi 使用的轻量差量渲染 |
| JavaScript | Ink | 使用 React 编写 CLI |
| JavaScript | cli-progress | 单个或多个进度条 |
| Go | Bubble Tea | Elm 风格 TUI 框架 |
| Rust | Ratatui | Rust 生态常用 TUI |
| PHP | Symfony 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%" );
|