ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux终端输出保存全攻略:重定向、tee、script与tmux技巧

Linux终端输出保存全攻略:重定向、tee、script与tmux技巧

把 Linux 终端里的运行结果保存到文本文件,几乎是每个命令行用户都会遇到的需求。跑排查脚本、看日志、做性能压测,或者只是想留存一段输出,都会面临一个尴尬:屏幕上滚过去的字符稍纵即逝,等到想回头找的时候,早不知道刷到几百行之外了。这篇文章不是给你讲玄学,而是把我在日常运维和写脚本过程中用过的保存终端显示内容的方法一次性整理清楚——从最基础的重定向符号,到既显示又落盘的 tee,再到能完整录制整个会话的 script,以及 tmux/screen 里的缓冲区保存技巧,还有我踩过的一些坑。适合刚接触命令行的新人,也适合已经会用一两个命令但想搞明白背后原理的老手。

1. 先搞清楚:终端输出为什么“存不下来”

1.1 终端不是一份可以随时翻看的文档

很多新手对终端有个误解,以为屏幕上滚过的东西会老老实实留在某个地方,等你想找的时候用鼠标往上拖就行。但终端的本质是一个输入输出交互窗口,显示给用户看的内容只存放在一块内存缓冲区里。缓冲区有上限,行数一多,早期的内容就会被新内容顶掉。更麻烦的是,当你断开远程连接或者关闭终端窗口,这份缓冲区也会跟着消失。所以,与其事后在窗口里捞内容,不如在一开始就让输出“双份落地”。

之前带过的一个新人就闹过这么一出:在 SSH 连接里跑了一个批量任务,输出刷了两千多行,他本想用鼠标往上拉,结果拉完发现滚轮根本带不回最开始那几百行,又不想重跑一遍。最后只能灰溜溜地重新执行,并老老实实把所有输出重定向到文件里。这种事一次两次还好,要是跑的任务本身有副作用,重跑的成本可能就不是浪费几分钟的问题了。

1.2 保存内容之前,先明确你想保存什么

你可能想保存程序的普通输出(比如日志、列表),可能想同时保存错误信息(比如编译报错、脚本调试输出),还可能想保存“完整交互过程”——包括你敲的命令、系统的回显、中间的各种提示符。这三种需求对应的方案完全不同,千万不能一把梭。

  • 保存普通输出,用重定向就够了:command > file
  • 保存输出且实时看到屏幕,用tee
  • 要记录完整会话,包括输入和输出、时间、退出码等,用script
  • 如果已经运行在 tmux/screen 里,还可以直接捕获“屏幕缓冲区”

明确需求后,工具选型其实一目了然。很多人一开始只知道一个重定向符号,遇到“要保存错误又没保存下来”的问题才回头补课。下面我把这几类方案逐个展开。

2. 最基础的输出重定向:>、>> 和 2>&1

2.1 标准输出、标准错误与文件描述符

在 Linux 里,程序运行时会打开三个默认的“文件描述符”:0 是标准输入(stdin),1 是标准输出(stdout),2 是标准错误(stderr)。终端屏幕上能看到的输出,其实由两条通道组成:标准输出用于正常结果,标准错误用于报错和诊断信息。把输出保存到文件,本质就是告诉 shell:“文件描述符 1 别连屏幕了,把数据写到这个文件里去。”

这也是为什么command > file能把部分输出存下来,却存不完整。它只重定向了标准输出,错误信息仍然会跑到屏幕上。如果你看到屏幕上还有解释性文字,但文件里没有,那大概率是因为那一部分走了 stderr。

举一个最常见的例子:

ls no_such_file > output.txt

执行完你会发现屏幕上出现了ls: cannot access 'no_such_file': No such file or directory,但 output.txt 是空的。因为那句报错走的是 stderr(fd 2),而> output.txt只接管了 stdout(fd 1)。这个现象如果你不理解背后的文件描述符,很容易被绕晕。

2.2 覆盖还是追加:> 和 >> 的区别

>会创建或截断文件,也就是每次执行都把目标文件清空然后写入新内容;>>则是追加,保留已有内容,把新输出接在后面。

# 覆盖写 ping -c 5 127.0.0.1 > ping_result.txt # 追加写 echo "---- 第二次测试 ----" >> ping_result.txt ping -c 5 127.0.0.1 >> ping_result.txt

