路线图

管道与重定向

星辉 2026-07-02 阅读 6 min 1,182 字 路线图
管道与重定向 封面

Linux 运维基础 · 第八章 目标:理解 Linux I/O 模型,掌握管道串联和重定向的完整用法


1. 概述#

Linux 的哲学是"小工具组合完成大任务"。管道(|)和重定向(> / >> / <)正是实现这种组合的核心机制。

如果说单个命令是"零件",管道就是把零件串成流水线的传送带,重定向则决定了数据从哪来、到哪去。


2. stdin / stdout / stderr#

2.1 三个标准流#

Linux 下每个进程启动时,系统自动打开三个文件描述符:

文件描述符名称缩写含义默认
0stdin标准输入进程读取数据的来源键盘输入
1stdout标准输出进程正常输出的去处终端屏幕
2stderr标准错误进程错误输出的去处终端屏幕(和 stdout 分开)

2.2 为什么要分 stdout 和 stderr#

bash
# 假设一个命令输出正常结果 + 警告
./process.sh
# Processing file1... Done
# Processing file2... Done
# WARNING: skipping file3 (permission denied)   ← 这是 stderr
# Processing file4... Done

# 如果 stdout 和 stderr 混在一起:
# - 想把输出结果重定向到文件保存 → 警告也进去了,污染数据
# - 想把警告单独收集 → 需要分开处理

# 结论:stdout 是"产出",stderr 是"诊断",分开处理才合理

2.3 查看区分#

bash
# 正常情况下,两者都打印到终端
ls /tmp /nonexist
# /tmp:            ← stdout
# file1 file2
# ls: /nonexist: No such file or directory   ← stderr

# 把 stdout 重定向后,stderr 仍然显示在终端
ls /tmp /nonexist > /dev/null
# ls: /nonexist: No such file or directory   ← 只有 stderr 了

3. 重定向#

3.1 输出重定向#

stdout 重定向#

bash
# > 覆盖写入
ls -la > file_list.txt                  # 覆盖(文件不存在则创建)
echo "new line" > file.txt              # 每次执行都覆盖原有内容

# >> 追加写入
echo "another line" >> file.txt          # 追加到文件末尾

stderr 重定向#

bash
# 2> 只重定向 stderr
./script.sh 2> error.log               # stderr 进文件,stdout 照常显示

# 只保存错误信息,忽略正常输出
./script.sh 2> error.log 1>/dev/null    # stdout 丢弃,stderr 保存

合并重定向#

bash
# 把 stderr 合并到 stdout
./script.sh > output.log 2>&1           # stdout 和 stderr 都进 output.log
# 解释:2>&1 意思是"把文件描述符 2 重定向到文件描述符 1 当前指向的地方"

# 全部重定向(bash 简写)
./script.sh &> all_output.log           # bash 专有简写(非 POSIX sh;追加形式 &>> 才需 bash 4.0+)
./script.sh >& all_output.log           # 另一种写法

# 追加全部
./script.sh &>> all_output.log
./script.sh >> all_output.log 2>&1      # 等价

⚠️ 顺序陷阱:2>&1 必须放在 > file 之后#

重定向是从左到右按顺序生效的,2>&1 复制的是"此刻 fd 1 指向哪",不是"最终指向哪"。

bash
# ✅ 对:先把 stdout 指向文件,再让 stderr 跟着 stdout(一起进文件)
./script.sh > out.log 2>&1

# ❌ 错:stderr 先复制了“此刻的 stdout(还是终端)”,之后 stdout 才改指向文件
./script.sh 2>&1 > out.log
#   结果:stderr 打到终端,只有 stdout 进了文件 —— 常见踩坑,日志里死活看不到报错

记忆:> file 在前,2>&1 在后;顺序反了就等于没合并。

3.2 常用的"静默"写法#

bash
# 丢弃 stdout 和 stderr
./script.sh > /dev/null 2>&1
./script.sh &> /dev/null                # 等效简写

# cron 任务常用写法:只输出错误
./script.sh > /dev/null                  # 成功不输出,失败才报

# 检查命令是否成功(不关心输出)
if grep -q "pattern" file.txt 2>/dev/null; then
    echo "Found"
