Linux 管道与重定向:命令组合艺术
Unix 哲学有一条经典原则:“做一件事,并把它做好;程序之间可以协作。” 管道和重定向正是这一哲学的实现机制。它们不是独立的命令,而是 Shell 提供的"胶水",让 grep、sed、awk、sort、uniq 等小工具可以像乐高积木一样组合起来,完成复杂的任务。理解它们的工作方式,是从"会用命令"到"会写脚本"的关键一步。
三个标准流
每个 Linux 进程启动时,都会自动打开三个标准流:
| 流 | 文件描述符 | 默认指向 | 用途 |
|---|---|---|---|
| stdin | 0 | 键盘 | 读取输入 |
| stdout | 1 | 终端屏幕 | 输出正常结果 |
| stderr | 2 | 终端屏幕 | 输出错误信息 |
重定向的本质,就是改变这三个流的指向——让它们从文件读、写到文件、或者互相合并。
最基础的用法
# 管道:将前一个命令的 stdout 连接到后一个命令的 stdin
ls -l | grep ".txt"
# 输出重定向:将 stdout 写入文件(覆盖)
echo "hello" > file.txt
# 输出重定向:将 stdout 追加到文件
echo "world" >> file.txt
# 输入重定向:从文件读取而非键盘
sort < unsorted.txt
实际使用中,高频场景与命令组合
1. 管道串联多个命令
# 统计当前目录下 .py 文件的数量
find . -name "*.py" | wc -l
# 查看占用内存最多的前 5 个进程
ps aux | sort -k4 -rn | head -5
# 从 Nginx 日志中提取访问量最高的 10 个 IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# 实时监控系统日志中的错误
journalctl -f | grep -i "error"
管道可以无限串联,每个命令只负责一件事,数据像流水线一样依次流过。
2. 覆盖与追加
# 覆盖(文件原有内容被清空)
date > /tmp/last_run.txt
# 追加(保留原有内容)
date >> /tmp/last_run.txt
# 危险操作:重定向到正在读取的文件会导致数据丢失
cat file.txt | grep "pattern" > file.txt # 错误!file.txt 会被先清空
重要:
>会先清空目标文件再写入,如果目标文件和输入文件是同一个,数据会丢失。此时应使用临时文件或sponge命令。
3. 合并标准输出和标准错误
# 将 stdout 和 stderr 都写入同一个文件
command > output.log 2>&1
# Bash 4+ 的简写形式
command &> output.log
# 追加模式
command >> output.log 2>&1
command &>> output.log # Bash 4+
2>&1 的含义是:将文件描述符 2(stderr)重定向到文件描述符 1(stdout)当前指向的位置。顺序很重要——> file 2>&1 正确,2>&1 > file 则 stderr 仍指向终端。
4. 丢弃不需要的输出
# 丢弃 stdout(常见于 cron 任务)
command > /dev/null
# 丢弃 stdout 和 stderr
command > /dev/null 2>&1
command &> /dev/null # Bash 4+
# 只丢弃 stderr,保留 stdout
command 2>/dev/null
/dev/null 是 Linux 的"黑洞",写入其中的数据会被永久丢弃。
5. Here Document(内联输入)
# 将多行文本作为输入传递给命令
cat << EOF > config.txt
server {
listen 80;
server_name example.com;
}
EOF
# 配合 ssh 远程执行多行命令
ssh user@host << 'EOF'
cd /var/www
git pull
systemctl restart nginx
EOF
<< EOF 表示从当前行开始,直到遇到单独的 EOF 行为止,中间的所有内容都作为 stdin 输入。用单引号包裹 EOF(<< 'EOF')可以阻止变量展开。
6. Here String(单行内联输入)
# 将字符串作为输入
grep "pattern" <<< "this is a test string"
# 配合 bc 做计算
result=$(bc <<< "3.14 * 2.5")
<<< 是 Bash 特有的简写,比 Here Document 更轻量。
7. 管道中的子 Shell 陷阱
# 错误:变量在子 Shell 中赋值,外部不可见
cat file.txt | while read line; do
count=$((count + 1))
done
echo $count # 仍然是 0!
# 正确:使用进程替换或重定向
count=0
while read line; do
count=$((count + 1))
done < file.txt
echo $count # 正确
管道中的每个命令都在子 Shell 中运行,子 Shell 中对变量的修改不会影响父 Shell。这是新手常踩的坑。
8. tee:同时输出到终端和文件
# 既在终端显示,又写入文件
ls -l | tee output.txt
# 追加模式
ls -l | tee -a output.txt
# 同时写入多个文件
command | tee file1.txt file2.txt > /dev/null
tee 在调试长管道时非常有用——你可以在中间某个节点插入 tee 来检查数据流的状态。
9. 进程替换
# 比较两个命令的输出
diff <(ls /dir1) <(ls /dir2)
# 将命令输出当作文件使用
grep -f <(echo "pattern") file.txt
<(command) 会创建一个临时的命名管道(/dev/fd/xx),让命令的输出可以像文件一样被读取。这解决了"某些命令只接受文件参数,不接受管道输入"的问题。
10. 命名管道(FIFO)
# 创建命名管道
mkfifo /tmp/my_pipe
# 终端 1:写入数据
echo "hello" > /tmp/my_pipe
# 终端 2:读取数据
cat /tmp/my_pipe
命名管道是持久化的管道文件,可以实现两个独立进程之间的通信。
如果不知道如何下手,可以先尝试
ls -l | head -5
这条命令将 ls -l 的输出通过管道传给 head,只显示前 5 行。确认效果后,可以逐步增加复杂度:
ls -l | awk '{print $5, $9}' | sort -rn | head -5
提取文件大小和文件名,按大小降序排列,显示最大的 5 个文件。
或者,当你想记录一个长时间运行命令的输出时:
./build.sh 2>&1 | tee build.log
既能在终端实时看到构建进度,又能将完整日志保存到文件。
常见误区与注意事项
>和>>的区别:>覆盖,>>追加。写脚本时务必确认用对了。- 管道中变量不共享:管道两侧是独立进程,变量不互通。需要共享状态时用
< file重定向代替管道。 2>&1的顺序:必须写在> file之后,否则 stderr 不会进入文件。- 大文件避免多次重定向:
cat file | grep pattern | sort比grep pattern file | sort多了一个无用的cat进程(UUOC, Useless Use of Cat)。直接用文件参数更高效。 - 管道会缓冲:某些命令在检测到输出不是终端时会启用行缓冲或块缓冲,导致实时性下降。可以用
stdbuf -oL command强制行缓冲。
管道与重定向速查表
| 语法 | 含义 |
|---|---|
cmd1 | cmd2 |
将 cmd1 的 stdout 连接到 cmd2 的 stdin |
cmd > file |
将 stdout 重定向到文件(覆盖) |
cmd >> file |
将 stdout 重定向到文件(追加) |
cmd < file |
从文件读取 stdin |
cmd 2> file |
将 stderr 重定向到文件 |
cmd > file 2>&1 |
stdout 和 stderr 都写入文件 |
cmd &> file |
同上(Bash 4+ 简写) |
cmd > /dev/null |
丢弃 stdout |
cmd 2> /dev/null |
丢弃 stderr |
cmd | tee file |
同时输出到终端和文件 |
cmd <(subcmd) |
进程替换,将子命令输出当作文件 |
- 点赞
- 收藏
- 关注作者
评论(0)