为什么强调这个区别?因为不少人和我一样,第一次用>跑了个挺耗时的命令,跑完发现之前手动整理过的日志文件被清空了。追加模式在需要分多次收集输出时特别有用,比如定时任务脚本里,每次执行都在同一个日志后面追加时间戳和结果,而不是每次覆盖。

我还会给文件名加时间戳,避免覆盖:

command > ~/logs/test_$(date +%Y%m%d_%H%M%S).log

这样每跑一次就是一个新文件,不会互相覆盖。缺点是时间一长文件变多,需要定期清理。如果只是临时看一次结果,无所谓;如果是长期留档,更推荐用追加到同一个按日期命名的日志文件。

2.3 把标准错误也一起存下来

如果需要连同报错一起保存,就要明确把 2 合并到 1,或者单独重定向:

# 报错和正常输出都进同一个文件 command > output.log 2>&1 # 也可以分开存 command > output.log 2> error.log

2>&1的含义是把文件描述符 2 重定向到描述符 1 “当前指向的地方”。这里有一个非常容易踩的细节:2>&1跟在> output.log之后才有效,顺序写反(写成2>&1 > output.log)的话,只会让 stderr 仍然指向屏幕,因为那一刻 fd 1 还没重定向到文件。

我自己就栽过一回。有次写部署脚本,把一长串命令的输出全部收进日志,结果因为把2>&1写在了重定向符号前面,导致报错全打在了屏幕上,日志里干干净净。排查问题的时候对着屏幕愣了半天,后来才意识到是顺序写反了。

2.4 重定向解决的只是“落盘”,看不到屏幕

重定向的缺点很明显:一旦用>或>>,程序的标准输出就不往屏幕走了,你只能对着一个文件干等,不知道命令跑到哪一步。尤其是那种跑十几分钟的编译或者数据同步任务,没有实时输出很难判断进度。这时候要么等命令完成再看文件,要么用 tee 同时显示和保存。

提示:有些程序自带日志参数,优先用程序自己的日志,而不是命令行重定向。比如大部分服务类命令都有-l或--log选项,重定向只能抓到 stdout/stderr,抓不到程序内部的一些执行轨迹。

3. 既要显示又要保存:tee 命令的实战用法

3.1 tee 是管道里的“三通管”

tee 的名字来源于水管工用的 T 形三通管,一头进,两头出。它从标准输入读取数据,然后原样写到标准输出,同时写入参数指定的一个或多个文件。用法很直观:

ping -c 5 127.0.0.1 | tee ping_result.txt

执行的时候屏幕照常显示 ping 结果,同时结果被写进 ping_result.txt。这相当于给命令输出“做了一份复印件”,不需要像重定向那样在屏幕和保存之间二选一。

如果你在跑一个长时间任务,又希望实时看到进度,tee 几乎是唯一顺手的选择:

mvn package 2>&1 | tee build.log

这样即使命令跑得再久,你也能盯着屏幕看输出有没有推进,同时日志文件一直在更新。

3.2 追加、多文件与常用参数

tee 默认会覆盖文件,如果不想覆盖,用-a追加:

command | tee -a trace.log

同时写多个文件也很方便:

command | tee result.txt backup.txt

我常用-a的组合做现场日志留档,比如:

uptime | tee -a ~/server_status.log

每跑一次就追加一行,日志越积越厚,既保留了历史,又不用手动拼接文件。用-a时要注意,如果文件不存在会自动创建,不过文件的父目录必须存在,否则会报错。

3.3 在管道链中灵活使用 tee

tee 的输入不限于直接跟在命令后面,它可以在任意管道中间“截留”数据。比如你要统计一段文本里的关键字数量,又想保留原始输出:

dmesg | tee dmesg_full.log | grep -i error | tee error.log

这个例子看起来有点绕,但实际效果是:dmesg_full.log收全量信息,error.log收过滤后的错误行,屏幕还能看到最终结果。这种多级 tee 很适合在做日志审计时,既保留原始数据、又保留筛选结果。

还有一招是用 tee 来“分流”同一份数据做不同处理。比如:

cat access.log | tee raw_copy.log | awk '{print $1}' | sort -u > unique_ips.txt

