本文要点
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.log5.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 会把会话同时镜像到当前终端,你照常看得见。后面那几行才是 cat -v 打印的文件内容。逐项拆开看:
- 头尾标记:
Script started on …/Script done on …由script自己写入,不属于被录制程序的输出。-q只是让这些信息不显示在终端上,文件里该写还是写。 [COMMAND_EXIT_CODE="0"]:被录制命令的退出码记在结尾。事后翻日志时不用猜「这次操作到底成没成」。[<not executed on terminal>]:这次script的父进程不是终端(通过 ssh 非交互执行),util-linux 如实标注了这一点。人在终端里手工录时,这里会换成终端设备名。- 每行结尾的
^M:cat -v把 CR 显示成^M,文件是 CRLF 换行的——pty 层把\n转成了\r\n。file会把它认成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.logScript 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 -vModemManager
NetworkManager
--- 录制文件里:
^[[0m^[[01;34mModemManager^[[0m inputrc profile^M
^[[01;34mNetworkManager^[[0m ^[[01;34miproute2^[[0m ^[[01;34mprofile.d^[[0m^M
被管道接走的那次,ls 老老实实一行一个名字、没有颜色;进了 script 的伪终端之后,ls 认为自己在跟人对话,于是分栏、上色,连 ^[[01;34m 这种转义序列都原封不动写进了日志。
这解释了两个常见现象:
- 为什么
cmd | tee log抓不到完整画面。 管道让程序的 stdout 不再是终端,程序自己就会切到「非交互模式」:颜色关掉、进度条换成一行行文字、ls变单列。想留下「人看到的画面」,就得让程序以为自己面对着人——script干的就是这件事。 - 为什么明明用了
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.logone
two
--- 时序文件:
0.010224 5
1.992274 3
0.000132 2
--- 回放:
one
two
时序文件每行只有两个数:距上一次输出的间隔(秒) 和 本次输出了多少字节。中间那行 1.992274 3 就是 sleep 2 留下的痕迹——间隔约 2 秒,输出 3 字节(two)。回放时 scriptreplay 按这个节奏把字节重新打进终端,所以 one 和 two 之间会真的停顿两秒;录的是 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.txtcol 在 Ubuntu 上由 bsdmainutils 提供,本机路径是 /usr/bin/col。
第二,密码也可能被录下来。 script 记的是终端的字节流:ssh、mysql 这类提示符下的密码因为终端本身不回显,不会进日志;但如果程序把敏感内容打在了屏幕上(带 token 的 curl、export 的明文变量、打印配置的调试命令),它就安安静静躺在文件里了。真要在录制中处理敏感内容,用 -E never 隐去输入,或者干脆跳过这一段。
第三,谁来清理。 录制文件会稳定增长,交给 Linux logrotate 日志轮转 管理,再配合 -o 设个上限更稳妥。
和几件容易混淆的工具对比着记更清楚:
- auditd 安全审计 在内核层记账(谁读了哪个文件、执行了哪个系统调用),它不关心屏幕上显示了什么;
- tmux 终端复用 解决的是「会话别断」,
capture-pane抓的是某个窗格此刻的文本,需要人主动触发; - 管道与重定向 是 shell 层的数据搬运,管不到程序对「我是不是面对终端」的判断;
- 而
script录的是终端画面本身——输入、颜色、耗时、交互过程,一个不落。
小结
script 的定位可以用一句话概括:它是终端层的录像机,不是命令输出的收集器。所以它擅长的是三件事——把一次运维操作原样留档(配合 -I/-O/-e 还能分清输入输出和成败)、把一次排错现场完整保存下来、把一段演示录下来供人回放(--log-timing + scriptreplay)。
用之前记住两个坑就够了:非交互场景记得 < /dev/null,脚本里记得加 -e;至于颜色,TERM 设对了它自然就有。
评论 (0)
暂无评论,快来抢沙发吧!