第 1 章 · 02 标准流与重定向


第 1 章 · 02 标准流与重定向

本节摘要:上一节讲了「Shell 替你执行命令」,这一节要拆开命令本身,看它内部是怎么和外界交换数据的。答案就是三个标准流:标准输入(stdin)、标准输出(stdout)、标准错误(stderr)。每个命令一启动,Linux 就默认给它开好这三条通道——0 号读、1 号写正常结果、2 号写报错信息。理解了这三个流,你就能解释为什么 > 能把输出存进文件、为什么管道能把命令串起来、为什么 2>/dev/null 能把报错吞掉。本节要把重定向(> >> < 2> 2>&1 &>)讲透,它是下一节管道的物理基础——管道本质上就是「把上一个命令的 stdout 接到下一个命令的 stdin」,不先把流理清,管道永远是黑盒。

内容来源:原项目「Linux 命令大全」及 Linux 基础整理,精选并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 说出每个命令启动时默认拥有的三个标准流(标准输入、标准输出、标准错误)及其编号(0、1、2)。
  2. 用 > 把命令输出写入文件、用 >> 追加、用 < 从文件读入,并解释它们各自的覆盖/追加语义。
  3. 用 2> 单独重定向错误、用 2>&1 把错误并到正常输出、用 &> 一次性重定向两者。
  4. 解释为什么「重定向写文件」和「管道接命令」是同一套机制(都是给流换「接线」)。
  5. 识别 /dev/null 这个「黑洞设备」在静音报错时的用法。
  6. 在遇到「输出没进文件」「错误漏到屏幕」时,根据流的编号定位问题。

一、设计动机:为什么命令天生有三个流

很多人学重定向,是死记「> 是输出到文件,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 自己打开文件读」。区别在:

  • 有些命令只能从 stdin 读(比如 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>> 漏写了,导致报错跑了屏幕(而不是文件)。这就是「流分离」在排错时的价值。

三、踩坑与排错

坑 1:把 2>&1 写在 > 前面

最经典的坑,前面提过,再强调一次:

cmd 2>&1 > out.log # 错! 错误仍然漏到屏幕 cmd > out.log 2>&1 # 对! 错误也进 out.log cmd &> out.log # 对! Bash 简写 ​

记忆口诀:「想让错误跟着输出走,2>&1 必须写在输出重定向之后」。

坑 2:> 会无声清空文件

echo "重要内容" > data # 想写入 ls /xxx > data # ls 失败了,但 data 已经被清空! ​

重定向是 Shell 在命令启动之前完成的动作——一旦写 > data,Shell 就先把 data 截断成 0 字节,然后才去跑命令。命令是否成功、是否真的有输出,都不影响「文件被清空」这个事实。

规避:要保护已有数据,用 >>(追加)或打开 set -o noclobber。

坑 3:把流编号和重定向符之间加了空格

ls 2 > err.log # 错! Shell 把 "2" 当成 ls 的参数, "> err.log" 重定向标准输出 ls 2> err.log # 对! "2>" 是一个整体 ​

2>、1>、&> 中间都不能有空格。新手常犯这个错,还以为是命令本身有问题。

坑 4:以为 > 能重定向错误

ls /不存在 /tmp > out.log # 报错会显示在屏幕,out.log 里只有 /tmp 的列表 ls /不存在 /tmp 2> out.log # 这样才把错误存进文件(但正常输出又跑了屏幕) ​

默认的 > 只搬走标准输出(1 号流),不动错误(2 号流)。错误会原样显示在屏幕上。要同时搬走,用 &> 或 > out 2>&1。

坑 5:在交互终端看不出 stdin 的存在

很多命令(比如 cat 不带参数、grep 不带文件)会「卡住等你输入」——其实它们在等 stdin。新手敲完命令看到光标闪,以为终端坏了,其实是命令在等你从键盘喂它数据。按 Ctrl+D(发送文件结束符 EOF)即可结束输入。

cat # 不带参数,会等你输入;按 Ctrl+D 结束 grep error # 同理,等你输入,逐行过滤 ​

本节要点回顾

  1. 每个命令天生有三个流:标准输入(0)、标准输出(1)、标准错误(2),默认分别连键盘、屏幕、屏幕。
  2. > 覆盖、>> 追加,默认作用于 1 号流;命令失败也会先清空文件,因为重定向发生在命令启动之前。
  3. < 从文件读 stdin;很多命令不传文件名时就从 stdin 读——这是管道能成立的前提。
  4. 2> 单独重定向错误,2>&1 把错误并到正常输出(必须写在 > 文件 之后),&> 是 Bash 的简写。
  5. /dev/null 是黑洞,> /dev/null 2>&1 是静音组合拳,常配合退出码做判断。
  6. 正常输出与错误走不同流是 Unix 的优雅设计,让你能「拿数据」和「看错误」两不耽误。
  7. 常见坑:2>&1 顺序写反、> 无声清空文件、2 > 中间多空格、以为 > 也能搬走错误。

下一节讲管道。如果说本节是「把命令的流接到文件」,那管道就是「把上一个命令的 stdout 直接接到下一个命令的 stdin」——同一套机制,只是接线两头从「文件」换成了「另一个命令」。一旦理解了本节的流,管道对你将毫无神秘可言。


作者与出处
原作者: 灏天文库
来源:jaywcjlove
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U