暗色模式

Linux 终端会话录制:从 script 到 scriptreplay 的操作留痕

技术教程
2026-09-18
2
0
本文要点
  • script 录的不是「命令的输出」,而是整个伪终端会话:键盘输入、屏幕输出、控制码、提示符、交互式程序的界面全都在里面——这是 cmd | tee log 做不到的
  • 录制产物有固定骨架:开头 Script started on …、结尾 Script done on … [COMMAND_EXIT_CODE="0"],中间每行以 ^M(CRLF)结尾;-q 只让终端闭嘴,文件里的头尾照样写
  • ⚠️ 在脚本或 CI 里非交互调用,必须加 < /dev/null:否则 script 会把 stdin 上的一切当成「用户正在敲键盘」,全部灌进录制会话——实测直接吃掉了整段 heredoc 脚本里后面的命令
  • 光有 tty 还不够,程序还要看 TERM:环境里是 TERM=dumb 时录出来只有黑白分栏,改成 TERM=xterm-256color script … 之后,颜色转义码会原样落进日志文件
  • --log-timing 记录「间隔秒数 + 输出字节数」两列,配 scriptreplay 可原速回放;-e 透传子进程退出码(不加则恒为 0),-I/-O 把键盘输入与屏幕输出拆成两个文件

为什么要把终端录下来

服务器出问题、事后复盘的时候,最缺的往往不是日志,而是「当时那个人到底在终端里敲了什么、屏幕上回了什么」。系统日志记的是服务视角(journalctl 系统日志),内核审计记的是系统调用和文件访问视角(auditd 安全审计)——而操作者眼前那块终端上发生的事,这两边都不管。

script 补的正是这个位置。它不 hook 任何命令,也不依赖程序配合:只是给会话套一个伪终端(pty),把流经它的字节原样抄一份到文件。于是「屏幕上是什么样,日志里就是什么样」,包括输入、颜色、进度条和交互式程序的界面。

本文在一台 Ubuntu 22.04 的小机(1 核、内存 1.9G,util-linux 2.37.2)上把它完整跑了一遍,包括几个不亲自跑一次就不会知道的细节。

第一次录制:产物长什么样

最简单的用法是 -c 直接把要录的命令交给它:

script -q -c "uname -r; ls -l /etc/hostname" /tmp/session.log < /dev/null; cat -v /tmp/session.log
5.15.0-30-generic
-rw-r--r-- 1 root root 16 Sep  9 13:57 /etc/hostname
Script started on 2026-09-17 21:03:11+00:00 [<not executed on terminal>]
5.15.0-30-generic^M
-rw-r--r-- 1 root root 16 Sep  9 13:57 /etc/hostname^M

Script done on 2026-09-17 21:03:11+00:00 [COMMAND_EXIT_CODE="0"]

script 录制出来的日志文件:头尾标记、COMMAND_EXIT_CODE 与每行结尾的 ^M

屏幕上先出现的是被录制命令自己的输出——script 会把会话同时镜像到当前终端,你照常看得见。后面那几行才是 cat -v 打印的文件内容。逐项拆开看:

  • 头尾标记Script started on … / Script done on …script 自己写入,不属于被录制程序的输出。-q 只是让这些信息不显示在终端上,文件里该写还是写。
  • [COMMAND_EXIT_CODE="0"]:被录制命令的退出码记在结尾。事后翻日志时不用猜「这次操作到底成没成」。
  • [<not executed on terminal>]:这次 script 的父进程不是终端(通过 ssh 非交互执行),util-linux 如实标注了这一点。人在终端里手工录时,这里会换成终端设备名。
  • 每行结尾的 ^Mcat -v 把 CR 显示成 ^M,文件是 CRLF 换行的——pty 层把 \n 转成了 \r\nfile 会把它认成 ASCII text, with CRLF, LF line terminators。后面要用 awk/sed 处理这份日志时,别被行尾那个 \r 绊住。

那个 < /dev/null 不是可有可无的

上面命令里的 < /dev/null 是踩坑之后补上的。script 的职责是把输入和输出都记下来,所以它会持续从 stdin 读数据送进 pty。交互式终端里 stdin 就是你的键盘,一切正常;但在脚本或 CI 里,stdin 可能是管道、heredoc 或别的什么东西——这些内容会被当成「你敲的字」灌进录制会话。