这样你既保留了原始访问日志的一份拷贝,又能继续统计独立 IP。相当于在管道链路里给数据复制了一份副本,不影响后续继续处理。

3.4 tee 和 stderr 的配合:顺序一定要对

有些人会在 tee 后面再补一个重定向:command | tee file 2>&1。理解错了。tee 只能接收标准输入上到来的内容。如果命令的 stderr 没有合并到 stdout,错误信息根本不会出现在管道里,自然也就不会被 tee 写进文件。所以正确的组合是:

command 2>&1 | tee -a file.log

把2>&1放在管道前面,而不是放在 tee 的后面,这个顺序问题一定要记牢。我曾经看到有人写command | tee file.log 2>&1,以为这样能把 stderr 也写进去,结果错误照样打在屏幕上,他还百思不得其解。

4. 完整录制终端会话:script 命令全解析

4.1 script 和重定向、tee 的本质区别

重定向和 tee 保存的是程序“打印到标准输出/标准错误”的文字。但终端会话里不仅有程序输出,还有你自己敲的命令、命令的提示符、交互式程序显示的画面变化。这些东西不在 stdout 流里,普通重定向根本抓不到。如果要完整记录一次故障排查过程、一次操作演示,或者给新人示范时的全部交互,script 命令才是正解。

script 会启动一个新的子 shell,并把终端上的原始字节流完整记录下来,包括输入输出、控制字符、光标移动序列。它保存的不是抽干净文字后的纯文本,而是更接近“终端录像”的原始数据。你可以把 script 理解成给终端会话装了一台行车记录仪,重定向只是给 stdout 装了个复印机。

4.2 基本用法与退出方式

最简单的用法:

script session.log

执行后你会看到一个提示,shell 已经在录制状态。接下来的所有操作,包括敲命令、看结果、按回车、光标移动,都会记录到 session.log。退出时输入exit或按 Ctrl+D,script 会打印一句类似“Script done”的话,表示录制结束。

我习惯这样给文件名加时间:

script ~/logs/$(date +%Y%m%d_%H%M%S).log

这样每次记录的会话文件不会互相覆盖,找历史也方便。文件命名看起来是小事,但真到要找某一天的记录时,一个带日期时间的名字能省掉你大量对照文件修改时间的手工活。

4.3 录制原理和回放

script 默认生成的 typescript 文件里带着转义序列。直接用 cat 看可能全是花花绿绿的符号,但用scriptreplay可以配合时间戳文件回放整个操作过程。要生成时间戳,需要先用-t选项:

script -t timing.log session.log # 操作完成后回放 scriptreplay timing.log session.log

回放的效果是原速再现当时的屏幕变化,像是看一份终端录像。如果只需要文字内容,可以把文件里的转义序列去掉,方法我放在后面“踩坑实录”里。

-t生成的 timing 文件很有意思,里面记录了每个屏幕更新的时间间隔。scriptreplay 并不是逐字播放,而是按时间戳控制每次刷新,所以重放的节奏和实际操作几乎一致。这对复盘操作习惯和分析问题很有价值,比如查看某人到底卡在哪一步卡了很久。

4.4 我再补充两个常用参数

  • -q:安静模式,不打印“Script started”和“Script done”之类的提示,日志更干净。
  • -c "command":直接执行一条命令并录制,录制结束自动退出,适合在脚本中无人值守使用。

例如:

script -q -c "top -b -n 1" top_once.log

这样就能把 top 的一次批处理输出完整存下来,而不会进入一个交互式子 shell。如果脚本里需要定时采集某个命令的完整输出,-c这种方式就很省心。

注意:script 记录的是终端字节流,不只记录 stdout。文件里可能包含控制字符,直接当日志读不一定直观。如果是给同事看文字内容,建议配合 sed 或 col 清理一下。

5. 再用 tmux/screen 拉回被刷走的屏幕

5.1 远程连接里滚轮翻不回历史

场景是这样的:你在 SSH 连接里跑了一条命令,输出刷了几千行,想往上翻看但终端窗口只保留了一小段。虽然部分终端模拟器支持回滚缓冲区,但回滚长度有限,而且断开连接后全没了。stderr 也没存文件,这下怎么办?

