Linux 管道与重定向

如果说前面学的所有命令是"零件",管道和重定向就是把它们拼起来的"螺丝"。这是 Unix 哲学的精髓——"写只做一件事、做到极致的小程序,用管道把它们串起来"。一行管道命令能完成的工作,等价于写一段脚本。

1. 三个标准流

理解管道和重定向,先要理解每个进程都有三个标准流:

# Linux 进程的三个标准流

# 每个进程启动时自动获得三个流(文件描述符 fd):
#   fd 0  标准输入(stdin)   默认接键盘
#   fd 1  标准输出(stdout)  默认接屏幕
#   fd 2  标准错误(stderr)  默认接屏幕

# 用 cat 演示标准输入:
cat                    # 不带文件名,从 stdin 读
hello                  # 你输入什么,它回显什么
                       # 按 Ctrl+D 结束输入(发送 EOF)

# 用 echo 演示标准输出:
echo "hi"              # 输出到屏幕(就是 stdout)
echo "hi" > file.txt   # 重定向 stdout 到文件(屏幕上不显示了)

这三个流默认都连到终端(键盘和屏幕),但重定向可以把它们改连到文件,管道可以把两个进程的流对接起来。

2. 重定向:输出到文件

重定向用 >>> 把标准输出写进文件;用 2> 单独处理标准错误;用 2>&1 把两者合并。这是部署脚本、CI 配置里天天见到的写法。

# 重定向输出到文件

# > 覆盖写(文件不存在就建,存在就清空再写)
echo "hello" > file.txt
date > now.txt
ls -l > listing.txt

# >> 追加写(在文件末尾加,不清空)
echo "line 2" >> file.txt
date >> app.log              # 累积日志,每次运行追加一行

# 标准错误单独重定向(2>)
ls /nonexistent 2> err.txt   # 错误信息写进 err.txt,正常输出还到屏幕
command 2> /dev/null         # 把错误扔进黑洞(不关心错误时)

# 合并标准输出和错误(2>&1)
command > all.log 2>&1       # 两者都进 all.log(老写法,顺序很重要)
command &> all.log           # bash 4+ 简写(两者都进 all.log)
command &>> all.log          # 追加版的简写

# 重定向顺序很重要!这样写是错的:
command 2>&1 > all.log       # 错误仍然到屏幕
# 原因:2>&1 让 stderr 指向"当前 stdout 的目标"(屏幕),
#       然后 > 才把 stdout 改向到文件。stderr 已经被定死了。

# 正确顺序:先 > 再 2>&1
command > all.log 2>&1

最容易踩的坑是 2>&1顺序。记住规则:重定向是从左到右求值的,2>&1 让 stderr 指向"此时 stdout 指向的地方",所以必须写在 > 后面。简写 &> 不用考虑顺序,推荐日常用。

3. 管道:把命令串起来

竖线 | 把左边命令的标准输出,直接接到右边命令的标准输入——像流水线一样,每道工序做一件事,最后拼出复杂结果。

# 管道 | :把左命令的 stdout 喂给右命令的 stdin

# 基本用法
ls -l | less                  # 翻页看长输出(按 q 退出)
ls -l | grep ".py"            # 在 ls 输出里找 .py
ps aux | grep nginx           # 找进程(高频)
cat file.txt | wc -l          # 数行数(等于 wc -l file.txt)
history | grep ssh            # 在历史命令里找 ssh

# 多级管道(像流水线)
cat access.log | grep "404" | wc -l           # 404 出现次数
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
#   取IP → 排序 → 去重计数 → 按次数倒排 → 取前几

# 管道的本质:fork 两个进程,用匿名管道连起来
#   左边进程的 stdout 接到右边进程的 stdin
#   两个进程并行执行,数据"流"过去

经典例子:cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 一行管道搞定"访问最多的 IP"。用 Python 写至少十几行。这就是命令行的威力。

4. tee:三通阀门

管道有个问题:输出直接传给下个命令,屏幕上看不到了。tee 解决这个——它像水管的三通,一路继续传,一路写文件。

# tee:三通,把输入分两路——一份写文件,一份继续输出

# 用途:想"看到输出"的同时又"存到文件"
ls -l | tee listing.txt       # 既显示在屏幕,又写进 listing.txt
echo "deploy start" | tee -a deploy.log   # -a 追加(等于 >>)

# 在 sudo 场景特别有用(重定向是 shell 做的,不是 sudo):
echo "127.0.0.1 mysite" | sudo tee -a /etc/hosts
# 直接 echo > /etc/hosts 会因为权限失败
#   (重定向发生在你的 shell 里,此时还没提权)
# 用 tee 接管 /etc/hosts 的写入,tee 由 sudo 提权

# 多个输出
command | tee a.txt b.txt     # 同时写两个文件

