场景引入:为什么需要tee
刚学Linux那会儿,我经常犯一个低级错误。跑一个编译脚本或者数据迁移任务,看着屏幕上刷过去几百行输出,觉得"反正我肉眼看到了",就没管重定向。结果程序跑到一半报错,屏幕被新输出冲走了,我连错误信息都没看清。更糟的是,想回看刚才的输出只能用鼠标往上滚,终端缓冲区有限,滚到一定位置就截断了。
后来我学乖了,开始用 command > log.txt 2>&1 把输出全存文件。但新的问题来了:文件里倒是全了,可我眼睛看不到实时进度了。跑一个大文件处理任务,终端一片寂静,你不知道是在跑还是在卡死。这个时候tee就派上用场了。
tee是什么
tee是一个从标准输入读取数据,同时写入一个或多个文件以及标准输出的命令。它的名字来源于管道中的三通接头(T型管),一根管子进,两根管子出。tee正好起到这个分流作用:输入流一份送屏幕让你看,一份送文件让你存。
基础用法:同时输出到屏幕和文件
最简单的用法是 command | tee log.txt,把命令的标准输出同时打印到终端和写入文件。
举个实际的例子。我在腾讯云有一台轻量服务器,经常用它跑一些数据处理任务。有一次我需要统计一个10GB的访问日志文件中各个IP的出现次数:
# 使用awk统计日志中每个IP的出现次数,同时用tee保存结果
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | tee ip-count.txt
屏幕上会实时滚动看到IP排名,同时文件里也完整保存了一份。这样你可以随时按 Ctrl+C 中断查看,文件已经存下来了,不会丢数据。
tee -a 追加写入
默认情况下,tee会覆盖目标文件。如果你想追加而不是覆盖,用 -a 参数。
我有写一个监控脚本,每小时采集一次服务器负载,要不断追加到同一个文件里:
# 获取当前时间和服务器负载,追加写入监控日志文件
echo "=== $(date) ===" | tee -a server-monitor.log
# 使用uptime命令获取系统负载,结果追加到同一个日志文件
uptime | tee -a server-monitor.log
# 使用free命令获取内存使用情况,继续追加
free -h | tee -a server-monitor.log
每次跑完,日志文件里就多了一条时间戳和负载记录,不会覆盖之前的。
与管道组合的各种玩法
tee最有价值的地方在于它可以在管道中间"截流"。你可以在管道链的任何位置插入tee,把中间结果存下来,方便调试。
有一次我在处理一个复杂的文本分析任务,管道链很长,不知道是哪个环节出了问题。我在每个关键位置插入tee来调试:
# 第一步:从原始日志提取特定时间的行,同时保存中间结果到step1.txt
grep "2026-07-27" /var/log/app/backend.log | tee step1.txt | \
# 第二步:提取包含ERROR的行,同时保存中间结果到step2.txt
grep "ERROR" | tee step2.txt | \
# 第三步:提取错误码,统计出现次数
awk '{print $NF}' | sort | uniq -c | sort -rn > final-result.txt
第三步出错了。我就去看 step1.txt 和 step2.txt,发现第二步提取出来的根本没有ERROR行,原来后端日志的错误级别是"ERR"不是"ERROR"。如果没有tee保存中间结果,我就得把管道拆开一步一步跑,效率低很多。
同时记录stdout和stderr:command 2>&1 | tee
很多命令会把错误信息输出到stderr(标准错误输出),而tee默认只接管stdout(标准输出)。如果你想连错误信息一起记录,需要先把stderr重定向到stdout:
# 编译C程序,将标准输出和标准错误都重定向到tee,同时显示在屏幕和保存到文件
gcc myprogram.c -o myprogram 2>&1 | tee build.log
如果不加 2>&1,编译报错只会显示在屏幕上而不会写入文件。我踩过这个坑,排查线上问题的时候发现日志文件里干干净净,但错误却真实存在。后来养成了习惯,任何有可能出错的命令都用这个写法。
如果你想分别保存stdout和stderr到不同文件,还可以这样玩:
# 将标准输出通过tee显示并保存到out.log,标准错误保存到err.log
(command 2>&1 | tee out.log) 2>err.log
但这种写法比较绕,实际用得少。日常场景 2>&1 | tee log.txt 就够了。
tee写入多个文件
tee支持同时写入多个文件,依次列出文件名即可:
# 查看系统信息,同时写入三个不同用途的日志文件
uname -a | tee sys.log arch.log kernel.log
更实用的场景是利用 /dev/null 来"吃掉"输出。有时候你既不想看到输出也不想存文件,但又必须使用管道:
# 执行命令,将输出同时写入日志文件和丢弃副本
./long-running-task | tee task.log > /dev/null
这样屏幕干干净净,但日志文件保留完整。
配合sudo tee写受保护文件
这是tee的一个非常经典的用法。当你需要以普通用户身份写一个只有root有权写的文件时,直接 sudo > file 是不行的,因为重定向由当前shell处理,sudo只管命令本身。
你看这个错误写法,初学者几乎都会试一次:
# 错误的写法:直接使用sudo重定向到/etc/hosts会失败
echo "127.0.0.1 myapp.local" > /etc/hosts
# 这个也失败,因为sudo只管echo,不管重定向
sudo echo "127.0.0.1 myapp.local" > /etc/hosts
正确的做法是用 sudo tee:
# 正确写法:使用sudo执行tee,利用tee的权限写入受保护文件
echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts
这个技巧我几乎每周都用。比如配置 nginx 站点时修改 /etc/nginx/sites-available/default,或者往 /etc/sysctl.conf 追加内核参数。记住这个模式:echo "xxx" | sudo tee -a /path/to/protected/file。
tee在脚本中的应用
写shell脚本时,tee的用途更广泛。我维护的一个备份脚本中大量使用了tee:
#!/bin/bash
# 备份脚本:将网站数据和数据库打包并记录日志
BACKUP_DIR="/data/backups"
LOG_FILE="/var/log/backup-$(date +%Y%m%d).log"
# 输出开始备份的时间戳,同时写入日志文件
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始备份..." | tee -a "$LOG_FILE"
# 打包网站目录,同时记录磁盘使用情况
du -sh /var/www/html | tee -a "$LOG_FILE"
# 使用tar打包网站文件,详细输出同时写入日志
tar czf "$BACKUP_DIR/www-$(date +%Y%m%d).tar.gz" /var/www/html 2>&1 | tee -a "$LOG_FILE"
# 检查备份文件大小并记录
ls -lh "$BACKUP_DIR/www-$(date +%Y%m%d).tar.gz" | tee -a "$LOG_FILE"
# 输出完成信息
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份完成" | tee -a "$LOG_FILE"
脚本跑完,屏幕上看到每一步的实时输出,日志文件也完整记录了整个过程。排查问题时,打开日志文件就能还原当时的执行情况。
安全提醒
使用tee时有一个容易被忽视的风险:如果tee的目标文件已经存在,默认会覆盖它。你本意是追加,忘记加 -a 导致原有文件内容全部丢失,这种情况在我团队里发生过不止一次。特别是配合sudo写系统配置文件时,一不留神就把 /etc/nginx/nginx.conf 给清空了。养成好习惯,追加就用 -a,覆盖前确认文件内容不重要。
另外,tee写入大文件时如果没有做好日志轮转(log rotation),单个日志文件会无限增长,很快占满磁盘。生产环境的日志一定要配合 logrotate 使用,或者用 | tee -a 搭配文件大小限制脚本来做。
小结
tee是我日常用得最多的命令之一,基本已经形成肌肉记忆了。它的核心价值就是"分流":既实时看到输出,又持久保存记录。掌握 2>&1 | tee 和 sudo tee 这两个模式,能解决绝大部分使用场景。下次再跑命令,别再纠结"要不要保存输出"了,直接 pipe 给 tee 就行。