# 命令的输出去哪了：0、1、2 三个描述符，和 >、2>&1、| 到底改了什么

标准输入、标准输出、标准错误分别是什么，> 与 >> 改写了谁，2>&1 为什么和书写顺序有关，以及管道为什么只接 stdout

> 命令行操作 · 日志排查 · Linux 基础 · 约 5 分钟 · 10 月 07 日

## 本篇要点

1. 每个进程一启动就带三个带编号的通道：0 是标准输入，1 是标准输出，2 是标准错误；程序只管往编号里写，不关心它连到哪里。
2. 重定向是 shell 在启动命令之前完成的，所以命令自己完全不知道输出被改去了哪。
3. > 默认作用于 1 并把目标文件先截断成 0 字节，>> 是追加；截断发生在命令运行之前，因此命令失败也可能已经清空了文件。
4. 只写 > 时错误（2）不受影响，仍会打到终端；要接走错误必须显式写 2>。
5. 2>&1 是把 2 变成 1 当前指向的拷贝，拷贝的是当时的值，所以顺序重要：> log 2>&1 让两股都进 log，2>&1 > log 只让 stdout 进 log。
6. &> log 是 > log 2>&1 的简写，&>> log 对应追加。
7. 管道只把左边命令的 1 接到右边命令的 0，不经过 stderr；要连错误一起筛必须写 2>&1 |，或用简写 |&。
8. 管道连接先于命令自带的重定向建立，所以 2>&1 | 里的 1 已经指向管道，2 拷贝到的就是管道。
9. 不要写 > log 2> log，也别对正在写的日志用 >。

---

上一篇里出现过 `tail -F app.log | grep timeout` 这样把两个命令接起来的写法，服务器上的启动脚本里也到处是 `> log 2>&1` 这种看起来很像乱码的东西。这篇要讲清楚的是同一件事：一条命令打出来的输出，默认去哪，`>`、`2>`、`2>&1`、`|` 又把它改去了哪。

搞明白这一层，日志排查里那些“明明重定向了还是有东西冒出来”“错误怎么不见了”“为什么只有一半进了文件”的怪事，就都变成理所当然的了。

## 一条命令有三个固定出口，编号 0、1、2

先看一个现象：

```
ls /etc /nonexistent
```

屏幕上会出现两类东西：`/etc` 下的文件列表，和一行 `ls: cannot access '/nonexistent': No such file or directory`。看起来都在屏幕上，其实是两条不同的流。

程序本身并不知道“屏幕”是什么。每个进程一启动，内核就给它准备好三个带编号的通道，叫做**文件描述符**（file descriptor，常缩写为 fd）：

- 0：标准输入（stdin），程序从这里读东西；
- 1：标准输出（stdout），正常结果写这里；
- 2：标准错误（stderr），报错信息写这里。

程序只管往编号 1 或 2 里写，完全不关心编号背后连的是终端、文件还是管道。默认情况下 shell 把这三个编号都接到你的终端，所以两股输出混在一起显示，你分不出哪句来自 1、哪句来自 2。

这里有个关键认识：**重定向不是命令做的，是 shell 做的**。shell 在启动命令这个进程之前，先把 0、1、2 接到你指定的地方，然后再执行。所以 `ls` 从头到尾都不知道自己写去的是文件。[1]

## `>` 和 `>>`：把 1 接到文件

```
ls -l > list.txt
```

`>` 后面跟文件名，就是“让 1 指向这个文件”。文件不存在就创建；已经存在，就**先截断成 0 字节**（truncate），再往里写。想追加而不是覆盖，用 `>>`。

这里有个容易吃亏的地方：截断发生在命令运行**之前**。所以 `ls /nonexistent > out.txt` 里，`ls` 什么都没输出、还报了错，但 `out.txt` 已经被清空了。你不能靠“命令失败了文件就没动”来自我保护——真要保留旧内容，就得用 `>>`。

`>` 省掉编号时默认作用于 1；把符号换成 `<`，就默认作用于 0，这是输入侧的重定向，比如 `wc -l < app.log` 让 `wc` 从文件读，而不是从键盘读。完整写法是 `[n]>文件` 或 `[n]<文件`。[1]

## `2>`：只把错误接走

只写 `>` 的时候，它只管编号 1。错误（编号 2）照样打在屏幕上——这正是“我明明重定向了，怎么还有东西冒出来”最常见的答案。

要让两股分开，就分别指定编号：

```
ls /etc /nonexistent > ok.log 2> err.log
```

`> ok.log` 把 1 接到 `ok.log`，`2> err.log` 把 2 接到 `err.log`。文件列表进 `ok.log`，报错进 `err.log`。

