- 1. Shell 心智模型·炼气篇(一):从 ./script.sh 报错说起,把 / ~ . $PATH .zshrc 一次讲透
- 2. Shell 心智模型·炼气篇(二):从 cat | grep | wc -l 严丝合缝说起,把管道 | 一次讲透
- 3. Shell 心智模型·炼气篇(三):从 `[ -d dir ] || mkdir dir` 说起,把 `[`、`[[ ]]`、`$`、`$?`、`&&`、`||` 一次讲透
- 4. Shell 心智模型·炼气篇(四):从 ./my_script.sh 里 export 出的变量,回终端却查无此变量说起,把子进程、export、source、命令替换一次讲透
命令行为什么能这么灵活地”拼积木”?cat access.log | grep "ERROR" | wc -l 这样一行命令,三个程序严丝合缝地接在一起,没有一行胶水代码——这篇要讲透的,就是 | 这个符号到底是怎么把它们串起来的。
不过在讲管道本身之前,得先打好一层地基:每个程序天生自带的三根”数据管子”——stdin、stdout、stderr。管道说到底,操作的就是这三根管子,不先弄懂它们,管道的行为没法真正说清楚。
一、地基:标准输入/输出/错误是什么
每个命令行程序默认连着三条”数据管道”:
- 标准输入(stdin)——程序读取数据的地方,默认情况下是你的键盘。
- 标准输出(stdout)——程序打印结果的地方,默认情况下是你的终端屏幕。
- 标准错误(stderr)——专门用来打印错误信息的通道,默认也输出到屏幕。
举个例子,你在终端里敲 cat,然后开始打字,它会把你打的每一行原样重新打印出来——因为 cat 没收到文件参数时,就会从标准输入(也就是键盘)读取内容,再打印到标准输出(屏幕)。
有了这三根管子的概念,重定向和管道要做的事,其实就是”把这三根管子里的一根,改接到别处”——重定向接的是文件,管道接的是另一个程序。
二、重定向:改变数据的”去处”
echo "hello" > file.txt这里的 > 把标准输出从”打印到屏幕”改成了”写入 file.txt”——文件原有内容会被覆盖。
echo "world" >> file.txt>> 是追加,不会覆盖,而是接在文件末尾。
sort < file.txt< 是把标准输入从”读键盘”改成”读文件”——让 sort 去读 file.txt 里的内容,而不是等你手动输入。
三、主角登场:管道 | 到底在做什么
地基打好了,现在看这篇真正的主角。管道做的事情是:把前一个命令的标准输出,直接接到后一个命令的标准输入。
拆开看开头那条命令链:
cat access.log | grep "ERROR" | wc -lcat access.log把文件内容打印到标准输出- 这个输出不是打印到屏幕,而是被
|接到了grep "ERROR"的标准输入 grep "ERROR"从这些内容里筛出包含ERROR的行,打印到它的标准输出- 这个输出又被
|接到wc -l的标准输入 wc -l数一下总共有多少行,最后打印到屏幕——因为这是链条的最后一环,没有再被重定向
类比一下:可以把它想象成工厂流水线,每个命令是一个工位,| 就是传送带,把上一个工位加工完的”半成品”直接送到下一个工位,不需要你手动搬运(比如先存成文件,再手动读进下一个程序)。
四、一个真实的陷阱:管道接的具体是哪一条?
上面说”管道把标准输出接到标准输入”——注意,是标准输出,不是”全部输出”。stderr 单独走它自己的路,管道压根不接它。拿一个会同时往两条流写内容的小脚本实测:
#!/bin/bashecho "这是 stdout 的内容"echo "这是 stderr 的内容" >&2只重定向 stdout,stderr 照样冒到终端:
$ ./noisy.sh > out.txt这是 stderr 的内容 ← 直接冒到屏幕,没被接管$ cat out.txt这是 stdout 的内容 ← 文件里只有这一条很多人第一次这么写,以为”重定向了就该啥都不打印”,结果发现终端还在冒东西——那是因为 stderr 根本不知道 > 这回事,继续老老实实往屏幕写。
管道 | 同理,只连接 stdout,不连接 stderr:
$ ./noisy.sh | cat这是 stderr 的内容 ← 直接漏到屏幕,没走管道这是 stdout 的内容 ← 这条才是真的走了 cat这是最常见的坑之一:很多人以为 | 会把程序的”全部输出”都接给下一个程序,其实管道只连接了 fd1(stdout)到下一个程序的 fd0(stdin),stderr 完全被晾在一边,原样冒到你的终端。所以有时候你写了 some_command | grep xxx,报错信息却还是直接甩在终端上、没被 grep 过滤掉——就是这个原因。
想让 stderr 也走管道,得先用 2>&1 把它并入 stdout:
$ ./noisy.sh 2>&1 | cat这是 stdout 的内容这是 stderr 的内容 ← 现在两条都乖乖走了 catfd 编号:0、1、2 到底是什么
标准输入/输出/错误还有官方编号,叫文件描述符(file descriptor,简称 fd):
0= 标准输入(stdin)1= 标准输出(stdout)2= 标准错误(stderr)
前面写的 >file 其实是 1>file 的简写——因为重定向默认操作的就是 fd 1(stdout)。2>file 就是显式指定操作 fd 2(stderr)。2>&1 读作”把 fd 2 重定向到 fd 1 当前指向的地方”。
这也回答了一个常见顾虑:管道会不会污染数据?
比如这行:
grep pattern huge_log.txt | sort | uniq -c你可能会担心:如果 grep 中途报了个警告(比如某行编码有问题),这条警告会不会混进 stdout,被 sort 当成”数据”处理,把统计结果搅乱?前面已经验证过答案——不会,因为管道天生只接 stdout,警告走的是 stderr,sort 根本看不到它。
那如果你确实想对 stderr 做点什么,具体想要什么效果,分三种情况:
-
情况 A:只是想让警告照常显示,统计结果也照常输出——什么都不用做,默认行为就是这样。
-
情况 B:想把 stderr 存下来,方便以后查:
Terminal window grep pattern huge_log.txt 2> errors.log | sort | uniq -c警告被存进
errors.log,统计结果照常走管道。 -
情况 C:想让 stderr 也流进管道,和 stdout 合并成一条(比较少见,调试或者想把所有输出存进单一日志文件时才用):
Terminal window grep pattern huge_log.txt 2>&1 | sort | uniq -c这时警告确实会被
sort当成数据处理——回到了最初担心的那个”污染”问题,所以这个写法通常只在故意想合并两条流时才用,比如command > all_output.log 2>&1。
为什么常常和 > 一起用
比如这样一行:
cargo test my_test > "$LOGFILE" 2>&1这行按从左到右执行:
> "$LOGFILE"—— 先把标准输出(1)指向$LOGFILE这个文件2>&1—— 再把标准错误(2)指向”标准输出现在指向的地方”,也就是同一个文件
结果就是:正常输出和报错信息全部一起写进同一个日志文件,事后看日志时能同时看到程序打印了什么、又报了什么错,顺序也是真实执行顺序,不会错乱。
顺序很重要:一个常见坑
command 2>&1 > file.txt # ❌ 不是你想要的command > file.txt 2>&1 # ✅ 这才对为什么反过来就错了?重定向是按从左到右顺序生效的,可以理解成”赋值”:
- 第一种写法:先执行
2>&1(此时 stdout 还指向屏幕,stderr 也被指向了屏幕),然后才> file.txt(只把 stdout 改指向文件)。结果 stdout 进了文件,stderr 还是打印在屏幕上——两者没有汇合。 - 第二种写法:先把 stdout 指向文件,这时 stderr 依然对着屏幕;接着
2>&1才把 stderr 也指向”stdout 现在指向的地方”,也就是文件。这样两者才真正汇合进同一个文件。
记忆技巧:把 2>&1 想象成”复制当前指向”,而不是”绑定关系”——它只是在那一刻把 2 的指向复制成了 1 当时的指向,之后 1 再变,2 也不会跟着变。
为什么这套设计这么好用
cat、grep、wc 这些工具,谁也不用为谁专门写兼容代码,却能随便组合——因为大多数命令行工具都遵循同一个约定:读 stdin、写 stdout。正是这个共同的约定,让工具之间可以自由拼接。这就是 Unix 哲学里”每个程序只做一件事,做好它,然后用管道组合”的核心。
一句话总结
管道能随便拼,靠的是”读 stdin、写 stdout”这一个共同约定;> >> < 改变的是数据的”去处”,| 改变的是”谁接谁的输出”。stdin/stdout/stderr 背后对应着 fd 0/1/2,> 就是 1> 的简写;管道接的具体是 stdout,不是全部——stderr 单独走自己的路,想让它也进管道得显式 2>&1,而且顺序不能反。