前面三篇解决的是“东西在哪、能不能动”:路径怎么解析、cp/mv/rm 会出什么意外、rwx 到底允许你做什么。这篇往下走一步:文件已经找到了,怎么看清里面是什么。
服务器上最常干的两件事——看日志、从日志里找线索——靠的就是这一组命令。它们看起来都是“显示文件内容”,但各自适用的场景差别很大,用错了轻则白等,重则把终端刷得没法用。
前四个命令是同一件事的四种取法
cat、head、tail 走的是同一条路:把文件当 一串按行切开的字节 读出来——读到换行符就算一行——然后写到标准输出。它们不解析格式,不关心这是日志、代码还是配置,只负责把行倒出来。
三者的区别因此非常单纯:
cat从头读到尾,全部输出;head只读开头几行就停;tail只从末尾几行开始输出。
less 不太一样。它不只输出,还会 接管你的终端,把内容当成一页可以上下翻的东西——这类程序通常叫 pager(分页器)。man 和 git log 用的也是 pager,所以你在终端里“卡住了、敲什么都没反应”的时候,往往只是有一个 pager 在等你按 q。
cat:它是用来拼的,不是用来通读的
cat 的名字来自 concatenate,拼接。它的本职是 cat a.txt b.txt > c.txt 这种把多个文件接成一串的操作,往屏幕上打只是它的默认行为。
所以“想看看这个文件里有什么”直接用 cat,一般不是好选择,有三个具体后果:
- 几万行日志会把终端的回滚缓冲区冲掉。冲掉之后往上翻也翻不回去了,你反而丢掉了本来想找的内容。
- 对二进制文件用
cat会刷出一屏乱码,还可能打出一串控制字符,把终端的显示弄乱,得敲reset才恢复。所以对来源不明的文件先别急着cat。 cat file | grep xxx能工作,但多起了一个进程——grep本来就能直接读文件,写成grep xxx file就够了。
less:需要“整体看一眼”时用它
less 是 more 的改进版。它有两个关键性质:
- 按需读取。打开一个几百 MB 的日志几乎是瞬间完成的,因为它只读你看得见的那一屏,往后翻才继续读。
- 不影响终端历史。退出后终端还是干净的,你原来敲的命令都还在。
进去之后要记的键很少:
| 键 | 作用 |
|---|---|
空格 / PageDown | 往下翻一页 |
b | 往回翻一页 |
g / G | 跳到文件头 / 文件尾 |
/关键词 | 向下搜索,n 找下一个,N 找上一个 |
q | 退出 |
less -N file 可以带行号打开。
它还能接管道:任何输出太长的命令都可以套一层,比如 ps aux | less、history | less。这一条下一页讲管道时还会再用到,这里先记住它存在。
less 对查日志还有一个特殊用法:less +F app.log。它一开始会像 tail -f 一样不断追加新内容;你想停下来往回翻历史时按 Ctrl+C,就回到普通浏览模式,按 F 又能继续跟随。实时盯梢和历史回看切换起来比 tail 更方便。
head 和 tail:只要两头
两者默认都输出 10 行,用 -n 改:head -n 50 app.log、tail -n 50 app.log。
真正的重点是 tail -f。它在打印完末尾几行之后 不退出,文件后面每多一行就继续打一行,直到你按 Ctrl+C。查日志的标准姿势就是:
1tail -n 100 -f /var/log/app/app.log先看最后 100 行了解当前发生了什么事,然后挂在那里等新日志出现。这也正是不能用 cat 查日志的原因:服务一直在往文件里追加,cat 只会把“你敲命令那一刻”的内容倒出来就结束,之后新的日志你全看不到。
tail -f 为什么会在日志切割后“失声”
这是实际排查中很常见的一幕:tail -f 跑了半夜,日志突然不再刷新了,而且不是服务停了——你另开一个窗口 tail app.log 能看到新内容。
原因和前面讲过的“改名动的是目录里的条目,不是文件本身”是同一件事。日志通常会被切割:旧的 app.log 被改名成 app.log.1,程序再新建一个 app.log 继续写。而 tail -f 按默认行为跟的是 它打开时拿到的那个文件对象(描述符),不是那个名字。改名之后,这条描述符仍然指向旧文件——也就是现在的 app.log.1。程序往新的 app.log 里写,你这边自然一声不响。
解决办法是用 tail -F。-F 相当于 --follow=name --retry:改成按 文件名 跟踪,发现这个名字指向的文件被换掉了,就重新打开新的那个,并打印提示 has been replaced; following new file。23
一个实用的记法:看正在写的日志,用 -F 而不是 -f,代价几乎没有,但能免掉上面这类莫名其妙的故障。
grep:它不“看文件”,它“筛行”
到这里为止的命令都在决定 看多少。grep 回答的是另一个问题:哪些行是我要的。
它的模型极简单:一行一行读进来,看这一行里有没有出现你要的模式,有就把整行原样打印,没有就丢掉。它不改文件、不做替换、不关心上下文,只是一个过滤器。
grep "timeout" app.log 就够了。下面这些选项是实际排查里高频的:
-i:忽略大小写。日志里Error、ERROR、error混着写,-i error能一次捞全。-n:带上行号,方便你再去less +行号里看上下文。-v:反过来——打印 不 匹配的行。-c:不打印内容,只告诉你匹配了多少行。-A 3/-B 3/-C 3:把匹配行前后各 3 行也打出来。报了错的那一行往往什么也说明不了,真正有用的是它前后几行。-q:什么也不打印,只看结果成不成立(下面讲退出码时用)。
一个新手必踩的坑:| 不是“或者”
1grep "error|fail" app.log # 什么都搜不到默认情况下 grep 用的是 基本正则表达式(BRE),里面 | 只是个普通字符,所以上面这条命令实际是在找 error|fail 这个字符串本身。要表达“或者”,加 -E 切换到扩展正则:
1grep -E "error|fail|timeout" app.log-E 之后 +、?、() 这些也都能直接用了。正则本身的写法这里不展开,先记住“想让 | 生效就加 -E”这一条。
grep 的专业用法是和别的命令接起来
grep 既能读文件,也能从标准输入读,所以你可以拿它筛任何命令的输出:
1tail -n 200 -f /var/log/app/app.log | grep -i -A 3 "timeout"这条就是“实时盯日志,只显示含 timeout 的那几行,外加后 3 行”。
但它有个反直觉的地方:这样写,屏幕上可能半天不出东西。原因不是没有匹配,而是 grep 只有在输出直接接终端时才会读一行写一行;一旦输出接到管道或文件,它就改成攒够一块(几 KB)再写一次。1 实时流接管道时,就会出现“明明有匹配,却等很久才刷出来”的现象。加 --line-buffered 就能强制它按行刷新:
1tail -n 200 -f /var/log/app/app.log | grep -i --line-buffered "timeout"退出码:grep 常被脚本用来做判断
grep 的返回值本身携带信息:找到至少一行返回 0,一行都没找到返回 1,命令本身出错(比如文件不存在)返回 2。所以想知道“日志里到底有没有出现过 panic”,不用去数输出:
1if grep -q "panic" /var/log/app/app.log; then
2 echo "出现过 panic"
3fi-q 让它保持安静,只提供这个“是 / 否”。
权限在这里再次出现
用 grep -r 在一个目录里递归搜索时,往往会看到一堆 Permission denied。这不是 grep 的问题:想进入某个目录,需要对那一层目录有 x 权限,进不去就会被跳过;想读某个文件的内容,还需要那个文件自己的 r。排查时要看 报错写的是哪个路径,再去检查那条路径上每一层目录的权限。
怎么选
一句话版本:要模式用 grep,要实时用 tail -F,要通读用 less,要两头用 head/tail,要拼接用 cat。
练一下
下面这条命令为什么可能一条结果都搜不到?
1grep "connection refused|timeout" app.log答:默认使用基本正则,| 是普通字符,所以它找的是 connection refused|timeout 这个完整字符串。改成 grep -E "connection refused|timeout" app.log,或者把竖线转义成 \|。