tee:终端里的"三通管"
文章导览
前言
最近在调试 LLM 应用时,我遇到了一个特别影响效率的问题:
程序一跑起来,终端日志就像瀑布一样疯狂滚动。等我想找刚才的报错堆栈时,关键信息早已被顶出屏幕,往上翻半天也找不到。
给项目接入正规的日志模块当然更完善,但在快速验证想法、跑 Demo 的阶段,为了保存一次输出就去改代码,未免有些麻烦。
好消息是,这种情况不用改代码,一个 Linux、macOS 等系统自带的命令就能帮上忙:tee。
它可以把同一份输出一边显示在终端,一边保存到文件。通常不影响终端观察,也方便运行结束后搜索和回看。
一条命令,边看边存
以 npm 脚本为例:
mkdir -p ./tmp
npm run --silent demo:04 -- "这个项目有哪些文件?" 2>&1 | tee ./tmp/demo.log核心就是第二行。日志会照常显示在终端,同时悄悄写入 ./tmp/demo.log。等程序跑完,再打开文件慢慢找报错,不用和滚动的屏幕“赛跑”了。
其中:
2>&1:把错误输出合并到正常输出,避免漏掉报错;|:把合并后的输出交给下一个命令;tee ./tmp/demo.log:在终端显示输出,同时保存到文件;--silent:减少 npm 自身的提示信息,可按需省略。
有个小提醒:tee 不会自动创建上级目录,所以示例先用 mkdir -p ./tmp 把目录准备好。
tee:终端里的“三通管”
tee 这个名字来自水管中的“三通管”(T 形管件)。它会接收标准输入,然后把内容分成两路:
- 一路继续输出到终端;
- 一路写入指定文件。
命令输出 ──► tee ──► 终端显示
└──► 写入文件先来看一个最简单的例子:
echo "hello llm" | tee ./tmp/debug.log终端会显示 hello llm,文件中也会保存同样的内容。
关键一步:别漏掉错误日志
每个命令运行时,默认都有三个标准数据流:
| 编号 | 名称 | 缩写 | 作用 |
|---|---|---|---|
0 | 标准输入 | stdin | 接收键盘或其他命令传来的输入 |
1 | 标准输出 | stdout | 输出正常结果 |
2 | 标准错误 | stderr | 输出错误和警告 |
终端默认会同时显示 stdout 和 stderr,所以平时看起来没什么区别。可实际上,它们走的是两条不同的通道。
坑就在这里:管道 | 默认只把 stdout 交给下一个命令。
npm run demo | tee ./tmp/demo.log如果程序同时产生正常输出和报错,tee 只能收到正常输出。报错仍会显示在终端,却不会写进日志文件。
所以,调试时更推荐这样写:
npm run demo 2>&1 | tee ./tmp/demo.log第一次看到 2>&1,是不是有点像乱码?别担心,拆开看就简单了:
2代表stderr;&1代表stdout当前指向的位置。
合起来就是:让 stderr 和 stdout 走同一条通道。这样再接管道,正常信息和报错就都能进入 tee。
这里的 & 很重要:它表示后面的 1 是文件描述符编号,而不是文件名。
# 把 stderr 合并到 stdout
command 2>&1
# 把 stderr 写进一个名为“1”的文件,并不是同一回事
command 2>1不想记文件描述符也没关系,只要记住一句话:2>&1 就是把错误输出并入正常输出。
三个顺手又实用的小技巧
追加写入,保留历史日志
tee 默认会覆盖同名文件。如果想保留之前的日志,增加 -a:
npm run demo 2>&1 | tee -a ./tmp/demo.log文件名加时间,方便回溯
调试不同版本时,可以把时间写进文件名:
npm run demo 2>&1 | tee "./tmp/demo-$(date +%Y%m%d-%H%M%S).log"运行后会得到类似 demo-20260823-143022.log 的文件,方便对比和回溯。
文件存完整,屏幕看重点
tee 会继续把内容传给后面的命令,因此可以一边保存完整日志,一边筛选终端显示内容:
npm run demo 2>&1 | tee ./tmp/demo.log | rg "报错|工具调用"如果没有安装 rg,也可以使用 grep:
npm run demo 2>&1 | tee ./tmp/demo.log | grep -E "报错|工具调用"注意:这里过滤的只是终端最终显示的内容,demo.log 中仍然保留完整日志。
只保存需要的输出
大多数时候,stdout 和 stderr 一起保存最省心。但如果正常输出特别长,或者某些警告实在太吵,也可以只留其中一种。
# 只保存 stdout,丢弃 stderr
npm run demo 2>/dev/null | tee ./tmp/demo.log
# 只保存 stderr,丢弃 stdout
npm run demo 2>&1 1>/dev/null | tee ./tmp/demo.log这里出现了一个新面孔:/dev/null。它是 Linux、macOS 等 Unix 系统中的特殊设备文件,可以把它理解成一个“黑洞”:任何写进去的内容都会被直接丢弃,不会显示,也不会保存。
所以上面两条命令分别表示:
2>/dev/null:丢弃stderr,只让stdout进入tee;2>&1 1>/dev/null:先让stderr指向原本的stdout通道,再把stdout改到/dev/null,最终只有stderr进入tee。
重定向会从左到右执行,因此第二条命令的顺序不能颠倒。看到这里觉得有点绕也很正常——如果暂时不需要区分两种输出,记住最常用的 2>&1 | tee 就够用了。
使用前要留意的三个细节
如果只是日常调试,前面的用法已经够用;下面三个细节主要面向脚本和自动化场景。
日志为什么不实时了?
用了管道后,有些程序的日志会变慢,甚至攒一批后才一起出现。
这通常不是 tee 在故意拖延,而是上游程序或它使用的运行库发现 stdout 不再直接连接终端,于是改变了缓冲策略。Linux、macOS 等系统都可能遇到这种情况。
如果程序支持关闭缓冲或强制刷新,优先使用它自身提供的选项。在 Linux 上,也可以视情况尝试:
stdbuf -oL -eL command 2>&1 | tee ./tmp/demo.log如果程序必须在真实终端环境中才会及时输出,还可以用 script 模拟终端。不过 macOS 和 Linux 的参数不同:
# macOS 常见写法
script -q ./tmp/demo.log command
# Linux(util-linux)常见写法
script -q -c 'command' ./tmp/demo.logscript 记录的是终端会话,日志里可能带有颜色控制符、终端信息或头尾记录,因此它更适合作为缓冲问题的备选方案,而不是所有场景下都替代 tee。
命令失败,状态却是成功?
执行下面的命令时:
command 2>&1 | tee ./tmp/demo.logShell 默认关注管道中最后一个命令,也就是 tee 的退出状态。即使前面的 command 失败,只要 tee正常结束,整条命令仍可能表现为成功。平时手动调试不一定会注意到它,但放进自动化脚本或 CI,就很容易造成误判。
Bash 和 Zsh 可以提前启用 pipefail:
set -o pipefail
command 2>&1 | tee ./tmp/demo.log这样,只要管道中的任意命令失败,整条管道就会返回非零状态。
日志里的奇怪字符是什么?
一些命令会输出 ANSI 颜色代码。保存后直接打开日志,可能看到类似 ^[ 的字符。可以优先关闭上游命令的彩色输出;具体参数因工具而异,常见形式包括 --no-color。
另外,如果进程被 kill -9 强制终止,上游程序尚未刷新的少量日志仍可能丢失。
Windows 平台怎么用
| 环境 | 使用方式 | 说明 |
|---|---|---|
| Linux / macOS / Unix | tee | 通常开箱即用 |
| Windows PowerShell | Tee-Object | PowerShell 内置的等价命令 |
| Windows Git Bash / WSL | tee | 可以直接使用 Unix 写法 |
| 极简容器镜像 | 视镜像而定 | scratch 等镜像可能不包含该命令 |
PowerShell 示例:
npm run --silent demo:04 -- "..." 2>&1 | Tee-Object -FilePath C:\temp\demo.log需要注意,PowerShell 的流和重定向规则与 POSIX Shell 并不完全相同。上面的写法适合日常记录输出;复杂脚本应按所用 PowerShell 版本进一步确认。
一张表快速记住 tee
| 需求 | 命令 |
|---|---|
| 边看边保存 stdout | command | tee output.log |
| 同时保存正常输出和报错 | command 2>&1 | tee output.log |
| 追加到已有日志 | command 2>&1 | tee -a output.log |
| 只保存 stdout | command 2>/dev/null | tee output.log |
| 只保存 stderr | command 2>&1 1>/dev/null | tee output.log |
| 保存完整日志、终端只看重点 | command 2>&1 | tee output.log | rg "关键词" |
写在最后
调试 LLM 应用时,日志就是定位问题的线索。
下次再遇到终端刷屏,先别急着和鼠标滚轮较劲。试着在命令后面加上:
2>&1 | tee ./tmp/debug.log让终端负责实时显示,让日志文件负责完整记录。等程序跑完,再用编辑器或 rg 慢慢搜索。
一个小小的 tee,不会替代正规的日志系统,却很适合那些“先跑起来看看”的时刻。少翻几次屏幕,多留一份线索,调试也能从容不少。