本节摘要:上一节讲了「Shell 替你执行命令」,这一节要拆开命令本身,看它内部是怎么和外界交换数据的。答案就是三个标准流:标准输入(stdin)、标准输出(stdout)、标准错误(stderr)。每个命令一启动,Linux 就默认给它开好这三条通道——0 号读、1 号写正常结果、2 号写报错信息。理解了这三个流,你就能解释为什么
>能把输出存进文件、为什么管道能把命令串起来、为什么2>/dev/null能把报错吞掉。本节要把重定向(>>><2>2>&1&>)讲透,它是下一节管道的物理基础——管道本质上就是「把上一个命令的 stdout 接到下一个命令的 stdin」,不先把流理清,管道永远是黑盒。
内容来源:原项目「Linux 命令大全」及 Linux 基础整理,精选并套用体系化模板。
阅读完本节,你应当能够:
> 把命令输出写入文件、用 >> 追加、用 < 从文件读入,并解释它们各自的覆盖/追加语义。2> 单独重定向错误、用 2>&1 把错误并到正常输出、用 &> 一次性重定向两者。/dev/null 这个「黑洞设备」在静音报错时的用法。很多人学重定向,是死记「> 是输出到文件,2> 是错误到文件」,却不理解背后的设计。真正的关键,是 Linux 在创建每个进程时,默认会为它打开三个流,它们有固定的编号(叫「文件描述符」):
| 编号 | 名称 | 常用缩写 | 默认连到 |
|---|---|---|---|
| 0 | 标准输入 | stdin | 终端键盘(交互时)/ 启动它的父进程 |
| 1 | 标准输出 | stdout | 终端屏幕 |
| 2 | 标准错误 | stderr | 终端屏幕 |
关键概念:这三个流是「命令眼里看到的接口」,命令自己根本不知道它最后是输出到屏幕还是文件——它只管往「1 号流」写数据。至于 1 号流最终接哪里,由 Shell 在启动命令前替它接好。这就是重定向能成立的全部原理:Shell 在启动命令之前,把它的某个流「重新指」到别处。
为什么要分成「正常输出」和「错误输出」两条?这是 Unix 最优雅的设计之一:让结果和报错走不同的管道。这样你用 > 把结果存进文件时,报错仍然会显示在屏幕上,不会被一起写进文件污染数据。如果两者混在一起,「拿数据」和「看错误」就只能二选一,管道里排查问题也会困难十倍。
> 与 >>最基础的重定向,是把命令的标准输出(1 号流)接到一个文件上。
ls > list.txt # 把 ls 的输出写入 list.txt(覆盖:原内容被清空) ls >> list.txt # 追加:在文件末尾继续写,不动旧内容 date > now.txt # 把当前时间存进 now.txt echo "done" >> log.txt # 往日志追加一行
> 与 >> 的唯一区别是语义:> 是「覆盖」,先清空再写;>> 是「追加」,在末尾续写。默认重定向的是 1 号流(标准输出),所以 > 其实是 1> 的简写,只是 1 可以省略。
⚠️ 注意:
>是立刻清空文件,即使后面的命令失败了!比如ls /不存在 > out.txt,虽然ls会报错,但out.txt已经在命令启动前被清空了。这是「Shell 先接线、再启动命令」的副作用,排错时经常踩到。
# 验证:一条会失败的命令,文件照样被清空 echo "重要数据" > out.txt # 先放点内容 ls /这个目录不存在 > out.txt # ls 会失败,但 out.txt 已被清空 cat out.txt # 空文件——原来的"重要数据"没了
一个常见又安全的写法,是用 set -o noclobber(或 set -C)让 > 在文件已存在时拒绝覆盖,改用 >| 才能强制覆盖。生产脚本里值得打开。
<< 是反过来的——把一个文件的内容接到命令的标准输入(0 号流)上。命令从 stdin 读数据时,它读到的就是这个文件的内容。
sort < unsorted.txt # 让 sort 从 unsorted.txt 读输入,排序后输出到屏幕 wc -l < big.txt # 统计 big.txt 的行数(注意:这里 wc 只读不改) mail user < body.txt # 把 body.txt 的内容当作邮件正文发送
这里有个细节需要想清楚:sort < unsorted.txt 和 sort unsorted.txt 看起来效果差不多,但本质不同。前者是「Shell 把文件喂给 sort 的 stdin」,sort 根本不知道文件名;后者是「sort 自己打开文件读」。区别在:
wc、tr、sort 在不传文件时),这时 < 是必需的。wc file 会显示 5 file,wc < file 只显示 5)。💡 技巧:很多命令既能「传文件名参数」也能「从 stdin 读」。约定:不传文件名时,命令就从 stdin 读。这个约定是管道能成立的根基——下游命令永远从 stdin 拿数据,所以才能被任意拼接。
2>错误信息走的是 2 号流(标准错误),默认也连到屏幕。要把错误单独存下来,用 2>。
ls /不存在 /tmp 2> err.log # 正常结果进屏幕,报错进 err.log ls /不存在 /tmp > out.log 2> err.log # 正常结果进 out.log,报错进 err.log grep x big.txt 2> /dev/null # 把报错丢进黑洞(见下),只看正常输出
编号写在尖括号前面,中间不能有空格:2> 是一个整体,写成 2 > 就变成了「参数 2」加「重定向」,语义完全变了。同理 1> 是显式重定向标准输出(和 > 等价)。
2>&1有时你不关心「正常」和「报错」要分开,只想把所有输出都丢进同一个文件。这时用 2>&1,意思是「把 2 号流复制一份,接到 1 号流当前所指的地方」。
ls /不存在 /tmp > all.log 2>&1 # 正常和报错都进 all.log
这里有个最容易踩的顺序坑,务必记住:
ls > all.log 2>&1 # 对! 先把 1 指向 all.log,再把 2 复制成"和 1 一样" → 都进 all.log ls 2>&1 > all.log # 错! 先把 2 复制成"和 1 一样"(此时 1 还指向屏幕,所以 2 也指向屏幕),再把 1 指向 all.log → 报错仍然漏到屏幕
⚠️ 注意:重定向的解析顺序是「从左到右」,每一步都会改变后续
&N的指向。所以2>&1必须写在> 文件之后,才能让错误跟着正常输出一起进文件。写反了是新手最常见的坑之一。
因为 > out 2>&1 这个组合太常用,Bash 提供了简写 &>(以及追加版 &>>):
ls &> all.log # 等价于 > all.log 2>&1 ls &>> all.log # 等价于 >> all.log 2>&1
💡 技巧:写脚本时推荐用
> out 2>&1这种「经典写法」,而不是&>,因为前者兼容所有 POSIX Shell(sh、dash都认),&>是 Bash 扩展。在#!/bin/sh的脚本里用&>会报错。
/dev/null/dev/null 是一个特殊文件,写进去的数据会被立刻丢弃,读它立刻返回文件结束。它最常用的场景是「丢弃不想要的输出」。
grep x big.txt > /dev/null # 只想看退出码($?),不关心内容 grep x big.txt 2> /dev/null # 屏蔽报错(比如权限不足的提示) grep x big.txt > /dev/null 2>&1 # 屏蔽一切输出,只看退出码判断"有没有匹配到" command &> /dev/null # 一刀切丢弃所有输出
> /dev/null 2>&1 是排查和脚本里的「静音组合拳」:它让你能拿到命令的退出码(下一节细讲),却不在屏幕上留任何痕迹。这种用法在判断「文件里有没有某个字符串」时极其常见——grep -q 就是对这个模式的封装。
下面是一个接近真实的场景:扫描一批目录,把能访问的列表存进 ok.txt,把报错的存进 err.txt。
# 三流分流:正常进 ok.txt,错误进 err.txt,屏幕上啥都不留 for d in /etc /var /不存在 /root; do ls "$d" >> ok.txt 2>> err.txt done
注意这里同时用了 >> 和 2>>——正常输出追加到 ok.txt,错误追加到 err.txt,两条流各走各的。如果哪天 ok.txt 里突然混进了报错,你立刻就知道是 2>> 漏写了,导致报错跑了屏幕(而不是文件)。这就是「流分离」在排错时的价值。
2>&1 写在 > 前面最经典的坑,前面提过,再强调一次:
cmd 2>&1 > out.log # 错! 错误仍然漏到屏幕 cmd > out.log 2>&1 # 对! 错误也进 out.log cmd &> out.log # 对! Bash 简写
记忆口诀:「想让错误跟着输出走,2>&1 必须写在输出重定向之后」。
> 会无声清空文件echo "重要内容" > data # 想写入 ls /xxx > data # ls 失败了,但 data 已经被清空!
重定向是 Shell 在命令启动之前完成的动作——一旦写 > data,Shell 就先把 data 截断成 0 字节,然后才去跑命令。命令是否成功、是否真的有输出,都不影响「文件被清空」这个事实。
规避:要保护已有数据,用 >>(追加)或打开 set -o noclobber。
ls 2 > err.log # 错! Shell 把 "2" 当成 ls 的参数, "> err.log" 重定向标准输出 ls 2> err.log # 对! "2>" 是一个整体
2>、1>、&> 中间都不能有空格。新手常犯这个错,还以为是命令本身有问题。
> 能重定向错误ls /不存在 /tmp > out.log # 报错会显示在屏幕,out.log 里只有 /tmp 的列表 ls /不存在 /tmp 2> out.log # 这样才把错误存进文件(但正常输出又跑了屏幕)
默认的 > 只搬走标准输出(1 号流),不动错误(2 号流)。错误会原样显示在屏幕上。要同时搬走,用 &> 或 > out 2>&1。
很多命令(比如 cat 不带参数、grep 不带文件)会「卡住等你输入」——其实它们在等 stdin。新手敲完命令看到光标闪,以为终端坏了,其实是命令在等你从键盘喂它数据。按 Ctrl+D(发送文件结束符 EOF)即可结束输入。
cat # 不带参数,会等你输入;按 Ctrl+D 结束 grep error # 同理,等你输入,逐行过滤
> 覆盖、>> 追加,默认作用于 1 号流;命令失败也会先清空文件,因为重定向发生在命令启动之前。< 从文件读 stdin;很多命令不传文件名时就从 stdin 读——这是管道能成立的前提。2> 单独重定向错误,2>&1 把错误并到正常输出(必须写在 > 文件 之后),&> 是 Bash 的简写。/dev/null 是黑洞,> /dev/null 2>&1 是静音组合拳,常配合退出码做判断。2>&1 顺序写反、> 无声清空文件、2 > 中间多空格、以为 > 也能搬走错误。下一节讲管道。如果说本节是「把命令的流接到文件」,那管道就是「把上一个命令的 stdout 直接接到下一个命令的 stdin」——同一套机制,只是接线两头从「文件」换成了「另一个命令」。一旦理解了本节的流,管道对你将毫无神秘可言。