如果你一开始就在 tmux 或 screen 里运行,问题会简单很多。这两个工具是终端复用器,它们有自己的“屏幕缓冲区”,独立于外面的终端窗口。即使外面的终端窗口已经跟不上了,tmux/screen 内部仍然保留着完整的历史内容,可以被捕获和导出。

5.2 用 tmux 捕获当前窗口内容

tmux 里常用的保存方式有几种。

第一种,直接用 bypass copy mode 的capture-pane命令:

# 先进入 tmux,然后执行 tmux capture-pane -pS - > output.txt

-p表示打印到标准输出,-S -表示从当前缓冲区最开头开始抓取。如果你想抓取当前屏幕可见的 20 行,就写成tmux capture-pane -pS -20 > partial.txt。

第二种,把capture-pane和管道组合,过滤内容:

tmux capture-pane -pS - | grep -i error > errors.txt

这很适合在长输出里现场捞关键行,不用把几百行全留下,只留关心的内容。

需要说明的是,-S -的-表示抓取无限行,从缓冲区最早的记录开始。如果会话运行了很久,缓冲区可能存了成千上万行,导出时要注意文件大小。也可以加上一个正整数上限,比如-S 1000,这样只抓取从当前行往上 1000 条。

5.3 在 screen 会话里开 log

screen 比 tmux 老得多,也支持记录功能。在 screen 会话中按Ctrl+a然后按H,会在当前目录生成screenlog.0文件,把后续终端输出都写进去。再按一次同样的键会停止记录。日志文件名与窗口编号有关,如果同时开多个窗口,会生成 screenlog.0、screenlog.1 等。

可以在启动时加-L参数强制开启记录:

screen -L -S mysession

如果你提前预料到这次操作要留档,用-L就不用后面手动按快捷键了。另外,screen 的 log 默认记录的是写入屏幕的内容,也可能包含控制序列。和 script 一样,查看时可能要清理一下。

5.4 双保险: tmux 里再挂 script

我的日常习惯是:进入 tmux 后,再开一个 script 录制会话。这样既保证随时能翻看屏幕历史,又保证每次操作都有完整文件留档。虽然多一层开销,但对故障复盘或需要给别人写报告的场景,这点开销非常值得。

tmux new -s debug script ~/logs/session_$(date +%H%M%S).log

退出 script 后,tmux 里还能继续操作,屏幕缓冲区历史也还在,互不冲突。即使外面 SSH 会话意外断了,tmux 里的进程还会继续跑,等你重新连回去,屏幕内容还在,script 的录制也还接着录。这算是我远程排查问题时的标准姿势。

6. 踩坑实录:保存失败、乱码、控制字符怎么处理

6.1 文件是空的,或者只有半截

最常见的情况是,命令输出到文件后,打开一看是空的。原因多为重定向了 stdout 但程序把信息写到了 stderr;或者程序检测到输出不是终端时,会改变自己的行为。比如某些命令在非交互模式下会禁用彩色输出、省略进度条,甚至不输出任何内容。

解决办法是先用最简单的命令验证重定向链路,比如echo test > file,再看程序是否有--force、-n之类的参数来确保输出。如果实在搞不定,用stdbuf -oL命令强制行缓冲:

stdbuf -oL command | tee file.log

这样可以让程序的输出更快地刷进管道,避免因为缓冲区没刷新而看起来像卡住或丢内容。-oL表示行缓冲,也就是每输出一行就立刻刷新缓冲区。对于日志类输出,这几乎是必备参数。

6.2 打开文件满屏乱码、ANSI 控制字符

