# 看清文件内容：cat、less、head、tail 和 grep 各自解决什么问题

四个“显示文件”命令的区别只在读多少，grep 筛的是行；顺带讲清 tail -F 为什么能扛住日志切割，以及实时管道里 grep 为什么迟迟不出结果

> 命令行操作 · Linux 基础 · 日志排查 · 约 8 分钟 · 09 月 23 日

## 本篇要点

1. cat、head、tail 的共同点是按行读取并输出，区别只在从哪读、读多少；less 不同，它接管终端、按需读取，属于 pager 类程序。
2. cat 的本职是拼接文件，用它通读大文件会把终端回滚缓冲区冲掉，对二进制文件还会刷出乱码；它更适合把内容喂给下一个命令。
3. less 打开大文件很快，常用键只有翻页、g/G、/搜索、n/N、q 几个；它能接管道，也能用 less +F 先跟随、Ctrl+C 后回到浏览模式。
4. tail -n 100 -f 是查实时日志的标准写法：先看最近 100 行，再持续跟进新增内容。
5. tail -f 跟的是打开时拿到的文件对象，日志切割把旧文件改名后它仍在读旧文件，所以会“失声”；tail -F 按文件名跟踪并自动重开新文件，看正在写的日志应当用 -F。
6. grep 不做替换，只按行判断是否匹配，匹配就整行打印；-i、-n、-v、-c、-A/-B/-C、-q 是排查中最常用的选项。
7. grep 默认使用基本正则，模式里的 | 需要 -E 或转义才表示“或者”。
8. grep 接管道或重定向时改为块缓冲，实时管道里会看不到即时输出，加 --line-buffered 可以按行刷新。
9. grep 的退出码有语义：0 表示找到，1 表示没找到，2 表示出错；脚本里用 grep -q 做判断。
10. grep -r 需要路径上每一层目录有 x 权限才能进入、文件有 r 权限才能读内容，报 Permission denied 时去看具体路径的权限。

---

前面三篇解决的是“东西在哪、能不能动”：路径怎么解析、`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`，一般不是好选择，有三个具体后果：

1. **几万行日志会把终端的回滚缓冲区冲掉**。冲掉之后往上翻也翻不回去了，你反而丢掉了本来想找的内容。
2. **对二进制文件用 `cat` 会刷出一屏乱码**，还可能打出一串控制字符，把终端的显示弄乱，得敲 `reset` 才恢复。所以对来源不明的文件先别急着 `cat`。
3. `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`。查日志的标准姿势就是：

```
tail -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`。[2][3]

一个实用的记法：**看正在写的日志，用 `-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`：什么也不打印，只看结果成不成立（下面讲退出码时用）。

### 一个新手必踩的坑：`|` 不是“或者”

```
grep "error|fail" app.log     # 什么都搜不到
```

默认情况下 `grep` 用的是**基本正则表达式**（BRE），里面 `|` 只是个普通字符，所以上面这条命令实际是在找 `error|fail` 这个字符串本身。要表达“或者”，加 `-E` 切换到扩展正则：

```
grep -E "error|fail|timeout" app.log
```

`-E` 之后 `+`、`?`、`()` 这些也都能直接用了。正则本身的写法这里不展开，先记住“想让 `|` 生效就加 `-E`”这一条。

### grep 的专业用法是和别的命令接起来

`grep` 既能读文件，也能从标准输入读，所以你可以拿它筛任何命令的输出：

```
tail -n 200 -f /var/log/app/app.log | grep -i -A 3 "timeout"
```

这条就是“实时盯日志，只显示含 timeout 的那几行，外加后 3 行”。

但它有个反直觉的地方：**这样写，屏幕上可能半天不出东西**。原因不是没有匹配，而是 `grep` 只有在输出直接接终端时才会读一行写一行；一旦输出接到管道或文件，它就改成攒够一块（几 KB）再写一次。[1] 实时流接管道时，就会出现“明明有匹配，却等很久才刷出来”的现象。加 `--line-buffered` 就能强制它按行刷新：

```
tail -n 200 -f /var/log/app/app.log | grep -i --line-buffered "timeout"
```

### 退出码：grep 常被脚本用来做判断

`grep` 的返回值本身携带信息：**找到至少一行返回 0，一行都没找到返回 1，命令本身出错（比如文件不存在）返回 2**。所以想知道“日志里到底有没有出现过 panic”，不用去数输出：

```
if grep -q "panic" /var/log/app/app.log; then
    echo "出现过 panic"
fi
```

`-q` 让它保持安静，只提供这个“是 / 否”。

### 权限在这里再次出现

用 `grep -r` 在一个目录里递归搜索时，往往会看到一堆 `Permission denied`。这不是 `grep` 的问题：想进入某个目录，需要对那一层目录有 `x` 权限，进不去就会被跳过；想读某个文件的内容，还需要那个文件自己的 `r`。排查时要看**报错写的是哪个路径**，再去检查那条路径上每一层目录的权限。

## 怎么选

```mermaid
flowchart LR
  A["要看清文件内容"] --> B{"你要的是多少？"}
  B -->|"整份，慢慢看"| C["less"]
  B -->|"开头 / 结尾几行"| D["head / tail"]
  B -->|"持续新增的行"| E["tail -F"]
  B -->|"符合某个模式的行"| F["grep"]
  B -->|"全部内容，喂给下个命令"| G["cat"]
  F --> H["筛任何命令的输出：命令 | grep"]
  E --> H
```

一句话版本：**要模式用 `grep`，要实时用 `tail -F`，要通读用 `less`，要两头用 `head`/`tail`，要拼接用 `cat`。**

<details><summary>练一下</summary>

下面这条命令为什么可能一条结果都搜不到？

```
grep "connection refused|timeout" app.log
```

**答**：默认使用基本正则，`|` 是普通字符，所以它找的是 `connection refused|timeout` 这个完整字符串。改成 `grep -E "connection refused|timeout" app.log`，或者把竖线转义成 `\|`。

</details>

## 术语表

- pager（分页器）：接管终端、把内容按页浏览的程序，less 属于这一类，man、git log 也用同类的程序，退出按 q。
- 描述符与文件名的区别：tail -f 跟的是打开时拿到的那个文件对象，tail -F 跟的是文件名，所以文件被改名换掉后只有 -F 能继续跟上。
- 基本正则与扩展正则：grep 默认用基本正则，| 是普通字符，加 -E 后才表示“或者”。
- 输出缓冲：程序发现输出没直接接终端时会攒够一块再写，这是实时管道看不到即时结果的原因，--line-buffered 可以改回按行刷新。
- grep 的退出码：0 找到、1 没找到、2 出错，脚本借此判断某段内容是否出现过。

## 来源

1. [grep, pipes and output buffering — 说明输出未直接接终端时会改为块缓冲，以及 --line-buffered 为何能解决实时管道看不到输出的问题](https://pnote.eu/notes/command-line-output-buffering/)
2. [coreutils 邮件列表：tail --follow=name 在文件被改名或删除时的行为讨论 — 印证按名字跟踪与按描述符跟踪的差别](https://lists.nongnu.org/archive/html/bug-coreutils/2024-12/msg00003.html)
3. [Red Hat Enterprise Linux 6.3 Technical Notes: coreutils — 说明 tail --follow 基于 inotify 实现，在不支持 inotify 的文件系统上回退到轮询](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/6.3_technical_notes/coreutils)

---

原文：https://pangzhengboyin.com/articles/reading-and-searching-file-contents-02fcc84d

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
