1. 为什么需要理解流与管道
前几篇学了很多独立命令: ls, cat, find, chmod... 每个命令都能独立完成一件事. 但 Unix 的真正威力不在于单个命令有多强大,而在于把多个小命令组合起来,像搭乐高一样.
想象一个加工流水线: 第一台机器把原材料切割成形,传给第二台打磨,再传给第三台喷漆.
每台机器只做一件事,但连起来就能生产出复杂产品.
管道(pipe)和重定向(redirection)就是这条流水线上的传送带--它们决定数据如何在不同命令之间流转.
掌握流与管道之后,才真正开始 "用 Unix 的方式思考". 这也是接下来 grep / sed / awk 三篇的基础--这三个文本处理工具离开了管道就失去了大半武功.
2. 三个标准流: stdin / stdout / stderr
每个进程(包括 Shell 中运行的每个命令)启动时,操作系统会自动给它 "接通" 三条数据通道:
|
| 名称 | 全称 | 文件描述符 | 默认流向 | 用途 |
|---|---|---|---|---|
| stdin | standard input | 0 | 键盘 | 程序读取输入数据的来源 |
| stdout | standard output | 1 | 屏幕 | 程序输出正常结果的地方 |
| stderr | standard error | 2 | 屏幕 | 程序输出错误/诊断信息的地方 |
2.1 文件描述符(File Descriptor)
0, 1, 2 这三个数字就是文件描述符--操作系统用整数来追踪每个进程打开了哪些文件(在 Unix 中,"一切皆文件",键盘和屏幕也是 "文件"). 0/1/2 这三个号是每个进程的 "标配",由父进程(通常是 Shell)在 fork 时预先设置好.
为什么 stdout 和 stderr 要分开?
想象运行一个命令,正常输出想保存到文件,但错误信息想在屏幕上看到. 如果它们混在同一个通道,就无法分开处理. stderr 的存在让我们可以分别控制 "结果" 和 "诊断" 的去向--这是 Unix 设计中的精妙之处.
2.2 用最简单的例子验证
|
看起来都是 "打印到屏幕",但它们是两条不同的通道. 接下来学习如何分别控制它们的去向.
3. 输出重定向
重定向的本质是把某个流的默认目标(屏幕)替换成文件.
3.1 覆盖写入: >
|
> 的完整写法是 1>(1 = stdout 的文件描述符),Shell 允许省略这个 1.
3.2 追加写入: >>
|
>> 追加到文件末尾,不会覆盖已有内容. 完整写法是 1>>.
3.3 防覆盖保护: set -o noclobber
|
建议在 .zshrc 中加上 set -o noclobber,防止误操作覆盖重要文件.
4. 错误重定向
stderr 的文件描述符是 2,所以错误重定向以 2 开头.
4.1 2>: 让错误单独走
|
标准输出去了 output.txt,错误去了 error.log--互不干扰.
4.2 2>&1: 合并到标准输出
2>&1 的含义是 "让文件描述符 2(stderr)指向文件描述符 1(stdout)当前指向的地方". 这是最经典的合并写法:
|
顺序很重要--2>&1 必须在 > 之后
ls > file 2>&1 -- 正确: 先把 stdout 指向 file,再把 stderr 指向 stdout(此时 stdout 已经指向 file 了).
ls 2>&1 > file -- 错误: 先把 stderr 指向 stdout(此时 stdout 还指向屏幕),再把 stdout 指向 file--结果是 stderr 打印到屏幕,stdout 写到 file,相当于没合并.
为什么不是引用语义?--理解 dup2 的拷贝机制
直觉上可能认为 2>&1 是把 fd2 设成一个 "指向 fd1 的引用"--以后 fd1 变,fd2 也自动跟着变. 但实际并非如此.
重定向的本质是 dup2 系统调用,语义是值拷贝(快照)而非引用绑定:
|
这意味着 2>&1 所做的就是 "把 fd1 此刻指向的目标拷贝给 fd2",之后 fd1 再怎么变都与 fd2 无关. 因此:
|
一句话概括: 2>&1 不是 "以后跟着 fd1 走",而是 "复制 fd1 此刻的目标". 时机不同,复制到的就是不同的值.
4.3 &>: 现代 Shell 的快捷方式
|
&> 比 2>&1 更简洁,但注意它是 bash/zsh 的扩展语法,POSIX sh 不支持. 写脚本时如果需要兼容性,用 > file 2>&1.
5. 输入重定向
5.1 <: 从文件读取
默认情况下命令从键盘读取输入(stdin = 键盘). < 让命令从文件读取:
|
重定向输入 vs 文件名参数--很多命令同时支持两种方式,但行为略有不同. 上面的 wc 例子中,< 版本只输出数字(wc 不知道数据来自哪个文件),而传文件名时 wc 会带上文件名.
5.2 <<: Here Document(内嵌文本)
Here Document 让在命令行中直接写多行文本,以一个自定义的结束标记(通常是 EOF)结尾:
|
最常用的场景: 往配置文件追加一段多行内容,或者在脚本中给命令提供输入:
|
带引号的 EOF vs 不带引号的 EOF
|
如果文本内容中含 $ 或反引号且不想被 Shell 解释,记得给 EOF 加引号.
6. 管道 |: Unix 的核心发明
6.1 直觉理解
管道 | 把左边命令的 stdout 直接连到右边命令的 stdin,中间不经过磁盘,没有临时文件:
|
ls -l 的输出没有打印到屏幕,而是直接被送进了 grep 的输入. grep 从中筛选出含 "html" 的行再打印.
管道就像工厂流水线--前一台机器的出料口直接对接后一台机器的进料口.
数据不是整批搬运的,而是边生产边消费: ls 写出一行,grep 就立即处理一行.
这就是流式处理(streaming)--不积攒,不停顿.
6.2 管道 vs 重定向
管道 | |
重定向 > / < |
|
|---|---|---|
| 连接对象 | 命令 ↔ 命令 | 命令 ↔ 文件 |
| 数据形态 | 流式的,不落盘 | 持久的,写入磁盘 |
| 典型用法 | cmd1 | cmd2 | cmd3 |
cmd > file 或 cmd < file |
| 比喻 | 传送带 | 仓库入库/出库 |
6.3 管道的局限: stderr 不走管道
管道只传递 stdout. stderr 仍然打印到屏幕:
|
如果需要 stderr 也进管道,先用 2>&1 合并:
|
7. 组合实战
管道和重定向可以灵活组合. 以下是几个常用模式:
7.1 管道 + 重定向
|
7.2 多级管道
|
7.3 过滤与统计范式
|
Unix 哲学: 小工具组合干大事
上面的最后一个例子用了 6 个命令串联,每个命令只做一件小事--切列, 排序, 去重, 取前 N. 但连在一起就能完成 "找出访问量最高的 10 个 IP" 这样复杂的分析.
这就是 Unix 哲学的精髓: 写一个能做所有事的程序很难,但把多个只做一件事的小工具串起来就容易得多.
8. tee: T 型分流器
管道把数据从 A 传到 B,数据就 "消失" 在管道里了--没法同时看到中间结果. tee 解决的就是这个问题: 一边把数据传下去,一边写入文件(或打印到屏幕).
|
|
典型场景: 运行一条耗时很长的命令(比如编译),想同时在屏幕上看到进度,又把完整日志保存下来排查问题--make 2>&1 | tee build.log.
9. xargs: stdin → 命令行参数
这是一个重要的概念转折: 管道传递的是 stdin 数据流,不是命令行参数. 有些命令不接受 stdin,只接受参数--这时就需要 xargs 来架桥.
|
|
xargs 的文件名安全
文件名中的空格会导致传统 rm $(find ...) 出错: My Document.txt 会被当成两个文件. 解决方法是 find -print0 | xargs -0:
|
文件名不可能含 null 字符,所以这是完美的分隔方案.
什么时候需要 xargs? 判断标准很简单: 要传给目标命令的数据是参数(如文件名列表)还是内容(如要搜索/处理的文本)? 参数用 xargs,内容直接用管道.
| 场景 | 写法 | 说明 |
|---|---|---|
| 内容处理 | cat file | grep "error" |
grep 从 stdin 读文本内容 |
| 参数传递 | find . -name "*.log" | xargs rm |
rm 需要文件名作参数 |
| 参数替换到中间 | ... | xargs -I {} mv {} dest/{} |
{} 占位 |
10. /dev/null: 终极黑洞
/dev/null 是一个特殊的设备文件--写入它的数据会被内核直接丢弃,读取它则立即返回 EOF. 本质上是一个 "黑洞".
|
常见用途: cron 定时任务中在命令末尾加 > /dev/null 2>&1 可以防止输出触发邮件通知. 或者只关心命令的退出码($?),不在乎它打印了什么.
复习: 为什么是 > /dev/null 2>&1 而不是反过来?
可能直觉觉得应该 "先把两条流绑在一起(2>&1),再一起搬进黑洞(> /dev/null)"--听起来很合理,但这正是 4.2 节讲过的拷贝语义陷阱.
2>&1 不是 "以后跟着 fd1 走" 的引用绑定,而是 "复制 fd1 此刻目标的快照". 因此:
|
记住: 先让 fd1 到位,再让 fd2 效仿. 这和 4.2 节 > file 2>&1 的顺序是完全一致的道理.
快速回顾
- 每个进程有三个标准流: stdin(0) 读入, stdout(1) 输出正常结果, stderr(2) 输出错误信息
- 重定向改变流的默认去向:
>覆盖写,>>追加写,<从文件读 - 错误重定向:
2>单独控制 stderr,2>&1合并到 stdout,&>一次性全重定向 - 管道
|把左边命令的 stdout 连到右边命令的 stdin,实现流式传递,不落盘 - 管道只传 stdout--stderr 需要
2>&1合并后才能进管道 - tee 在管道中插入一个 "分支",一边往下传数据,一边写入文件
- xargs 把 stdin 数据流转成命令行参数--当目标命令不接受 stdin 时用它架桥
- /dev/null 是黑洞--写入即消失,用来抑制不关心的输出
- Unix 哲学: 小命令通过管道组合,完成远超单个命令能力的复杂任务
动手练习
- 分离 stdout 和 stderr: 运行
ls / /nonexistent > out.txt 2> err.txt,检查两个文件的内容--确认 stdout 和 stderr 是否被正确分开了 - 覆盖 vs 追加: 创建一个文件,用
echo "hi" > test.txt,然后echo "bye" >> test.txt,cat test.txt验证追加 vs 覆盖的区别 - 管道数据流: 运行
ls -la / | wc -l,理解数据如何从 ls 流向 wc - 多级管道: 用多级管道找出当前目录下占用空间最大的 3 个文件或目录:
du -sh * 2>/dev/null | sort -hr | head -3 - tee 分流: 用
tee运行ls -l | tee listing.txt | grep "^d",验证 listing.txt 中保存了完整输出,而屏幕只显示目录行 - xargs 处理空格: 创建几个带空格名字的文件:
touch "my file.txt" "your file.txt",然后用find . -name "*.txt" -print0 | xargs -0 ls -l列出它们 - 重定向与 /dev/null: 运行一条耗时命令(如
sleep 3; echo done),用time包裹,同时把 stderr 重定向到/dev/null:(time sleep 3) 2>/dev/null