用 script 录制的文件里常见这种问题,其实不只是 script,部分带有彩色输出的程序在用 tee 保存时,也会把ESC[31m这样的 ANSI 转义序列写进文件。直接传给同事或粘贴到文档时,满屏都是^[[0;31m之类的符号,非常难看。

清理控制字符可以这样:

# 去除 ANSI 颜色转义 cat raw.log | sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' > clean.log # script 录制的文件还可以用 col 处理 col -b < raw.log > clean.log

我用 col 比较多,它对 script 生成的换行和退格处理更彻底。如果文件里还夹带光标移动序列,单用 sed 不一定去得干净,组合使用更稳妥。还有一个判断技巧:先file raw.log看看类型,如果显示有 ASCII text,一般清理后就能直接读;如果显示 data,说明控制字符很多,用 col 试试。

6.3 命令运行时间长,如何实时查看并记录

命令要跑半小时,直接command > file的话,文件里一直没内容,等得很慌。这时用 tee 加行缓冲:

command 2>&1 | tee -a running.log

如果程序本身输出缓冲太深,再加 stdbuf:

stdbuf -oL command 2>&1 | tee -a running.log

另一个思路:先重定向到文件,然后用tail -f另开一个窗口实时看:

command > run.log 2>&1 tail -f run.log

这等价于既落盘又看屏幕,区别在于程序能完全按非交互模式运行,不会因为管道关系产生额外影响。tail -f还可以配合grep做实时过滤,比如只关心 ERROR 行:

tail -f run.log | grep --line-buffered ERROR

grep --line-buffered会让 grep 每读到一行就输出一行,而不是等缓冲区满了再刷出来,这样看起来才有“实时”的感觉。

6.4 把保存变成日常习惯

我给自己配了几个 alias 和函数,让保存变成一个肌肉记忆。

# 在 ~/.bashrc 或 ~/.zshrc 里 alias scriptlog='script -q -c' alias saveout='tee -a ~/logs/$(date +%Y%m%d).log'

用到的时候:

scriptlog "make build" build_log.log some_command | saveout

这样不用每次想半天用什么命令,直接套模板就行。尤其是saveout,不管什么命令,只要想留个底,直接加上| saveout就能追加到当天的日志文件里。习惯之后,很多原本需要手动记录的信息都能自动沉淀下来。

6.5 判断该用程序自身日志还是外部保存

最后想提醒一点:保存终端内容只是兜底方案。很多专业工具本身就有完善的日志机制,比如构建工具、包管理器、数据库客户端都支持输出到日志文件。终端环境里看得见的输出,和程序内部完整的日志,信息量完全不在一个级别。该用程序日志的地方,还是优先用程序日志。终端保存主要是给那些“没有日志参数”的命令和“需要完整操作记录”的场景兜底用。

比如说数据库导入工具,你有导入日志文件可以指定路径,就不建议只是把屏幕输出重定向到一个文件里,因为程序日志通常包含更详细的行数、耗时、失败记录,信息密度高得多。终端输出只是给人看的“摘要”,不是给人机分析的“明细”。

7. 我的最终建议:不同场景怎么选

如果让我给一个浓缩版的选择路径,我是这么用的:

  • 只需要一条命令的 stdout 结果,用>
  • 要分次累积收集日志,用>>
  • 既要屏幕显示又要保存,用tee
  • 想把 stderr 也收进来,在命令后加2>&1,注意位置不要写反
  • 要完整记录一次交互式会话、操作演示或故障排查过程,用script
  • 远程环境里想看历史屏幕,用tmux capture-pane或screen -L的 log

7.1 给新手的三个练习

如果你还不熟,花十分钟按顺序做一遍,基本就顺手了:

  1. 执行ls -l / | tee /tmp/ls_root.txt,然后打开文件,验证内容和屏幕上一致。
  2. 执行script /tmp/session1.log,随便敲几条命令,退出后用col -b < /tmp/session1.log | less查看。
  3. 进入 tmux,跑一条大量输出的命令,再用tmux capture-pane -pS - > /tmp/tmux_capture.txt导出缓冲区。

这三个动作覆盖了 80% 的保存需求。第一练 tee,第二练 script 会话录制和清理,第三练 tmux 缓冲区捕获。练完基本不会在遇到“输出丢了”的场景时手足无措。

7.2 一个我后悔没早学会的习惯

现在每次开一个大项目或者排查故障前,我都会顺手敲一句 script,给整个操作留个底。一开始觉得多此一举,后来有一次排查一个跨系统问题的过程持续了两个多小时,中间因为注意力分散漏掉了几条关键报错。靠 script 回放把现场完整还原,才定位到问题。从那以后,“先录再干”就成了我固定的操作习惯。你也可以试试,成本几乎为零,收益却可能很大。

最后再分享一个小技巧:给 script 文件名带时间戳时,用date +%Y%m%d_%H%M%S而不是只在文件名里写一个固定的 "log"。不然第二天就把上一次的覆盖了。文件命名是最容易被忽略的小事,但真正需要翻旧记录的时候,一个好名字能省掉半小时。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进