1.9 重定向与管道
预计阅读时间:14 分钟
📖 目录
前置知识
- 1.3:第一次进入命令行 命令行的基本结构(command、option、argument)与通配符展开
- 1.4:基本文件操作命令 基本文件操作(
ls、cat、sort等文本命令)与重定向的初步接触
学习目标
学完本章后,你将能够:
- 理解 stdin、stdout、stderr 三个标准数据流和文件描述符 0/1/2
- 用
>和>>将输出保存到文件,用2>把错误信息分离出来 - 用
<从文件读取输入,用 here-document(<<)提供内联输入 - 用
|管道把多个命令串联成数据处理流水线 - 用
tee同时保存输出到文件并在终端显示 - 用
xargs把命令输出转换成另一个命令的参数 - 用
$?、&&、||实现基于退出码的条件执行
知识关联
- 前置知识:1.4:基本文件操作命令 的文本处理命令(grep、sort、wc)——它们几乎是本章管道练习的固定搭档
- 后续影响:重定向、管道和
$?退出码是 2.1:Shell 脚本入门 Shell 脚本的基础构建块,脚本中的每一个命令都离不开本章概念 - 配套技术:1.6:文件查看与文本编辑 的文本工具(wc、sort、uniq、cut)常在管道末端做统计处理;2.10:正则表达式 正则表达式让 grep 过滤更强大
- 在整个体系中的位置:管道是命令行的"乘方"——从"一条命令做一件事"升级为"一条命令做一条流水线",这是 Linux 哲学"每个工具做好一件事,然后组合起来"的集中体现
核心知识
- 标准数据流(Standard Streams)——每个进程启动时自动打开的三个通道:stdin(0,输入)、stdout(1,输出)、stderr(2,错误)
- 输出重定向(> / >>)——把 stdout 写入文件(覆盖/追加),
2>重定向 stderr - 输入重定向(<)——从文件读取输入;here-document(
<<)提供多行内联输入 - 管道(|)——把前一个命令的 stdout 直接接到后一个命令的 stdin,命令之间用内存缓冲传递数据
- tee——同时把数据写入文件并继续输出到屏幕("T 型管道接头")
- xargs——把 stdin 的文本转成命令行参数,给不接受 stdin 的命令用
- 退出码(Exit Code)——命令结束时的整数状态:0=成功,非 0=失败;
echo $?查看 - 条件链(&& / ||)——根据前一条命令的退出码决定是否执行下一条
原理讲解
1. 三个标准数据流——一切皆文件的又一次体现
在 Linux 中,每个进程启动时操作系统都会为其打开三个标准数据流:
┌──────────┐
键盘 ════╣ stdin 0 ║
║ ║
║ 进程 ║════ stdout 1 ════ 屏幕
║ ║
║ stderr 2 ║═══════════════ 屏幕(独立通道)
└──────────┘
这三者都是"文件"(在 Unix 哲学下,"一切皆文件"意味着键盘、屏幕、打印机都是文件)。每个数据流都有一个文件描述符编号(0、1、2),Shell 通过它们操纵数据流向。
| 名称 | 文件描述符 | 默认目标 | 作用 |
|---|---|---|---|
| stdin | 0 | 键盘 | 程序读取输入的地方 |
| stdout | 1 | 屏幕 | 程序输出正常结果的地方 |
| stderr | 2 | 屏幕 | 程序输出错误信息的地方(与 stdout 分开) |
grep -r "pattern" / 2>/dev/null 隐藏"权限不足"的刷屏,只看到搜索结果。或者 command 2>error.log 把错误单独保存。2. 输出重定向——将结果保存到文件
# 覆盖写入(>)
echo "Hello World" > greeting.txt # 写入
echo "Second line" > greeting.txt # 覆盖!文件现在只有 "Second line"
# 追加写入(>>)
echo "Hello World" > greeting.txt # 先写
echo "Second line" >> greeting.txt # 追加
cat greeting.txt # 两行都在
# 重定向 stderr(2>)
ls /root 2> error.log # 错误信息写入文件,屏幕看不到
ls /root 2>> error.log # 追加到错误日志
# 合并 stdout 和 stderr(&> 或 2>&1)
command > output.log 2>&1 # 完整写法:stdout 到文件,stderr 重定向到 stdout
command &> output.log # 简写:两个流都写入文件
# 丢弃输出(/dev/null——黑洞文件)
command > /dev/null # 只丢弃 stdout
command &> /dev/null # 丢弃所有输出
command 2>/dev/null # 只丢弃 stderr(保留正常结果)
# 输入重定向(<)
sort < unsorted.txt # 从文件读取输入
sort < unsorted.txt > sorted.txt # 输入和输出都重定向
# Here Document(<<)——内联多行输入
cat << END > config.txt
server {
listen 80;
server_name example.com;
}
END
# Here String(<<<)——单行字符串输入
grep "error" <<< "no errors found" # 无匹配
grep "error" <<< "fatal error occurred" # 匹配!
# 实际用途:在脚本中提供内联输入
bc <<< "2+3" # 5
bc <<< "scale=2; 10/3" # 3.33
3. 管道——命令行的灵魂
管道(|)是 Linux 命令行最强大的特性。它将前一个命令的 stdout 直接连接到后一个命令的 stdin:
# 基本管道
ps aux | grep nginx # 列出进程 → 过滤出 nginx
history | grep "git" # 历史命令 → 只找 git 相关
ls -l /usr/bin | wc -l # 文件列表 → 统计行数
dmesg | grep -i error | tail -20 # 内核日志 → 过滤错误 → 只看最后 20 行
# 多个管道串联
cat /var/log/syslog | grep "ERROR" | awk '{print $5}' | sort | uniq -c | sort -rn | head -10
Shell 执行 cmd1 | cmd2 时,底层发生了一系列系统调用:① 内核创建一个 pipe 对象,包含两个文件描述符(读端和写端);② fork() 创建 cmd1 和 cmd2 两个子进程;③ cmd1 的 stdout(FD 1)被 dup2() 重定向到管道写端,cmd2 的 stdin(FD 0)被重定向到管道读端;④ 两个进程同时运行,cmd1 写满管道缓冲区时被阻塞,cmd2 读空时也被阻塞——这就是天然的"背压"(backpressure)机制,保证不会丢失数据。管道缓冲区默认 64KB(可通过 /proc/sys/fs/pipe-max-size 调整),所有数据流经内核空间,无需磁盘参与。
# 常见的管道用法
# 组合 1:过滤 + 分页
grep -r "TODO" ~/project/ | less
# 组合 2:计数
ps aux | grep -c "bash" # 统计 bash 进程数
# 组合 3:提取列 + 排序 + 去重
awk '{print $1}' access.log | sort | uniq -c | sort -rn
# 组合 4:和重定向结合
grep "ERROR" log.txt | head -50 > first50errors.txt
4. tee——同时输出到文件和屏幕
tee(取名自"T 型管道接头")同时将内容输出到文件和 stdout:
# tee:保存到文件同时继续在终端显示
dmesg | tee dmesg.log # 显示内核日志并保存
dmesg | tee -a dmesg.log # -a 追加模式
# 实际用途:记录命令输出以供后续分析
./long_running_task | tee task.log # 在屏幕上看到进度,同时保存
# 进阶:tee 到多个文件
echo "data" | tee file1.txt file2.txt file3.txt
# 进阶:tee + sudo(写入需要 root 权限的文件)
echo "some config" | sudo tee /etc/nginx/sites-available/default > /dev/null
# 这里 sudo tee 以 root 身份写入,> /dev/null 避免在屏幕上重复输出一次
5. xargs——将 stdout 转为命令参数
管道传递的是文本流(stdin),但有的命令不接受 stdin 输入,只接受命令行参数。xargs 解决这个问题:
# 基本用法:find 找到文件 → xargs 传给 rm
find /tmp -name "*.tmp" -type f | xargs rm
# -p:交互确认(安全)
find . -name "*.log" -type f | xargs -p rm
# -n:每次传递几个参数
echo "a b c d e" | xargs -n 2 # 每行两个参数
# -I:替换字符串(处理带空格的文件名)
find . -name "*.txt" -print0 | xargs -0 -I {} mv {} ~/backup/
# -print0 和 -0 使用 null 字符分隔(安全处理文件名中的空格)
# 实际例子
# 列出所有 Python 文件的信息
find ~/project -name "*.py" | xargs wc -l
# 并行下载 URL 列表
cat urls.txt | xargs -P 4 -n 1 wget # -P 4 = 同时 4 个进程
6. 退出状态码和条件执行——$?、&&、||
每个命令执行完毕都会返回一个整数退出码给 Shell:0 表示成功,非 0 表示各种失败。
# 查看退出码
true; echo $? # 0
false; echo $? # 1 (失败通用)
ls /nonexistent; echo $? # 2 (文件不存在)
grep "pattern" file.txt; echo $? # 0=找到了,1=没找到,2=文件出错
条件链:
# &&:前一条成功(退出码 0)才执行下一条
mkdir newdir && cd newdir && touch file.txt
# 相当于:如果 mkdir 成功,才 cd;如果 cd 成功,才 touch
# ||:前一条失败(退出码非 0)才执行下一条
command1 || command2 # 如果 command1 失败,执行 command2
# 组合使用
command1 && echo "success" || echo "failed"
# 注意:这个组合可能因为 echo "success" 的退出码 0 而导致 ||
# 总是执行。更安全的写法:
if command1; then echo "success"; else echo "failed"; fi
# 实际应用
ping -c 1 8.8.8.8 &> /dev/null && echo "Network OK" || echo "Network DOWN"
apt update && apt upgrade -y || { echo "Update failed!"; exit 1; }
示例代码
# 练习 1:重定向输出
echo "line 1" > test.txt
echo "line 2" >> test.txt
echo "line 3" >> test.txt
cat test.txt
# 练习 2:重定向错误
ls /root 2> /dev/null # 隐藏权限错误
ls /root /etc 2>/dev/null # 仍会显示 /etc 的内容
# 练习 3:管道基础
ls -l /usr/bin | head -20 # 前 20 个可执行文件
ls -l /usr/bin | grep "python" # Python 相关文件
ps aux | awk '{print $2, $11}' | head # PID 和命令名
# 练习 4:管道组合统计
# 统计 /etc 下各文件类型的数量
ls -l /etc | awk '{print substr($1,1,1)}' | sort | uniq -c | sort -rn
# 统计 Shell 类型分布
awk -F: '{print $7}' /etc/passwd | sort | uniq -c | sort -rn
# 练习 5:tee 保存实时输出
# 先观察
ls -l /usr/bin | tee usr-bin-list.txt | wc -l
# 然后查看 usr-bin-list.txt
# 练习 6:退出码和条件链
# 测试网络连接
ping -c 1 8.8.8.8 &> /dev/null && echo "Internet: OK" || echo "Internet: DOWN"
# 练习 7:xargs
# 用 find + xargs 统计所有 .conf 文件的行数
find /etc -name "*.conf" -type f 2>/dev/null | xargs wc -l 2>/dev/null | tail -5
真实踩坑案例
# 错误写法:stderr 仍然漏到屏幕上
command 2>&1 > output.log
# 正确写法:先重定向 stdout,再让 stderr 跟随
command > output.log 2>&1
根因:Shell 从左到右处理重定向。2>&1 的含义是"让 stderr 指向 stdout 当前指向的位置"——在它执行时 stdout 还指向屏幕,所以 stderr 被绑定到了屏幕;后面的 > output.log 只改变 stdout。排查思路:写合并重定向时永远把 > file 写在前面。
# 危险写法:My Notes.txt 会被拆成两个参数
find . -name "*.txt" | xargs rm
# 安全写法:null 字符分隔
find . -name "*.txt" -print0 | xargs -0 rm
根因:xargs 默认按空白字符切分输入,含空格的文件名会被拆碎。排查思路:动手前先 find ... | xargs echo 预览将被处理的参数;删除类操作养成 -print0 | xargs -0 或 find ... -delete 的习惯。
常见错误
| 错误/误区 | 解决方案 |
|---|---|
| "cat file > file 得到空文件" | 重定向操作符 > 先清空文件再执行命令。当输入和输出是同一文件时,先"清空"导致 cat 读到了空文件。总是写 cat file > tmp && mv tmp file 或使用 sed -i。 |
| "混淆了 2>&1 和 2>1" | 2>&1 将 stderr 重定向到 stdout(& 表示"文件描述符")。2>1 将 stderr 重定向到一个名为 1 的文件。正确的合并写法:command > out.txt 2>&1。 |
| "在管道中忘记某些命令会缓冲输出" | grep、sed、awk 默认有输出缓冲(特别是处理大文件时)。用 --line-buffered(grep)或在 awk 中加 fflush() 来强制实时输出。 |
| "find ... | rm 不工作" | rm 不接受 stdin 输入。需要使用 xargs:find ... | xargs rm。或者直接用 find ... -delete。 |
| "文件名的空格导致 xargs 出问题" | 使用 find -print0 + xargs -0 以 null 字符分隔文件名。或者用 find ... -exec rm {} +。 |
| "把 > 和 | 的传递方向记反" | 记住箭头指向数据流动方向:> file = 数据流向文件(→),< file = 数据从文件来(←),cmd1 | cmd2 = cmd1 流到 cmd2。 |
最佳实践
| # | 建议 | 说明 |
|---|---|---|
| 1 | 把管道当作"数据流水线"来思考 | 每一段管道完成一个简单的筛选/转换步骤。培养"先过滤→再处理→最后输出"的思维习惯。 |
| 2 | 逐段构建长管道 | 先跑第一部分:cat log.txt | grep ERROR;确认正确后再加:cat log.txt | grep ERROR | awk '{print $5}';逐步构建,避免一次写太长出错难以排查。 |
| 3 | 用 /dev/null 丢弃不需要的输出 | command 2>/dev/null 隐藏错误消息保持终端整洁。command &>/dev/null 完全静默执行。 |
| 4 | 危险命令前先用 echo 测试 | 准备用 xargs rm 之前,先 find ... | xargs echo 看哪些文件会被删除。确认无误后再替换 echo 为 rm。 |
| 5 | 用 tee 记录长时间运行的命令输出 | ./backup_script.sh | tee backup.log 让你看到实时进度,同时保存完整日志供事后分析。 |
| 6 | 用 ${PIPESTATUS[@]} 检查管道各段的退出码 | cmd1 | cmd2 之后,echo ${PIPESTATUS[@]} 输出各段命令的退出码数组——排查"管道整体成功了但结果不对"时很有用。 |
练习题
- (概念)stdout 和 stderr 分离的意义是什么?请举一个实际使用场景。
- (概念)管道(
|)和重定向(>)的区别是什么?什么时候用哪个? - (实操)用一条命令完成:查找
/var/log下所有.log文件,统计它们的总行数。提示:find+xargs wc -l。 - (实操)用一条命令完成:列出当前目录下最大的 5 个文件(提示:
ls -lS或du+sort+head)。 - (实操)编写一条命令组合:
ps aux的输出中,找到 CPU 占用率(第 3 列)最高的前 5 个进程,只显示 PID、CPU%、命令名三列。 - (探究 🔍)写一个管道组合,处理 Nginx 访问日志(或模拟日志)。要求:① 只保留 404 的请求 ② 提取请求的 URL ③ 按 URL 统计次数 ④ 按次数从大到小排序 ⑤ 只显示前 10 个。
- (探究 🔍)研究
cat file | grep pattern(UUOC——Useless Use of Cat)和grep pattern file的区别。为什么说前者是"无用的 cat"?在什么情况下cat | grep确实比直接grep file更好?
点击查看答案
- (概念)stdout 和 stderr 分离的意义在于可以分别处理正常输出和错误信息。例如:`find / -name "*.conf" 2>/dev/null` 忽略权限错误仅显示搜索结果。生产脚本中典型用法:stdout 写入日志,stderr 触发警报。
- (概念)管道(`|`)将前一个命令的 stdout 连接到后一个命令的 stdin(链式处理),重定向(`>`)将输出写入文件(数据持久化)。管道用于"加工"数据:`ps aux | grep nginx`;重定向用于"保存"数据:`ps aux > ps.txt`。
- (实操)`find /var/log -name "*.log" -exec wc -l {} + 2>/dev/null | tail -1` 或 `find /var/log -name "*.log" | xargs wc -l 2>/dev/null | tail -1`。注意权限不足的错误被 2>/dev/null 丢弃。
- (实操)`ls -lhS | head -6`(-S 按大小降序)或 `du -sh * | sort -rh | head -5`(du 方式递归子目录)。du 更适合统计目录总大小,ls 适合单文件排序。
- (实操)`ps aux --sort=-%cpu | awk 'NR<=6{print $2,$3,$11}'`。ps 第 2 列 PID、第 3 列 %CPU、第 11 列 COMMAND。`--sort=-%cpu` 降序排列。`NR<=6` 保留标题行 + 前 5 个进程。
- (探究 🔍)完整管道:`grep " 404 " access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -10`。逐步练习每个环节的输出变化:先看 grep 过滤结果,再加 awk 提取 URL,再加 sort 排序...逐步调试每个阶段。
- (探究 🔍)`cat file | grep pattern` 是 Useless Use of Cat(UUOC)——`grep pattern file` 直接由 grep 打开文件更高效。但需要合并多个文件输入时 cat 是合理的:`cat file1 file2 | grep pattern`。原则:能直接读文件就不 cat 中转。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能解释 stdout 与 stderr 分离的意义、管道与重定向的区别 | 尝试向他人讲解 |
| 命令操作 | 能不查文档用管道 + 重定向组合命令完成任务 | 在终端实际执行 |
| 原理掌握 | 能说出管道底层的实现机制(pipe 缓冲区、fork/dup2、背压) | 画出管道数据流图 |
| 故障排查 | 能排查 2>&1 顺序错误、xargs 空格误拆等常见问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么删除类命令要用 -print0 | xargs -0 而非普通管道 | 对比两种写法 |
本章总结
重定向和管道让命令行从"单兵作战"变成"流水线协作"。理解三个标准数据流(stdin/stdout/stderr)是理解一切组合命令的基石:> 保存输出、< 提供输入、| 传递数据、tee 一鱼两吃、xargs 打通"不读 stdin 的命令"、$? 和条件链让命令具备简单的判断能力。这一章的工具你每天都会用到,也是下一章 Shell 脚本的基础。
速查表
| 概念 | 一句话定义 |
|---|---|
| stdin / stdout / stderr | 进程的三个标准数据流:输入、输出、错误(文件描述符 0/1/2) |
> | stdout 重定向到文件(覆盖) |
>> | stdout 重定向到文件(追加) |
2> | stderr 重定向到文件 |
2>&1 | stderr 合并到 stdout(注意顺序:先 > file 再 2>&1) |
/dev/null | 黑洞设备,丢弃所有写入的数据 |
< / << / <<< | 文件 / here-document / here-string 输入 |
| | 管道:前一个命令的 stdout 接后一个命令的 stdin |
| tee | 同时输出到文件和屏幕 |
| xargs | 把 stdin 文本转成命令行参数(-0 配 -print0 处理空格) |
| $? | 上一条命令的退出码(0=成功) |
&& / || | 成功才执行 / 失败才执行 |
下一步:进入 2.1:Shell 脚本入门「Shell 脚本入门」,把本章的命令组合能力升级为可复用、可调度的自动化脚本。
延伸阅读
- Bash 官方手册:Redirections——重定向的完整语法,包括
>>、<&、here-document 的所有细节 - Bash 官方手册:Pipelines——管道语法与 PIPESTATUS 的官方说明
- 命令:
ls -l /proc/self/fd——查看当前进程打开的所有文件描述符,直观理解 0/1/2 - 命令:
cat -A file——显示文件中的隐藏特殊字符(行尾$、Tab 为^I),排查文本处理问题常用