前面三篇解决的是“东西在哪、能不能动”:路径怎么解析、cp/mv/rm 会出什么意外、rwx 到底允许你做什么。这篇往下走一步:文件已经找到了,怎么看清里面是什么

服务器上最常干的两件事——看日志、从日志里找线索——靠的就是这一组命令。它们看起来都是“显示文件内容”,但各自适用的场景差别很大,用错了轻则白等,重则把终端刷得没法用。

前四个命令是同一件事的四种取法

catheadtail 走的是同一条路:把文件当 一串按行切开的字节 读出来——读到换行符就算一行——然后写到标准输出。它们不解析格式,不关心这是日志、代码还是配置,只负责把行倒出来。

三者的区别因此非常单纯:

  • cat 从头读到尾,全部输出;
  • head 只读开头几行就停;
  • tail 只从末尾几行开始输出。

less 不太一样。它不只输出,还会 接管你的终端,把内容当成一页可以上下翻的东西——这类程序通常叫 pager(分页器)。mangit log 用的也是 pager,所以你在终端里“卡住了、敲什么都没反应”的时候,往往只是有一个 pager 在等你按 q

cat:它是用来拼的,不是用来通读的

cat 的名字来自 concatenate,拼接。它的本职是 cat a.txt b.txt > c.txt 这种把多个文件接成一串的操作,往屏幕上打只是它的默认行为。

所以“想看看这个文件里有什么”直接用 cat,一般不是好选择,有三个具体后果:

  1. 几万行日志会把终端的回滚缓冲区冲掉。冲掉之后往上翻也翻不回去了,你反而丢掉了本来想找的内容。
  2. 对二进制文件用 cat 会刷出一屏乱码,还可能打出一串控制字符,把终端的显示弄乱,得敲 reset 才恢复。所以对来源不明的文件先别急着 cat
  3. cat file | grep xxx 能工作,但多起了一个进程——grep 本来就能直接读文件,写成 grep xxx file 就够了。

less:需要“整体看一眼”时用它

lessmore 的改进版。它有两个关键性质:

  • 按需读取。打开一个几百 MB 的日志几乎是瞬间完成的,因为它只读你看得见的那一屏,往后翻才继续读。
  • 不影响终端历史。退出后终端还是干净的,你原来敲的命令都还在。

进去之后要记的键很少:

作用
空格 / PageDown往下翻一页
b往回翻一页
g / G跳到文件头 / 文件尾
/关键词向下搜索,n 找下一个,N 找上一个
q退出

less -N file 可以带行号打开。

它还能接管道:任何输出太长的命令都可以套一层,比如 ps aux | lesshistory | less。这一条下一页讲管道时还会再用到,这里先记住它存在。

less 对查日志还有一个特殊用法:less +F app.log。它一开始会像 tail -f 一样不断追加新内容;你想停下来往回翻历史时按 Ctrl+C,就回到普通浏览模式,按 F 又能继续跟随。实时盯梢和历史回看切换起来比 tail 更方便。

head 和 tail:只要两头

两者默认都输出 10 行,用 -n 改:head -n 50 app.logtail -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 file23

一个实用的记法:看正在写的日志,用 -F 而不是 -f,代价几乎没有,但能免掉上面这类莫名其妙的故障。

grep:它不“看文件”,它“筛行”

到这里为止的命令都在决定 看多少grep 回答的是另一个问题:哪些行是我要的

它的模型极简单:一行一行读进来,看这一行里有没有出现你要的模式,有就把整行原样打印,没有就丢掉。它不改文件、不做替换、不关心上下文,只是一个过滤器。

grep "timeout" app.log 就够了。下面这些选项是实际排查里高频的:

  • -i:忽略大小写。日志里 ErrorERRORerror 混着写,-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,或者把竖线转义成 \|