我第一次在一段 heredoc 脚本里调用 script 时,它把 heredoc 后面剩下的所有行都读走了,后面的命令一条都没执行。这个行为可以用管道复现:

printf 'echo from-pipe\n' | script -q -c "bash" /tmp/ops.log
Script started on 2026-09-17 21:02:29+00:00 [<not executed on terminal>]
echo from-pipe^M
root@MFY001832765701:/tmp# echo from-pipe^M
from-pipe^M
root@MFY001832765701:/tmp# exit^M

Script done on 2026-09-17 21:02:29+00:00 [COMMAND_EXIT_CODE="0"]

注意文件里既有输入(第一行 echo from-pipe),也有它引发的一连串结果(提示符、命令回显、from-pipe 这行输出)。这正是「操作留痕」想要的效果——键盘上敲了什么,日志里就有什么。反过来说,在非交互环境里调用它时,< /dev/null 决定了后面半段脚本还能不能跑。

它录的是终端,不是输出

把同一条命令分别放进管道和放进 script,差别一眼可见:

ls --color=auto /etc | head -2 | cat -v
TERM=xterm-256color script -q -c "ls --color=auto /etc" /tmp/color.log < /dev/null > /dev/null
echo "--- 录制文件里:"; sed -n '2,3p' /tmp/color.log | cat -v
ModemManager
NetworkManager
--- 录制文件里:
^[[0m^[[01;34mModemManager^[[0m                   inputrc                 profile^M
^[[01;34mNetworkManager^[[0m                 ^[[01;34miproute2^[[0m                ^[[01;34mprofile.d^[[0m^M

同一条 ls:管道里没颜色,录制文件里连颜色转义码都留着

被管道接走的那次,ls 老老实实一行一个名字、没有颜色;进了 script 的伪终端之后,ls 认为自己在跟人对话,于是分栏、上色,连 ^[[01;34m 这种转义序列都原封不动写进了日志。

这解释了两个常见现象:

  1. 为什么 cmd | tee log 抓不到完整画面。 管道让程序的 stdout 不再是终端,程序自己就会切到「非交互模式」:颜色关掉、进度条换成一行行文字、ls 变单列。想留下「人看到的画面」,就得让程序以为自己面对着人——script 干的就是这件事。
  2. 为什么明明用了 script,颜色有时还是没有。 光有 tty 不够,程序还要看 TERM。我最初在非交互 ssh 里跑,环境变量是 TERM=dumb,录出来的 ls 是黑白分栏的;显式写成 TERM=xterm-256color script … 才有颜色。写自动化录制脚本时,TERM 值得显式指定。

顺带一提,script 里的会话是真终端,所以 vim 这类全屏程序、apt 的进度条、ssh 的密码提示,都会以正常形态出现在录制文件里。

时序录制与回放:scriptreplay

加上 --log-timing 之后,除了画面本身,每一次输出的时间点也被记了下来:

script -q --log-timing /tmp/timing.log -c "echo one; sleep 2; echo two" /tmp/timed.log < /dev/null
echo "--- 时序文件:"; cat /tmp/timing.log
echo "--- 回放:"; scriptreplay -t /tmp/timing.log -O /tmp/timed.log
one
two
--- 时序文件:
0.010224 5
1.992274 3
0.000132 2
--- 回放:
one
two

时序文件的两列:距上一次输出的间隔秒数、本次输出的字节数

时序文件每行只有两个数:距上一次输出的间隔(秒)本次输出了多少字节。中间那行 1.992274 3 就是 sleep 2 留下的痕迹——间隔约 2 秒,输出 3 字节(two)。回放时 scriptreplay 按这个节奏把字节重新打进终端,所以 onetwo 之间会真的停顿两秒;录的是 vim 或者带光标重绘的程序时,回放出来也是会动的画面,而不只是一堆文本。

回放常用参数:

  • -d <num>:时间除数,-d 2 即两倍速,看长录制时很有用;
  • -m <num>:单次等待上限,避免录制中间有长时间空档时,回放干等;
  • -t/-T 指定时序文件,-O 指定录制文件(-s 是旧别名)。

一个实测结论:util-linux 2.37.2(Ubuntu 22.04 自带版本)上,script -m advanced 这个「结构化日志格式」虽然能被接受,写出来的仍然是 classic 格式——想靠它直接拿 JSON 日志的话,得先把 util-linux 升上去。

几个真正常用的开关

开关作用
-a追加到已有日志,而不是覆盖
-e用被录制命令的退出码作为 script 的退出码
-f每次写入都刷盘,可实时 tail -f 观察
-I / -O / -B分别记录 stdin / stdout / 两者
-E <when>控制是否回显输入(auto/always/never
-o <size>输出超过上限就终止,防止磁盘被写满
--force目标文件是符号链接时也照写(默认拒绝)

-e 值得单独说一句:不加它,script 自己永远返回 0,包在 CI 或者 Makefile 里会把失败当成功。

script -q -e -c "exit 7" /tmp/ec.log < /dev/null > /dev/null; echo "rc_with_e=$?"; script -q -c "exit 7" /tmp/ec2.log < /dev/null >/dev/null; echo "rc_without_e=$?"
rc_with_e=7
rc_without_e=0

-I 在留痕场景里特别顺手:它把「人敲进去的」和「屏幕吐出来的」分成两个文件,复盘时一眼就能分清哪些是操作、哪些是系统反馈。

printf 'whoami\nid -u\n' | script -q -I /tmp/in2.log -O /tmp/out2.log -c "bash"
Script started on 2026-09-17 21:01:42+00:00 [<not executed on terminal>]
whoami
id -u

Script done on 2026-09-17 21:01:42+00:00 [COMMAND_EXIT_CODE="0"]

上面是 in2.log 的内容:只有输入,没有一行输出;out2.log 则相反,是完整的屏幕画面。

-E never 会连输入都不记,只保留屏幕输出——实测同一条命令,日志里只剩提示符和结果,echo hi 这行输入本身不见了:

Script started on 2026-09-17 21:06:53+00:00 [<not executed on terminal>]
root@MFY001832765701:~# hi^M
root@MFY001832765701:~# exit^M

Script done on 2026-09-17 21:06:53+00:00 [COMMAND_EXIT_CODE="0"]

放进运维流程之前,先想清楚三件事

第一,日志里有控制码,别直接 grep。 录制文件混着 ANSI 转义和 \r,肉眼看可以 cat -v,机器处理前最好先洗一遍:

col -b < /tmp/session.log > /tmp/clean.txt

col 在 Ubuntu 上由 bsdmainutils 提供,本机路径是 /usr/bin/col

第二,密码也可能被录下来。 script 记的是终端的字节流:sshmysql 这类提示符下的密码因为终端本身不回显,不会进日志;但如果程序把敏感内容打在了屏幕上(带 token 的 curlexport 的明文变量、打印配置的调试命令),它就安安静静躺在文件里了。真要在录制中处理敏感内容,用 -E never 隐去输入,或者干脆跳过这一段。

第三,谁来清理。 录制文件会稳定增长,交给 Linux logrotate 日志轮转 管理,再配合 -o 设个上限更稳妥。

和几件容易混淆的工具对比着记更清楚:

  • auditd 安全审计 在内核层记账(谁读了哪个文件、执行了哪个系统调用),它不关心屏幕上显示了什么;
  • tmux 终端复用 解决的是「会话别断」,capture-pane 抓的是某个窗格此刻的文本,需要人主动触发;
  • 管道与重定向 是 shell 层的数据搬运,管不到程序对「我是不是面对终端」的判断;
  • script 录的是终端画面本身——输入、颜色、耗时、交互过程,一个不落。

小结

script 的定位可以用一句话概括:它是终端层的录像机,不是命令输出的收集器。所以它擅长的是三件事——把一次运维操作原样留档(配合 -I/-O/-e 还能分清输入输出和成败)、把一次排错现场完整保存下来、把一段演示录下来供人回放(--log-timing + scriptreplay)。

用之前记住两个坑就够了:非交互场景记得 < /dev/null,脚本里记得加 -e;至于颜色,TERM 设对了它自然就有。

发表评论

暂无评论,快来抢沙发吧!