Skip to content

管道与重定向

Shell 里处理日志、串联命令和写脚本时,离不开标准输入输出、重定向和管道。

一、三个标准通道

每个进程启动时,内核默认打开三个文件描述符。

文件描述符名称默认指向用途
0stdin键盘命令读取数据的地方
1stdout终端屏幕命令正常输出结果的地方
2stderr终端屏幕命令输出错误信息的地方

在 Shell 里,正常输出和错误输出默认都打到屏幕上,看起来混在一起。但底层是分开的两条通道,可以分别重定向。

bash
command >out.log 2>err.log

> 默认只重定向 stdout(等同于 1>)。stderr 不会被它带走,仍然会打印到屏幕上。

二、重定向

覆盖写入:

bash
echo "hello" > file.txt

追加写入(保留原有内容):

bash
echo "world" >> file.txt

单独重定向错误输出:

bash
ls /not-exist 2> err.log

将 stdout 和 stderr 合并到同一个文件:

bash
command > all.log 2>&1
command &> all.log              # 上面一句的简写形式

2>&1 的写法有顺序要求。command >all.log 2>&1 的执行顺序是:先 >all.log(把 stdout 指到文件),再 2>&1(把 stderr 指向当前的 stdout,即那个文件)。如果反过来写 2>&1 >all.log,stderr 先指向原来那个终端 stdout,然后 stdout 才被改掉——结果是 stderr 还打印在屏幕上。

定时任务里常见的写法:

bash
*/5 * * * * /opt/check.sh >>/var/log/check.log 2>&1

>> 追加而不覆盖。脚本执行频繁时,还要配合 logrotate 控制日志文件大小。

三、丢弃输出

bash
command >/dev/null
command >/dev/null 2>&1

/dev/null 是一个特殊的设备文件,写进去的数据会被直接丢弃。临时调试时可以用,生产脚本里把 stdout 和 stderr 全部丢弃后,失败时没有任何排查线索。至少保留 stderr 或关键步骤的输出。

四、管道

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

bash
ps aux | grep nginx

管道只传送 stdout,不传送 stderr。前一个命令报错时,错误信息仍然会直接显示在终端上。

很多命令可以直接读取文件,不需要 cat 中转:

bash
grep " 500 " access.log          # 更直接
cat access.log | grep " 500 "    # 多了一个进程,但功能一样

小文件无所谓,脚本里大批量处理时少一次管道就少一次进程开销。

管道的一个容易忽略的问题:默认情况下,管道命令的退出码是最后一个命令的退出码。

bash
grep "ERROR" app.log | head

如果 head 成功但 grep 没找到匹配而返回失败(grep 没匹配到返回 1),整体退出码是 0。巡检脚本里写上:

bash
set -o pipefail

pipefail 打开后,管道中任何一个命令失败,整条管道的退出码就是失败。

五、tee

tee 像 T 型三通——数据流进来,一份写到文件,一份继续输出到终端(或传给下一个命令)。

bash
echo "hello" | tee file.txt
echo "world" | tee -a file.txt    # -a 追加而非覆盖

一个常见场景:需要 sudo 权限写入受保护的文件:

bash
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ip-forward.conf

这里用 tee 而不是 sudo echo ... > file 的原因:重定向 > 发生在当前 Shell 进程里,不具备 sudo 权限;而 tee 以 sudo 身份运行,有权限打开目标文件。

六、xargs

xargs 把标准输入转换成命令的参数。

bash
find /var/log -name "*.log" | xargs ls -lh

xargs 从 stdin 读取数据,按空白字符分割后,拼到目标命令的末尾执行。默认的分割逻辑遇到文件名中有空格时会出错,配合 -print0-0 使用 null 字符作为分隔符:

bash
find /var/log -name "*.log" -print0 | xargs -0 ls -lh

删除文件前先打印确认:

bash
find /tmp -type f -mtime +7 -print         # 先看哪些会被删除
find /tmp -type f -mtime +7 -print0 | xargs -0 rm -f   # 确认后再删

在执行批量删除之前先跑一遍预览,避免路径条件写错导致误删。这是 find + xargs 组合操作的安全习惯。

七、几个常用组合

统计访问来源 IP 的 TOP N:

bash
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head

每条命令各司其职:awk 提取第一列(IP),sort 排序让相同 IP 聚在一起,uniq -c 统计每个 IP 出现次数,sort -nr 按次数降序排列,head 取前几行。

查看监听端口和对应进程:

bash
ss -lntp | grep ':80'

过滤最近错误日志的最后 50 行:

bash
grep -i "error" app.log | tail -n 50

统计 500 状态码的请求数:

bash
grep ' 500 ' access.log | wc -l

管道串到三四段以上时,可读性和排错都会变差。写成脚本、给中间结果起变量名,下次回头看时容易理解在做什么。