Bash 管道后接多条命令:四种写法与常见坑全解析

发布时间:2026/10/9 8:25:56
Bash 管道后接多条命令:四种写法与常见坑全解析 前几天有个读者问我“bash 里怎么让管道后面接多条命令我写echo hello | grep h echo found后面的 echo 倒是执行了但它根本不读管道里的数据这是为什么”这个问题几乎每个写过 bash 脚本的人都撞过。我当时的第一个反应是你想要的到底是哪一种“多条命令”因为“管道执行多条命令”这句话在 bash 里其实对应着至少四种完全不同的写法用错方案脚本跑出来的结果就会很诡异。这篇文章就把这四种场景一次讲透管道右侧怎么捆绑多条命令、同一份输出怎么同时分给多个下游、每一行输入怎么触发一组命令以及最关键的——bash 到底是怎么解析|、、;这些符号的。全文涉及的命令在 Linux、macOS 和 Git Bash 下都能直接跑适合刚接触 bash 的读者也适合写过不少脚本但没细究过管道语义的人。1. 先搞明白 bash 是怎么切分管道的|、、;根本不是一回事很多人写管道遇到问题第一反应是去查“管道怎么用”但真正卡住他们的往往是 bash 的语法解析顺序。你写出来的命令长什么样和 bash 眼里这条命令长什么样经常是两码事。1.1 一句话记住 bash 的语法层级bash 把一整条命令脚本解析成四个层级列表list→ 逻辑链and_or→ 管道pipeline→ 单条命令command。用简化语法表示就是list : and_or ( (; | ) and_or )* and_or : pipeline ( ( | ||) pipeline )* pipeline : command ( | command )*翻译成人话;分隔的是“列表项”和||连接的是“管道”|连接的是“单条命令”。所以|的优先级比高的优先级比;高。看两个例子就明白了echo hello | grep hello echo 命中 # bash 实际理解为 ( echo hello | grep hello ) echo 命中echo hello | grep hello; echo 命令结束 # bash 实际理解为 ( echo hello | grep hello ); echo 命令结束第一行里echo 命中能不能执行取决于管道echo hello | grep hello的退出码也就是grep有没有匹配到它本身完全不在管道里。第二行里echo 命令结束跟前面那条管道没有任何关系不管管道是成功还是失败它都会执行。这就是为什么你写cmd1 | cmd2 cmd3时总觉得“cmd3 吃不到管道里的数据”——因为它压根不在管道里。cmd3的 stdin 还是继承自当前 shell 的终端不是管道写端。1.2 一句“管道后面接多条命令”背后其实是四种需求把问题拆开之后你会发现“多条命令”这个说法太模糊了。我一般会让提问者先回答你到底想做下面哪件事需求一句话描述对应写法串联输出先经过 A 再经过 B形成处理链cmd | A | B右侧分组管道右边的这一步要同时做 A 和 B 两件事cmd | { A; B; }或cmd | ( A; B )分流同一份输出同时喂给 A 和 B 两个下游cmd | tee (A) (B)逐行批处理每一行输入都触发一组命令cmd | while read ...; do ...; done或xargs这四种需求看起来都叫“让管道执行多条命令”但方案完全不同。很多人踩坑是因为把“分流”当成了“右侧分组”来写或者把“逐行批处理”写成了“串联”结果数据被第一个命令吃掉后面的命令永远读到空。1.3 匿名管道的底层到底是什么bash 里的|创建的是匿名管道anonymous pipe。底层是系统调用pipe()内核分配一块内存缓冲区返回两个文件描述符一个管写write end一个管读read end。左边命令的 stdout 被重定向到写端右边命令的 stdin 被重定向到读端数据就顺着这块缓冲区从左流到右。它有几个关键特性理解之后很多“怪现象”都不用查了匿名这个管道在内核里没有文件系统层面的名字只通过 fork 传给子进程。所以它只能连接“父子进程之间”的命令你没法从一个独立终端往里写数据。需要跨进程、有名字的管道得用mkfifo创建 FIFO命名管道。字节流管道里没有“消息”这个概念就是连续不断的字节。左边写入 100 字节右边可能一次 read 到 100 字节也可能分 10 次每次 10 字节。它跟 C# 里的 Pipe 通信、工业上的管道机器人虽然都叫“管道”但完全不是一回事别混着类比。只有一个消费者数据从管道读走后就没了。管道不是磁盘文件不能“读两遍”。后面第五节会有专门演示。有缓冲区上限Linux 上管道默认容量通常是 64KB老内核是 4KB 页。如果右边读得慢左边写满缓冲区就会被阻塞如果右边先退出左边再写就会收到 SIGPIPE 信号直接终止。记好“字节流、单消费者、容量有限”这三点后面所有坑都能解释通。2. 管道右侧捆绑多条命令花括号和圆括号的正确姿势先解决最字面的需求管道右边接多条命令。比如你想对管道输出先做一件事、再做第二件事这两件事都要在管道这个“下游”里完成。2.1 花括号分组的基本写法与语法细节bash 里把多条命令“捆成一条命令”的语法是花括号分组printf hello world\n | { read -r line; echo 收到: $line; echo 字符数: ${#line}; }输出收到: hello world 字符数: 11这里管道右边的{ read -r line; echo ...; echo ...; }在语法树上是一个整体也就是一个 command所以它合法地占据了管道的右端。写花括号分组有三个细节少了任何一个都会报错{后面必须有一个空格。{在 bash 里是保留字不是运算符它靠空白和后面的命令分隔。最后一条命令后面必须有一个;或者用换行收尾。}前面也要有空格或换行它才算独立 token。所以下面这两种写法都是对的printf a\nb\n | { cat; echo ---; }printf a\nb\n | { cat echo --- }第二种写法在多命令场景里可读性好很多我实际写脚本时基本都这么排版。2.2 花括号和圆括号的区别当前 shell 与子 shell除了{ ...; }你还可以用( ... )把多条命令圈起来printf hello world\n | ( read -r line; echo 收到: $line )看起来效果一样但底层有个重要区别花括号分组在当前 shell 里执行圆括号分组强制开一个子 shell 执行。在没有管道的情况下这个区别影响很大对比项{ cmd; }( cmd )运行位置当前 shell子 shell变量修改能保留到后面子 shell 退出即销毁cd影响会改变当前目录临时改变结束自动回退额外开销没有 fork有一次 fork举个例子x0 { x1; } echo $x # 输出 1 ( x2; ) echo $x # 还是 1圆括号里的赋值影响不到外面但这里有个大多数人都忽略的细节一旦把分组放在管道右端连花括号也救不了变量。因为管道本身就要求每个组成部分在子 shell 里运行。你看bash -c x0; echo hi | { read y; x1; }; echo x$x # 输出 x0不是 x1花括号只负责“把多条命令捆成一条”不负责“让变量穿透管道”。想让循环里改的变量带出来得用第四节讲的进程替换或lastpipe这里先记住结论。2.3 分组之后还能继续接管道花括号分组不只是能当管道右端它自己还能作为管道的一个环节继续往后面接命令printf a\nb\nc\n | { tr a-z A-Z; echo --- 分割线 ---; } | cat -n输出1 A 2 B 3 C 4 --- 分割线 ---这里花括号组内所有命令的 stdout 都汇入同一条管道交给后面的cat -n。注意echo --- 分割线 ---并不是“管道之后”的命令它也在管道里它的输出同样会进入下游。这正是“右侧分组”和“写在外面”的本质区别。2.4 实战案例日志统计只用一条命令链光讲语法没意思来个真实场景。假设你有一份 nginx 或 Apache 的 access.log想统计每个 IP 的请求次数输出 Top 10再补一行说明文字cat access.log | { awk {print $1} | sort | uniq -c | sort -rn | head -10 echo --- 以上是 Top10 IP --- }awk {print $1}切出第一列 IPsort | uniq -c | sort -rn完成计数排序head -10取前十条最后 echo 输出分隔说明。整个统计逻辑在管道右侧这一“步”里中间用管道串联最后整体是一个下游。如果你想同时知道总请求数别想着再从管道里取——流已经被前面的awk消费掉了。老老实实另开一条grep ERROR app.log | { sed s/^\[//; s/\]// | cut -d -f2- | sort | uniq -c | sort -rn echo 错误总数: $(grep -c ERROR app.log) }这里的$(...)直接读原文件不碰管道流这样才拿得到正确数字。这也是很多人栽跟头的地方分组内多条命令共享同一个 stdin但流只有一个谁先读谁拿走。这个点我在第五节会专门再演示一遍。3. 一份数据同时喂给多个下游tee 结合进程替换“管道右侧接两条命令”还有一种更难的变体不是让两条命令按顺序处理同一份数据而是让两条命令同时各自拿到完整的一份数据。比如一份日志既要筛错误又要统计行数还要提取客户端 IP 列表。先泼一盆冷水管道默认做不到。3.1 先演示一个“下游只能有一个”的事实你可能会想那我写cmd | { A; B; }A 和 B 不都能读管道吗试一下就知道了printf a\nb\n | { cat; echo ---; cat; }输出a b ---第二个cat什么都没输出。原因前面说过管道是字节流第一个cat把数据全读走了第二个cat的 stdin 立刻到达 EOF读到的是空。分组只解决了“多条命令在语法上绑定在一起”的问题没解决“数据只能消费一次”的问题。想让多个下游各拿一份完整数据必须主动“复制数据”。bash 里最常用的工具是tee。3.2 tee 的常规用法边落盘边传递tee这个名字来自“T 型分流器”它的行为就是把 stdin 原样写到文件同时原样输出到 stdout。最基本用法ls -1 | tee file_list.txt | wc -l终端上能看到ls的输出file_list.txt里也存了一份wc -l还能接着统计行数。tee -a则是追加而不是覆盖。但tee只能复制一份到文件不能复制到“另一条命令”。想让它把数据分给命令就得配合进程替换。3.3 进程替换(...)的原理与完整案例进程替换是 bash 提供的一个很像“把命令当文件用”的机制。(cmd)会被 bash 展开成一个类似/dev/fd/63的路径这个路径是个管道写端当你把(cmd)当作一个“文件名”传给tee时tee就像打开普通文件一样打开它而实际上另一端cmd的 stdin 正连着这个管道的读端。一段代码胜过千言万语cat app.log \ | tee (grep -i error | sort -u errors.txt) \ (awk {print $1} | sort | uniq -c | sort -rn top_ip.txt) \ (wc -l total.txt) \ /dev/null这条命令做的事情grep -i error | sort -u errors.txt从完整日志里筛出错误信息并去重写入 errors.txt。awk {print $1} | sort | uniq -c | sort -rn top_ip.txt提取 IP 并按访问次数排序写入 top_ip.txt。wc -l total.txt统计总行数写入 total.txt。最后的 /dev/null是给tee的主 stdout 一个去处避免它把完整数据再打到终端刷屏。tee每读到一份数据会同时写进它打开的所有输出——三个进程替换加上 /dev/null。这样一来三个下游命令并行处理同一份数据谁也不影响谁。3.4 异步竞态的坑什么时候最好别用进程替换进程替换看起来很美但它有个让人头疼的副作用异步。tee退出、主管道结束的时候(...)里的子进程不一定已经写完文件。如果脚本下一步立刻去读 errors.txt很可能读到空文件或半截内容。我实测过数据量小几百 KB的时候通常管道结束时子进程也收尾了但数据量大或下游命令里有sort这种要等全部输入才能输出的命令时竞态就很容易出现。sort必须等所有数据到齐才能排序写文件主管道一结束它的输入就到 EOF 了可排序和写文件可能还要几十毫秒你的下一条命令早跑完了。所以我的建议很直接调试阶段、或者下游命令简单且结果不急着用可以用进程替换简洁漂亮。生产脚本、或者下一步立刻要读结果文件老老实实先落一个完整的临时文件再分三条命令处理确定性远高于进程替换。朴素但可靠的版本cat app.log /tmp/full.log grep -i error /tmp/full.log | sort -u errors.txt awk {print $1} /tmp/full.log | sort | uniq -c | sort -rn top_ip.txt wc -l /tmp/full.log total.txt rm /tmp/full.log多两行代码但每一步的先后关系清清楚楚不会出现“文件还没写完”这种玄学问题。真实运维场景里我宁可牺牲一点“优雅”也要换可预期的行为。4. 每行输入都触发一组命令while read 与 xargs第三种高频需求管道每输出一行就要对这一行做一组操作。比如拿到一个文件列表对每个文件先打印名字、再统计行数、再检查关键字。这种模式用while read和xargs都能做但细节差异很大。4.1 while read 的标配写法最通用的逐行处理循环ls -1 *.txt | while read -r f; do echo 文件: $f wc -l $f donewhile read每次从管道读一行按IFS默认是空格、Tab、换行切分成多个字段剩余部分全放进最后一个变量。比如printf alice 30\nbob 25\n | while read -r name age; do echo $name 今年 $age 岁 done两个细节我建议直接养成习惯永远加-rread -r不会把反斜杠当转义符。没有-r的话文件名里的\n这类字符会被吃掉一部分文件名就缺斤短两了。需要保留行首行尾空白时用IFS read -r默认情况下read会trim掉首尾空白printf hello \n | { IFS read -r line; printf %s\n $line; }才能得到 hello 。4.2 子 shell 陷阱循环里改的变量为什么带不出来这是 bash 管道最具迷惑性的坑。看这段count0 printf a\nb\nc\n | while read -r _; do ((count)) done echo count$count直觉上应该输出count3实际输出count0。原因还是那个管道的右端跑在子 shell 里while循环里的count是子 shell 的变量循环一结束子 shell 就没了外面的count一直是 0。解决方案有三种方案一进程替换把“管道”变成“重定向”count0 while read -r _; do ((count)) done (printf a\nb\nc\n) echo count$count # 输出 3 (cmd)把命令的输出当作一个文件描述符重定向给while这里没有管道while跑在当前的子 shell 里变量自然能保住。这也是(cmd)这个输入侧进程替换的经典用途。方案二开 lastpipe让管道最后一个元素跑在当前 shellshopt -s lastpipe count0 printf a\nb\nc\n | while read -r _; do ((count)) done echo count$count # 输出 3lastpipe开启后非交互式 bash 会让管道右端最后一条命令在当前 shell 里执行。注意它只在“job control 关闭”时生效也就是脚本里可以交互终端里经常不灵。方案三把要保留的结果写进临时文件printf a\nb\nc\n | while read -r _; do ((count)) echo $count /tmp/count.txt done count$(cat /tmp/count.txt)简单粗暴写一次性脚本时很实用。但我个人更推荐方案一因为它不加额外文件、也不依赖 shell 选项。4.3 xargs 用“命令模板”批量执行多条命令xargs是另一种批量执行方式适合“一条模板命令处理多个参数”的场景。配合sh -c模板里可以塞多条命令find . -name *.log -print0 \ | xargs -0 -I {} sh -c echo {}; wc -l {}; grep -i error {} | wc -l解释一下这个组合find ... -print0用\0分隔文件名能正确处理带空格的文件名。xargs -0告诉 xargs 输入是用\0分隔的。-I {}指定{}为占位符xargs 每读一个参数就把参数替换到所有{}出现的位置生成一条命令执行。sh -c ...里的多条命令会在一个子 shell 里执行于是“一个文件 → 三条命令”就成立了。如果想并行处理加-P 4表示同时跑 4 个进程。注意并行时输出顺序会乱因为每个子 shell 各自往 stdout 写。有个引号坑要提前说{}是直接文本替换如果文件名里有双引号或$这类特殊字符替换进sh -c的单引号字符串后可能会被二次解析轻则命令出错重则执行了不该执行的东西。处理不可信文件名时优先用while read -r加数组别用xargs -I {} sh -c。4.4 什么时候选 while什么时候选 xargs我自己的选择标准是这样的对比项while readxargs -I {} sh -c可读性高逻辑直接写在循环体里中要套一层sh -c字符串复杂逻辑随意写 if / case / 函数调用写多了很难维护变量回传配合 (cmd)可以带出带不出来子 shell 隔离并发自己写和wait麻烦一行-P 4搞定文件名安全read -r 引号基本安全-0安全但套sh -c后引号有风险一句话逻辑复杂要可控用 while纯批量、要并发、命令简单用 xargs。别为了少写两行 while 把逻辑塞进sh -c字符串里调试的时候你会后悔的。5. 排掉这几个高频坑管道脚本才算稳前面各节已经穿插讲了不少原理最后把我实际踩过、也看别人反复踩的几个坑集中列出来每条都是可以直接对照检查的。5.1 花括号三种最常见的语法报错花括号分组看着简单错误率却很高。最常见的三种# 错{ 和命令之间没空格bash 把 {echo 当成命令名 {echo hi; } # 错最后一条命令后面没分号} 被当成命令参数 { echo hi } # 对 { echo hi; }报错信息通常是command not found: {echo或syntax error near unexpected token }。看到这两种提示先检查空格和分号十有八九是这个问题。圆括号没有这些规矩因为它不是保留字而是语法结构所以(echo hi)也能跑。5.2的绑定位置和 grep 退出码的连锁反应再次强调优先级cat app.log | grep error echo 发现错误这里的echo 发现错误依赖于grep的退出码grep没匹配到时退出码是 1echo 不会执行。它不在管道里不会收到任何管道数据。如果你想让 echo 也在管道这一步里、并且不管有没有匹配都执行cat app.log | { grep error || true; echo 检查完成; }grep error || true保证分组内第一条命令的退出码是 0echo 一定执行。这类写法在“先检查再汇报”的脚本里很常见。如果你还开了set -e忘了这个退出码问题脚本可能中途就退出了。5.3 一个管道流只能被消费一次这个坑值得再看一次具体案例。比如你想从 /etc/passwd 里分别统计含 root 的行和含 nologin 的行cat /etc/passwd | { grep root | wc -l echo --- grep nologin | wc -l }第一个grep root | wc -l会把整个管道流读完第二个grep nologin的 stdin 已经是 EOF结果必然是 0。要拿到两个统计数字必须事先复制数据cat /etc/passwd /tmp/passwd.copy grep -c root /tmp/passwd.copy grep -c nologin /tmp/passwd.copy rm /tmp/passwd.copy或者用第二节的 tee 分流方案。记住一句话管道流是“读一次就没”的同一份数据要给多个命令先落盘或先 tee。5.4 读行时被 IFS 和 CRLF 坑到的经验read默认按 IFS 切分并吞掉首尾空白处理配置类文本时经常出问题。比如某行是host 10.0.0.1你用read -r key value读key拿到的是host但value拿到的是10.0.0.1多了前后空格和引号。处理这类内容要么先tr -d 要么read -r key value $line后自己再 trim。Windows 上写脚本或处理 Windows 来源的文件还要提防 CRLF。Git Bash 里从文件读行时行尾经常带着\r你 echo 出来看不出来比较字符串时永远不相等。统一在管道里先清一下sed s/\r$// input.txt | while read -r line; do echo 处理: $line done这个s/\r$//看起来不起眼但能救回你一晚上的调试时间。5.5 用 set -x 和 PIPESTATUS 快速定位问题管道脚本出问题时最忌讳干瞪眼猜。我的排查步骤基本固定第一步给脚本或当前 shell 开跟踪bash -x your_script.sh或者临时在脚本里set -x每条命令展开后的真实样子都会打出来一眼就能看到 bash 实际执行了谁、参数变成什么样。第二步看管道的退出码。默认情况下管道返回的是最后一个命令的退出码前面命令失败了你是看不到的。比如ls /nonexistent | wc -l echo ${PIPESTATUS[]} # 输出类似: 2 0PIPESTATUS数组按顺序保存管道里每条命令的退出码ls失败是 2wc成功是 0。如果只看$?你会以为整条管道成功了问题就被掩盖了。第三步脚本开头统一加上set -Eeuo pipefailpipefail让管道在任何一条命令失败时都返回非零-e让脚本在出错时及时停下-u抓住未定义变量。这三个组合是 bash 脚本的“安全带”能挡住一大半低级错误。我个人现在写管道脚本的习惯是拿到需求先判断它是“串联、分组、分流”还是“逐行批处理”绝不混用两条命令要读同一份数据默认先落盘循环变量要回传默认 (cmd)脚本头部永远挂着set -Eeuo pipefail。这套习惯帮我避免了绝大多数管道相关的玄学问题。你把这几个坑提前排掉管道脚本基本就稳了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询