fi
# -q = quiet,配合 2>/dev/null 抑制一切输出

3.3 输入重定向#

bash
# < 从文件读取输入
mysql < schema.sql                       # 把 .sql 文件作为 mysql 的输入

# <<  Here Document(脚本中嵌入多行文本)
cat << EOF > config.yaml
server:
  host: 0.0.0.0
  port: 8080
EOF

# <<< Here String(单行输入)
grep "error" <<< "This is an error message"

4. 管道 |#

4.1 管道的本质#

管道把前一个命令的 stdout 接到后一个命令的 stdin

bash
command1 | command2 | command3
#         ↑ stdout → stdin      ↑ stdout → stdin

# 注意:管道只传递 stdout,stderr 不经过管道!
# 所以下面这样是不行的:
cat error.log | grep "error"      # 即使 stderr 有内容也不经过管道
# 正确做法:把 stderr 重定向到 stdout 再管道
./script.sh 2>&1 | grep "error"

4.2 经典管道组合#

bash
# 统计文件数
ls | wc -l

# 找进程
ps aux | grep nginx

# 查看端口监听
ss -tlnp | grep 8080

# 找大文件
du -sh /* 2>/dev/null | sort -rh | head -10

# 找 Top10 访问 IP(运维经典)
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

# 看磁盘使用率(排除 tmpfs)
df -h | grep -v tmpfs

# 实时监控特定服务日志
tail -f /var/log/syslog | grep -i "error\|timeout"

4.3 管道的退出码陷阱(pipefail#

一条管道的退出码,默认只取最后一个命令的退出码——中间命令失败会被"吃掉",脚本里极易漏判错误。

bash
# 前一个命令失败,但整条管道仍然“成功”(因为 sort 成功了)
false | sort
echo $?          # 0 —— 明明 false 失败了!

# 真实场景:curl 挂了,但因为 grep 成功,脚本以为一切正常
curl http://api/xxx | grep "ok"
echo $?          # 只反映 grep,curl 的失败被掩盖

两种解法:

bash
# 解法 1:set -o pipefail —— 管道里任一命令失败,整条就算失败(脚本首选)
set -o pipefail
false | sort; echo $?      # 1 —— 现在能感知到了
# 生产脚本常见开头:set -euo pipefail

# 解法 2:查 $PIPESTATUS 数组,逐段看每个命令的退出码(bash)
false | sort | cat
echo "${PIPESTATUS[@]}"    # 1 0 0 —— 第一个命令返回 1

set -e(遇错即停)单独用时,管道中间命令的失败不会触发退出,必须配 pipefail 才完整。


5. tee — 同时输出到文件和屏幕#

5.1 为什么需要 tee#

管道中,数据是"单向流动"的。你无法在管道的中间节点看到数据。tee 在中间的分支上"偷看一眼"。

bash
# 不使用 tee:输出要么进文件,要么显示在屏幕,不能同时
./script.sh > output.log                # 只进文件,屏幕没输出

# 使用 tee:既看输出又保存
./script.sh | tee output.log            # 屏幕有输出,同时写入文件

# tee -a 追加
./script.sh | tee -a output.log         # 追加不覆盖

5.2 典型场景#

bash
# 场景 1:运行长时间脚本,想看着进度又怕 SSH 断线后丢输出
./long_running.sh 2>&1 | tee build.log

# 场景 2:调试管道——在中间看看数据长什么样
cat access.log | awk '{print $1}' | tee ips.txt | sort | uniq -c | sort -rn | head
#                 中间把 IP 也保存到 ips.txt

# 场景 3:同时输出到多个文件
./script.sh | tee file1.log file2.log file3.log

6. xargs — 把管道输出变成命令参数#

6.1 什么时候需要 xargs#

有些命令不接受管道输入(参数 ≠ stdin)。比如 rmcpmkdirechokill

bash
# 错误:rm 不接受 stdin
echo "old_file.txt" | rm
# rm: missing operand       ← rm 等的是参数,不是 stdin

# 正确:用 xargs 把 stdin 转换成命令行参数
echo "old_file.txt" | xargs rm

6.2 核心用法#

bash
# 查找并删除
find . -name "*.tmp" | xargs rm -f

# 查找并查看文件大小
find . -name "*.log" | xargs ls -lh

# 查找并打包
find . -name "*.conf" | xargs tar czf configs.tar.gz

# 批量创建目录
echo "dir1 dir2 dir3" | xargs mkdir -p

# 批量 kill 进程
ps aux | grep "sleep" | grep -v grep | awk '{print $2}' | xargs kill

6.3 处理特殊文件名(空格、换行)#

bash
# 隐患:文件名含空格会导致 xargs 误判
touch "my file.txt"
find . -name "my file.txt" | xargs rm
# rm: cannot remove 'my': No such file or directory
# rm: cannot remove 'file.txt': No such file or directory

# 解法 1:find 的 -print0 + xargs -0(推荐)
find . -name "*.tmp" -print0 | xargs -0 rm -f

# 解法 2:find ... -exec(更安全)
find . -name "*.tmp" -exec rm -f {} \;

6.4 xargs 高级参数#

bash
# -n:每次传递 N 个参数
echo "1 2 3 4 5 6" | xargs -n 2 echo
# 1 2
# 3 4
# 5 6

# -I:自定义占位符
find . -name "*.conf" | xargs -I {} cp {} /backup/{}
# 把 {} 替换成找到的文件名

# -p:执行前确认(安全)
find . -name "*.tmp" | xargs -p rm -f
# rm -f ./a.tmp ./b.tmp?... → 按 y 确认,n 取消

7. 实战场景#

实战 1:脚本输出全部进日志#

bash
#!/bin/bash
# 所有输出(含错误)都写入日志文件

LOG="/var/log/myapp/deploy_$(date +%Y%m%d_%H%M%S).log"

# 方式 1:在脚本开头重定向
exec > >(tee -a "$LOG") 2>&1

echo "部署开始..."
./install.sh
echo "部署完成"

# 方式 2:运行时重定向
./deploy.sh > /var/log/deploy.log 2>&1

实战 2:find + xargs 批量操作#

bash
# 删 30 天前的日志
find /var/log/myapp -name "*.log" -mtime +30 -print0 | xargs -0 rm -f

# 将 7 天前的日志压缩归档
find /var/log/myapp -name "*.log" -mtime +7 | xargs gzip

# 批量修改文件权限
find /var/www -type f -print0 | xargs -0 chmod 644

实战 3:管道跨机器备份#

bash
# 不落磁盘,直接通过网络备份到远程机器
tar czf - /data | ssh user@backup-server "cat > /backup/data_$(date +%Y%m%d).tar.gz"
#             ^ tar 的 -f - 表示输出到 stdout(不写文件)
#                                                    ^ cat > 接收 stdin 写入文件

# 跨机器同步目录(rsync 更好,但这展示了管道的灵活性)
ssh source-server "tar czf - /app/data" | tar xzf - -C /local/restore/

8. 调试技巧#

bash
# 管道出问题时,分段测试

# 不要一次性写完整条管道再试
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head

# 而是逐步构建
cat access.log | awk '{print $1}' | head      # 第 1 步:列对吗?
cat access.log | awk '{print $1}' | sort | head # 第 2 步:排序对吗?
# 逐级验证,问题一目了然

# 在管道中间用 tee 看中间结果
cat access.log | awk '{print $1}' | tee /tmp/ips.txt | sort | uniq -c
# 检查 /tmp/ips.txt 确认 IP 提取是否正确

9. 小结#

知识点一句话记忆
三个文件描述符0=stdin, 1=stdout, 2=stderr
> vs >>> 覆盖,>> 追加
2>&1把 stderr 合并到 stdout(顺序重要:>file 2>&1 对,2>&1 >file 错)
| 管道前一个 stdout → 后一个 stdin,stderr 不经过管道
管道退出码默认只看最后一个命令;脚本用 set -o pipefail 才不漏错
tee同时输出到屏幕和文件,管道中间"偷看"
xargs把 stdin 转成命令参数,配合 -print0 -0 处理空格

上一章:七、文本变换与提取 下一章:九、进程管理