惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
WordPress大学
WordPress大学
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
博客园 - Franky
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
Jina AI
Jina AI
Last Week in AI
Last Week in AI
雷峰网
雷峰网
IT之家
IT之家
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX

Nicksxs's Blog

Pi Agent 源码解析(一):从调试环境到双层主循环 6GB显存能跑35B MoE吗:FreeToken极限配置、性能压榨与实操教程 聊聊 Java 的类加载机制一 从ASM到ByteKit再到Arthas:一条Java字节码增强链路 8GB显存如何跑35B:深入理解FreeToken的专家缓存与CPU-GPU协同 让chatgpt来给我讲解下deepseek harness的插件体系 来学习下pi agent的原理 联想 G400 安装 Windows 7 折腾记录 记一个gitea推送失败的问题 学一下chrome的扩展开发 看下chrome的内置模型 从 app.test 到小锁:valet 本地 HTTPS 的完整链路 浅析一下jpeg图片格式及其来源 关于github拉取下载加速的另一个方式 关于适合什么模型,推荐下llmfit 看看目前本地能跑什么模型,使用llama.cpp 最近使用vibe coding的一些感悟 使用php的inotify扩展来监听文件变更 一些设计模式的记忆点 使用xiaomi mimo大模型api运行Hermes Agent 结合Obsidian的cli的一体化体验 开始尝试使用obsidian作为笔记软件 学习下大神的知识库 体验下微软开源的Markdown转换工具Markitdown 学习下git的worktree 一些架构师知识点的记录 解答一下关于traefik的一点疑惑 记录一下迁移服务器需要使用的一些命令 较早代iPhone更换新iPhone的一些小指南 如何查看mac的路由表和网关等信息
如何在命令行中输出进度条
Nicksxs · 2026-08-02 · via Nicksxs's Blog

之前就比较好奇,命令行一般输出都是往后追加,好像不知道怎么做覆盖更新,但是很多比较厉害的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%"
);