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:把输入变参数
管道有个限制:有些命令(如 rm、echo)不读标准输入,只接受命令行参数。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. 综合实战
把这一篇的工具串起来,完成几个真实任务:
- 找出当前目录最大的 5 个文件:
du -ah | sort -rh | head -n 5 - 统计代码行数:
find . -name "*.py" | xargs wc -l | tail -n 1 - 批量重命名 .txt 为 .md:
ls *.txt | sed 's/.txt//' | xargs -I {} mv {}.txt {}.md - 下载并解压:
curl -L https://example.com/file.tar.gz | tar -xz
小结
这一篇你掌握了把命令组合起来的核心技能:管道 |、重定向 > >>、错误处理 2> 2>&1、三通 tee、参数转换 xargs、黑洞 /dev/null。这是命令行从"会用"到"飞起来"的转折点。下一篇讲软件包管理——怎么在 Linux 上装软件。
← 上一篇 Linux 进程管理
下一篇 Linux 软件包管理 →