tee 还有个隐藏用法:用 sudo tee 写需要 root 权限的文件。因为普通 echo "x" > /root/file 会失败——重定向是你的 shell 做的,而你的 shell 没有 root 权限。改成 echo "x" | sudo tee /root/file 就行:echo 用普通权限,tee 用 sudo 提权。

5. xargs:把输入变参数

管道有个限制:有些命令(如 rmecho)不读标准输入,只接受命令行参数。xargs 的作用就是把 stdin 转换成参数。

# xargs:把 stdin 转成命令参数(管道的好搭档)

# 问题:有些命令不从 stdin 读,只接参数
# 例:echo "/etc" | rm 不工作(rm 不读 stdin)
echo "/tmp/file" | xargs rm           # 把 stdin 当参数传给 rm

# find + xargs 是黄金组合
find . -name "*.log" | xargs grep "error"
#   find 找到一批文件,xargs 把它们作为参数传给 grep
#   比 find -exec grep 快(批量传参,少 fork)

# 控制每次传几个参数
find . -name "*.bak" | xargs -n 1 rm
#   -n 1:每次只传 1 个参数(rm 一次删一个)

# 处理带空格的文件名(用 -0 配合 find -print0)
find . -name "*.mp3" -print0 | xargs -0 ls -l
#   -print0 用 \0 分隔,-0 用 \0 切(空格、换行都不会切错)

# 批量操作
cat urls.txt | xargs -n 1 curl         # 批量请求 URL
cat users.txt | xargs -I {} useradd {} # -I {} 用 {} 占位

find ... | xargs grep 是黄金组合,比 find -exec 快得多(批量传参减少进程创建)。注意文件名带空格时必须用 -print0-0,否则会被空格切断。

6. 输入重定向与 Here Document

除了把输出送到文件,还能反过来把文件作为输入:

# 标准输入重定向

# < :从文件读输入(代替键盘)
mysql -u root < dump.sql        # 把 SQL 文件喂给 mysql
wc -l < /etc/passwd             # 数 /etc/passwd 的行数
sort < names.txt > sorted.txt   # 读 names.txt 排序后写 sorted.txt

# << :Here Document(脚本里写多行文本)
cat << EOF
第一行
第二行
EOF
#   从下一行开始读到 EOF 之间的内容作为 stdin
#   脚本里写多行配置、SQL、邮件常用

# <<< :Here String(把字符串当输入)
wc -l <<< "hello world"         # 字符串作为输入

Here Document(<< EOF ... EOF)在 Shell 脚本里写多行配置、SQL、邮件正文时极其方便。

7. /dev/null:黑洞

/dev/null 是一个特殊的设备文件,写到这里的所有数据都被丢弃。看起来没用,实际场景很广:

# /dev/null:黑洞设备文件(写到这里的都消失)

command > /dev/null             # 不要输出(丢弃 stdout)
command 2> /dev/null            # 不要错误(丢弃 stderr)
command > /dev/null 2>&1        # 啥都不要(脚本里常用)
command &> /dev/null            # 同上的简写

# 典型场景
#   crontab 里不想收到邮件通知:
#   0 3 * * * /opt/backup.sh > /dev/null 2>&1
#   测试命令是否成功(只关心退出码,不关心输出):
#   if grep -q "error" /var/log/app.log 2>/dev/null; then ...

典型:cron 定时任务不想要邮件通知,就加 > /dev/null 2>&1;测试命令是否成功只看退出码就加 2> /dev/null 屏蔽错误输出。

8. 进程替换(进阶)

Bash 还有个高级功能叫进程替换,把命令输出包装成临时文件,可以一次喂给需要文件参数的命令:

# 进程替换(进阶,但很实用)

# <(...) :把命令的输出临时变成一个"文件"
diff <(ls dir1) <(ls dir2)     # 比较两个目录的文件列表
#   等价于:ls dir1 > /tmp/a; ls dir2 > /tmp/b; diff /tmp/a /tmp/b

# >(...) :把输出送到命令(类似管道但能传多个)
tar -czf - data | tee >(sha256sum > data.sha) >(wc -c > data.size) > /dev/null
#   边打包,边算校验和、边算大小

diff <(ls dir1) <(ls dir2) 比较两个目录内容,不需要再创建临时文件——非常优雅。

9. 综合实战

把这一篇的工具串起来,完成几个真实任务:

小结

这一篇你掌握了把命令组合起来的核心技能:管道 |、重定向 > >>、错误处理 2> 2>&1、三通 tee、参数转换 xargs、黑洞 /dev/null。这是命令行从"会用"到"飞起来"的转折点。下一篇讲软件包管理——怎么在 Linux 上装软件。

← 上一篇 Linux 进程管理

下一篇 Linux 软件包管理

✈️💬