既不想看错误又不想存，就丢进 `/dev/null`——一个专门“吃”掉写入内容的空设备：`2>/dev/null`。

## `2>&1`：让 2 变成 1 的一份拷贝

想把两股合到一起，新手容易写 `> log 2> log`。这很危险：两个描述符各自独立打开同一个文件，写入位置互不知情，会互相覆盖。

正确写法是 `2>&1`。这里的 `&` 在说“1 是一个描述符编号，不是名叫 1 的文件”。整句话的意思是：**让 2 变成 1 此刻所指目标的拷贝**。

“此刻”是关键。Bash 从左到右处理一条命令上的重定向 [1]，所以下面两条顺序不同的命令，结果完全不同：

```
cmd > log 2>&1      # 1 先接到 log，然后 2 拷贝 1 —— 两个都写进 log
cmd 2>&1 > log      # 2 先拷贝 1（此时 1 还是终端），然后 1 才接到 log —— 只有 stdout 进 log
```

第一种才是你要的：日志文件里既有正常输出也有报错。第二种里，2 已经拷成了“终端”，后面 1 再改去 `log` 跟它无关，所以错误照旧打在屏幕上，`log` 里只有正常输出。[3]

可以借变量来记：`2>&1` 相当于赋值 `2 = 1`，拷贝的是当时的值。之后再给 1 赋新值，2 不会跟着变。

因为这种写法用得太频繁，Bash 给了简写：`&> log` 等价于 `> log 2>&1`，`&>> log` 等价于 `>> log 2>&1`。[1]

## `|` 只接 stdout，不接 stderr

管道的规则一句话：**把左边命令的 1，接到右边命令的 0**，中间不经过任何文件。[2]

就这么一条，却解释了一整类困惑。`./app | grep timeout` 里，`app` 写进 stderr 的错误根本没进管道——它还是直接打在终端上，`grep` 永远看不到。所以“我接了管道，怎么错误还是刷屏”是正常现象，不是管道没生效。[4]

要让错误也进管道，在管道前面加 `2>&1`：

```
./app 2>&1 | grep -i error
```

这样能生效，是因为 bash 建立管道这一步，发生在处理命令自带的那些重定向之前 [2]。也就是说，`|` 已经把 1 接到管道上了，接着的 `2>&1` 让 2 拷贝这个“已经指向管道的 1”，于是两股都进了管道。`|&` 就是 `2>&1 |` 的简写。

反过来，只想筛错误、把正常输出丢掉，就写成 `./app 2>&1 >/dev/null | grep -i error`：先让 2 跟着 1 进管道，再把 1 改去 `/dev/null`，两者互不影响。这仍然是同一条“拷贝当时的值”的规则在起作用。

## 可以直接抄的两个写法

把一次部署的完整输出记下来：

```
./deploy.sh > deploy.log 2>&1
```

正常情况下屏幕上一片安静，全部内容都在 `deploy.log` 里；出问题也不会只截到一半。想同时看着它滚动，就用 `./deploy.sh 2>&1 | tee deploy.log`（`tee` 一边写文件一边往屏幕复制一份）。

最后提醒一句：`>` 会无条件截断目标文件，所以不要对正在被服务写着的日志用 `> app.log`——那是清空日志，不是查看日志。

## 术语表

- 文件描述符：进程启动时内核给的带编号通道，0 用来读、1 写正常输出、2 写错误输出。
- 标准输出与标准错误：同一程序的两条独立输出流，默认都接到终端，所以混在一起看不出来。
- 重定向：shell 在运行命令之前，把某个编号接到文件或另一个编号上。
- 2>&1：让 2 变成 1 当前目标的拷贝，拷贝的是当时的值，之后 1 再变不影响它。
- 管道：把左边命令的 1 接到右边命令的 0，只传 stdout，不传 stderr。

## 来源

1. [GNU Bash 参考手册：Redirections — 重定向操作符、省略编号时的默认作用对象、从左到右的处理顺序](https://www.gnu.org/software/bash/manual/bash.html#Redirections)
2. [bash(1) 手册页 Pipelines 一节 — 管道连接 stdout 到 stdin，且先于命令自带的重定向建立](https://man7.org/linux/man-pages/man1/bash.1.html)
3. [BashFAQ/055 — >file 2>&1 与 2>&1 >file 的差别，以及为什么顺序重要](https://mywiki.wooledge.org/BashFAQ/055)
4. [BashFAQ/047 — 管道只承载 stdout，以及把 stderr 送进管道的写法](https://mywiki.wooledge.org/BashFAQ/047)

---

原文：https://pangzhengboyin.com/articles/stdout-stderr-redirection-and-pipes-65b9106c

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