先说一个我印象特别深的场景。某次帮某团队排查定时任务脚本里写了run_task task.log结果日志文件空空如也任务本身却明明跑完了。排查到最后才发现真正的报错信息全被输出到了标准错误而重定向只抓走了标准输出。这种问题在做开发、做运维、甚至日常用命令行的朋友身上太常见了没搞懂重定向、管道以及标准输入标准输出之间的关系日志丢了、数据处理错了、脚本行为诡异你甚至不知道从哪儿下手。这篇文章就把这三个概念彻底拆开结合实操场景讲清楚它们到底是什么、怎么配合、以及最容易踩的坑。1. 三个标准流0、1、2 到底是什么1.1 stdin、stdout、stderr 的定位很多同学把标准输入、标准输出当成两个术语实际上它们是三个标准输入stdin、标准输出stdout和标准错误stderr。每个进程在启动时操作系统都会给它预设三根数据通道标准输入文件描述符 0程序默认从这里读取数据。终端下默认读取你的键盘输入文件、另一个程序、一段字符串都可以给它喂数据。标准输出文件描述符 1程序把正常结果写到这里。默认显示在屏幕上。标准错误文件描述符 2程序把错误信息写到这里。默认也是显示在屏幕上。关键在于这是一套进程级的默认机制不是某个 shell 特有的行为。你在终端执行ls、grep、python、java底层全都遵守这套约定。比如一个用 .NET 写的后台服务它用 Console.WriteLine 输出的信息走标准输出用 Console.Error.WriteLine 输出的内容走标准错误操作系统不关心你写的代码是什么语言它只认这三个通道。用水管类比最好理解标准输入是进水口标准输出是正常出水管标准错误是另一根报警出水管。三根管子一开始都接到终端上键盘往里进水两根本来出水的管子的水流都显示成屏幕上的字符。重定向和管道做的事情本质上是把这几个管子接口重新接。1.2 为什么要把正确结果和错误信息分开分开设计不是闲得无聊是为了数据可信。如果正常输出和错误信息混在一根管子里你想把程序结果保存下来错误也会掺进去反过来你只想看报错正常结果又会刷屏。举一个非常实际的例子你运行find / -name *.conf result.txt如果不把标准错误和标准输出分开权限不足的提示会污染结果文件后面再用grep处理 result.txt 就会得到一堆根本没权限读到的路径。操作系统把两根出水管分开就是让你可以独立处理正常的结果和异常的情况。从编程视角其实更好理解任何工具函数建议用返回值和异常机制分离正常路径与错误路径。命令行里的 stdout 和 stderr 就是这种分离思想在进程层面的实现。理解了这一点你对2/dev/null这种写法的理解会立刻上一个台阶——它不是玄学就是把报警水管接进碎纸机。1.3 文件描述符数字的直觉记忆法Linux 里用数字 0、1、2 代表这三个标准流很多教程直接让你记住但你可以通过编号顺序建立直觉0 在最前面表示输入源头天然符合数字扫描顺序从 0 开始的习惯。1 代表主要的正常出口也就是最常看到的输出。2 代表次要的额外出口也就是第二根出水管。在 shell 中你经常会看到、、这些符号它们默认操作的是标准输入和标准输出而标准错误必须显式加2才能重定向。这个默认操作 0 和 1必须点名才操作 2的规则恰恰是无数新手处理日志时最迷惑的地方。我的建议是每次写重定向心里都默念一遍我现在接的是哪根管子这个习惯能让你避开九成的基础错误。提示默认对应 fd 0默认对应 fd 1连2这种写法其实只是从 fd 2 导出数据。理解 fd 概念之后重定向规则会变得非常清晰。2. 重定向实操把数据流精准引到指定位置2.1 输出重定向的正确姿势输出重定向是最常用的操作基本写法就两类# 覆盖写入文件原有内容会被清空 command file.log # 追加写入保留原有内容新内容接在后面 command file.log这两个写法的杀伤力差别很大。会先创建或清空目标文件再写入内容。某次我在测试环境执行du -sh * 2024_size.txt结果那个文件名正好是一个子目录的名字于是一整个目录的统计内容直接被清空重写原本的旧报告没了。后来我的习惯是覆盖式重定向之前先确认文件名是不是真的不存在或者确认可以覆盖尤其是带日期的文件名很容易因为少打一个字符把一个月的数据全冲了。还有一个高频细节重定向的位置。shell 允许你写成 log 21也允许写成21 log但两者含义完全不同。前者把标准错误重定向到标准输出当前指向的地方再把标准输出引到文件最终两者都进文件后者先把标准错误指向当前标准输出多半是终端再把标准输出引到文件最终标准错误还在终端上。所以那句著名的21 要放在最后只是为了方便阅读和避免理解歧义并不是语法强制真正的规则是shell 从左到右处理重定向。我建议把command file 21当成标准模板记住不要轻易调换顺序。2.2 输入重定向的场景输入重定向平时用得比输出少但一旦用到就是关键救场# 从文件读取标准输入 program input.txt # 在这里手动输入多行直到出现 EOF 标记 program EOF line1 line2 EOF # Bash 的 here-string直接喂一段字符串 program 这是一段输入最常见的用途是把文件内容喂给只读 stdin 的程序。比如你写了一个交互式 Python 脚本input()接收数据调试时不想每次都手动敲就可以准备一个 csv 文件然后python script.py data.csv。再比如sort unsorted.txt这种写法虽然很多人习惯用sort unsorted.txt但在某些场景下输入重定向能避免命令语法歧义更稳妥。here-document 在脚本自动化里特别香可以在 shell 脚本里一次性写入多行内容并喂给某个程序甚至可以配合 sudo 执行需要输入密码的交互程序虽然密码这种敏感信息不建议明文写但批量执行自动化任务时here-document 比逐个手动输入可靠得多。是 Bash 扩展适合给程序喂短字符串。比如md5sum hello会输出 hello 加换行的哈希注意它会默认追加换行所以跟echo hello | md5sum结果一致。这个细节有时候会让结果和你预期不一致需要留意。2.3 标准错误重定向与经典组合标准错误单独重定向也有两种# 只把标准错误覆盖写入文件 command 2 error.log # 只把标准错误追加写入文件 command 2 error.log更常用的是组合运用。比如我想把标准输出和标准错误分别存到两个文件方便分开排查command normal.log 2 error.log我最常用的组合是 app.log 21将标准输出与标准错误全部合并进同一个文件。后台跑服务、跑数据任务、跑定时脚本统一日志文件是首选。同时我也常用 /dev/null 21丢弃所有输出——比如执行一个只关心退出码的命令在 crontab 里不想收到任何内容时这会非常干净。注意 Bash 还提供了一种简短写法# 等价于 file 21 command file这种写法简洁但移植性不如 file 21好因为 POSIX sh 不支持。如果你写的脚本需要跑到多种 shell 环境里建议统一使用 file 21这种兼容性最强的写法。2.4 /dev/null 与文件描述符的高级腾挪/dev/null在重定界里扮演黑洞角色。我常跟人讲把不想要的内容送进/dev/null比在脚本里费力去过滤强得多grep -r pattern /some/dir 2/dev/null crontab 命令 /dev/null 21你也可以自定义文件描述符做更精细的控制exec 3 custom.log command 13 21 exec 3-这种操作在复杂脚本里可以把指定文件描述符重定向到日志文件灵活度很高但日常用不了太多。我的建议是先在脑子里搞清 0、1、2 三根基本水管高级 fd 腾挪只是基于同一套规则的扩展。如果一上来就玩31之类的操作很容易在嵌套重定向里迷失。3. 管道用法拆解把命令串成流水线3.1 管道符的底层语义管道符|可以理解为把左边命令的标准输出接到右边命令的标准输入。这在操作系统层面是一项非常朴素的设计前一个进程的输出数据流向不落盘直接进入下一个进程的输入接口。你可能会问这和command temp.txt再执行next temp.txt有什么本质区别答案是效率与并发。管道不会在磁盘上产生中间文件更重要的是只要数据一产生右边的进程就可以立刻开始消费整个链路是并发的左右两个命令可以同时运行。比如top -b | head不需要等 top 全部跑完才让 head 动起来top 一边输出head 一边取前几行。管道也有自己的生活状态如果右边的进程处理得慢左边进程会等待如果左边进程产生大量数据而右边进程已经退出左边进程将收到 SIGPIPE 信号终止掉。这个行为有时候会让你在执行长管道时看到Broken pipe错误事实上这是正常现象不是故障。3.2 最常用的管道组合套路我列出几组我在日常工作中高频使用的管道组合直接照着抄基本就能解决大部分数据处理需求# 实时过滤日志 tail -f app.log | grep ERROR # 统计日志里每个 IP 出现次数并排序 awk {print $1} access.log | sort | uniq -c | sort -rn # 找出文件中最占磁盘空间的目录 du -sh /var/log/* | sort -hr | head -10 # 在项目代码里快速搜索关键词并显示行号 grep -rn TODO src/ | lesstail -f加grep的组合特别适合排查线上问题一边观察日志滚动一边只保留异常内容。awk加sort加uniq加sort的链条是统计类需求的经典流水线提取字段、排序、统计次数、按频率逆序排列。真正用熟之后你会发现这些基础命令唾手可得完全不用借助大而全的数据处理平台。还有一组组合我认为是管道进阶的核心那就是xargsfind . -name *.log | xargs grep OutOfMemoryfind列表通过管道传给xargs作为参数转发给grep省去手动拼参数。这里有个坑如果文件名包含空格xargs默认按空白切分会导致参数错位需要加上-0配合find -print0或改用xargs -d \n。这是我踩过不止一次的坑处理带空格路径的文件时务必留意。3.3 管道与重定向的嵌套技巧管道本身处理的是标准输出但程序的标准错误并不会自动进入管道。想让标准错误也进入右侧程序需要这样写command 21 | grep error这种写法很常用。比如你执行某个脚本既想捕获 stdout 也想捕获 stderr然后一起过滤。在 Bash 里还有一个简写|等价于21 |不过同上为了可移植性我在脚本里倾向于写完整的21。管道也可以和文件重定向混合command 2 error.log | tee output.log这里是标准错误进 error.log标准输出一边通过 tee 保存到 output.log 一边继续显示在终端。这种场景非常适合手工排查既保留运行痕迹又能实时看到进度。如果不想屏幕显示只想让数据同时流向两个文件可以用tee的经典设计甚至两个 tee 串起来实现一鱼三吃。再一个很多人容易忽略的点管道里命令的退出状态默认只看最后一个命令。cmd1 | cmd2如果 cmd1 失败而 cmd2 成功整个管道的退出码是 0。在 shell 脚本里这会掩盖掉真正的问题。处理方式可以在脚本开头设置set -o pipefail让管道中任何一环失败都反映在最终退出码上。我个人强烈建议在所有值得维护的脚本里加上它。4. 踩坑记录重定向与管道的高频问题4.1 为什么日志文件里什么都没写有一次线上定时任务的日志文件是空的命令在 crontab 里写的是run_task task.log。实际原因和开头说的类似输出内容全在 stderr用只捕获了 stdout。解决办法是统一使用 task.log 21让两个输出流都进同一个文件。另一个常见原因是缓冲机制。程序写 log 时默认会做缓冲区比如 Python 的 print 重定向到文件后默认可能进入块缓冲进程退出前数据没有及时 flush。你看到文件是空的但等程序结束后数据可能才刷进去。如果需要实时日志让 Python 脚本用python -u script.py取消缓冲或者在代码里加flushTrue很多后端服务的日志延迟八成就是这个原因。4.2 21 的位置陷阱同样一条命令只是换一下位置结果天差地别# 正常stdout 和 stderr 都进 file command file 21 # 坑只有 stdout 进 filestderr 还是会打到终端 command 21 file原因前面已经提过shell 从左到右处理。第二种写法先让 stderr 指向当前 stdout通常是终端然后再把 stdout 指向 file结果 stderr 并不会跟着转到 file。我见过不少同事在这里栽跟头他们把21 file想当然地理解为把 2 和 1 都弄进文件实际上完全不是。如果非用21 file来实现让 stderr 进文件需要在第二次重定向时也指定 stderr 指向新目标command 21 file 12这种花活我一般不推荐容易把人绕晕。稳妥的方案就是统一用 file 21。4.3 管道与重定向的优先级问题管道与重定向之间的优先级也有讲究。举例说明# 管道先建立把 stdout 连接到 grep 的 stdin再设置 result.txt command 21 result.txt | grep err在这个例子里由于管道先建立command 的 stdout 已经被交付给 grep后面 result.txt不会再捕获 stdout结果只有原来的 stderr 和重定向逻辑相关极其容易出错。实际调试时遇到我明明重定向了为什么没生效的情况多半就是管道优先级引起的混乱。更复杂的情况还会出现在分组命令里{ command1; command2; } log 21用大括号把多条命令包成组一次重定向全部生效。这个技巧在需要把一组命令统一输出到同一份日志时非常实用比在每条命令后都追加重定向干净得多。4.4 缓冲、退出码与并发隐患标准输出在终端和文件之间的缓冲策略不同。终端通常是行缓冲打印一行就刷新一行管道和文件通常是块缓冲4KB 或更大。所以同样的程序直接终端里运行有输出重定向到文件却迟迟不显示不是程序出 bug 了只是数据还在缓冲区里。排查手段是使用stdbuf -oL强制行缓冲例如stdbuf -oL python -u realtime_log.py | tee -a run.log另一个隐患是管道退出码掩盖问题我在 3.3 节已经提到。脚本里如果既有重定向又有管道还需要特别留意管道的 sigpipe 现象。比如yes | head -1这条命令head读完一行就退出yes随后收到 SIGPIPE 被系统终止但你不会在终端里看到报错这很正常。真正要警惕的是在循环中使用管道读取程序输出时一旦子进程被 SIGPIPE 杀掉可能导致资源未释放大数据量处理时需要格外小心。4.5 个人避坑清单最后我给出一份我从实际运维中积累的检查清单基本覆盖上面所有内容写日志一律使用 app.log 21不要只写否则 stderr 丢失。覆盖式重定向之前先确认文件名不要为了省事而清空错误文件。在脚本开头慎重使用set -e时记得考虑管道退出码问题要么加set -o pipefail要么避免裸管道。处理文件名带空格时优先find ... -print0 | xargs -0 ...而不是裸 xargs。要实时日志时确认待重定向程序的缓冲模式必要时用stdbuf、python -u或代码内 flush。组合命令需要统一输出时用{}分组加一次重定向而不是每条命令单独做。21与的顺序要形成肌肉记忆先改 stdout再让 stderr 跟着走。如果你在脚本中看到|或这种简写能兼容 Bash 就用要跨平台就改成21 |和 file 21。这几年下来我对这三个标准流和重定向管道的体会是掌握它们不是记住几个符号而是建立一套关于数据从哪里来、到哪里去的心智模型。一旦你习惯用输入输出流的视角去理解命令行和脚本以后排查问题会快得多写脚本也更有底气。建议遇到任何看不懂的命令组合都先拆一拆左边命令的哪个输出流被接到了哪里右边命令的哪个输入流在接收数据。搞明白这条链路重定向和管道基本就没秘密了。
阅读完成 · 觉得有帮助?