重定向是 Shell 里最容易绕晕的一块。>、>>、2>&1、&>、exec 3>这些写法,看着都是把输出导到文件,差别却很大。搞不清文件描述符,脚本一上线就出问题:日志文件是空的、错误全跑屏上、该抓的错误没抓到。这一篇把文件描述符和重定向一次讲透。
一、三个标准文件描述符
每个进程一启动,内核就帮它打开三个文件描述符(file descriptor,简称 FD):
| FD | 名字 | 方向 | 默认指向 |
|---|---|---|---|
0 | stdin 标准输入 | 读 | 键盘 |
1 | stdout 标准输出 | 写 | 终端 |
2 | stderr 标准错误 | 写 | 终端 |
程序里printf出来的正常内容走 1,报错信息走 2。很多人以为 stderr 是"错误的输出流",其实它就是第二条输出通道,专门放诊断信息——这样你把正常结果重定向到文件时,报错还能留在屏幕上,不会被一起埋掉。
二、基本重定向
输出重定向
| 写法 | 作用 |
|---|---|
cmd > file | 标准输出覆盖写进 file(截断原有内容) |
cmd >> file | 追加 |
cmd 2> file | 只把标准错误写进 file |
cmd &> file | 标准输出和标准错误都写进 file |
cmd &>> file | 两者都追加 |
>前面省略了 1,> file等价于1> file。
输入重定向
| 写法 | 作用 |
|---|---|
cmd < file | 把 file 当标准输入 |
cmd << EOF ... EOF | heredoc,把多行内容喂进标准输入 |
cmd <<< "字符串" | here-string,把一个字符串喂进去 |
heredoc 实例,直接生成一个配置文件:
cat>config.txt<<'EOF' host=127.0.0.1 port=8080 EOF预期config.txt内容:
host=127.0.0.1 port=8080EOF加了引号,里面的$不会被展开;不加引号,host=$HOST这行会先做变量替换。写配置文件时这个区别经常踩。结束标记 EOF 必须顶格写,前后不能有空格,否则 shell 认不出结束位置。
here-string 只喂一小段,比 heredoc 短:
grep-o'127[0-9.]*'<<<"server ip 192.168.1.1 and 127.0.0.1"预期输出:
127.0.0.1不用先建临时文件,脚本里做小测试很顺手。
三、2>&1:顺序是核心
这是整个重定向里最容易写错的一句。
command>file2>&1意思是:先把 1 指向 file,再让 2 复制 1 的指向。结果 1 和 2 都写到 file。
反过来写:
command2>&1>file意思是:先让 2 复制当时 1 的指向(终端),再把 1 改指向 file。结果错误还打在屏幕上,file 里只有正常输出。
记住这条就够:>&n是"复制现在 n 指向哪里",从左到右算。想把错误也收进文件,2>&1必须放在> file后面。
| 写法 | stdout 去哪 | stderr 去哪 |
|---|---|---|
cmd > f 2>&1 | f | f |
cmd 2>&1 > f | f | 终端 |
cmd > f 2> f2 | f | f2 |
cmd &> f | f | f |
四、>&n、n>&- 与 exec:自己玩文件描述符
Shell 允许你借用 3、4、5 这些高位 FD。exec不带命令时只改当前 shell 的重定向,不启动新进程。
exec3>/tmp/debug.logecho"调试信息">&3exec3>&-3>打开一个 FD,>&3往这个 FD 写,3>&-关闭。读方向一样:
exec4</etc/hostsreadline<&4echo"$line"exec4<&-这种写法在脚本里常见的用途:先把原来的 1 备份出来,干一堆重定向的活,最后恢复。
exec3>&1# 备份原 stdout 到 3exec>/tmp/all.log# 之后所有 echo 都进日志echo"脚本开始"exec>&3# 恢复 stdoutexec3>&-五、/dev/null 与 tee
/dev/null是个黑洞,写进去什么都不剩,读出来永远是空。丢弃输出用它:
rm-f*.tmp2>/dev/null这样没有匹配文件时的报错不会刷屏。
tee一分为二,既打到屏幕又写文件:
make2>&1|teebuild.log编译日志既能实时看到,又留一份存档。想追加到已有文件加-a。
六、进程替换:把输出伪装成文件
有些命令只接受"文件路径"当参数,不读标准输入,比如diff、comm。进程替换<(...)把一个命令的输出挂成/dev/fd/63这样的临时路径,骗命令以为它在看文件:
diff<(ls/var/log)<(ls/tmp)两个ls的结果直接比,不用先落临时文件。批量对比两份配置文件的差异,这招最省事。
反向>(...)把输入伪装成文件路径,少见,知道有这东西就行。
一个常用组合:脚本既打屏又落盘
#!/bin/bashexec>>(tee/tmp/run.log)2>&1echo"开始"ls/no-such-direcho"结束"预期输出:
开始 ls: cannot access '/no-such-dir': No such file or directory 结束屏幕上能看到全过程,/tmp/run.log里也留一份。第一行的意思:把标准输出接到tee的输入,再用2>&1把错误也送进去。这种写法比每个命令后面都加| tee干净。
七、⚠️ 常见错误
> file 2>&1写成2>&1 > file,错误全跑屏上,日志里只剩正常输出。- heredoc 结束标记 EOF 顶格、前后无空格,否则认不出。
- 重定向是每个命令独立计算的,
cmd1 > out | cmd2里 cmd2 读不到 out 文件内容,它读的是 cmd1 的标准输出。 cmd > file是覆盖写,脚本中断重跑会把上次日志清掉;要保留历史用>>。- 两个独立的
>a 2>a(不带&1)写同一个文件,stdout 和 stderr 各开一份,输出会互相覆盖交错。
常见写法速查:
| 目的 | 写法 |
|---|---|
| 丢弃所有输出 | cmd &> /dev/null |
| 只丢错误,正常输出还要 | cmd 2> /dev/null |
| 正常和错误都进日志 | cmd >> app.log 2>&1 |
| 错误进日志,正常打屏 | cmd 2>> app.log |
| 读文件当输入 | cmd < file |
| 同时打屏和写文件 | `cmd |
八、知识扩展:文件描述符怎么被子进程继承
fork 出来的子进程,会把父进程打开的文件描述符表整个复制一份。这就是重定向能工作的底层原因:你在 shell 里exec 3>file,再./script.sh,脚本里的echo >&3照样能写进去——它继承了 3。
这条规则反过来解释了2>&1的顺序。重定向在命令启动前由 shell 从左到右准备好,每个>&n都是在当时的快照上复制。2>&1 >file里 2 先复制的是"1 当前指向终端"这个事实,之后 1 再改指文件,2 不会跟着变。
文件描述符在系统里只是个小整数,内核用一张"FD 号 → 打开文件描述(file description)"的表来记。>&m复制的不是那个整数,是那条 file description 的引用——两个 FD 指向同一个打开文件,读写位置共享。所以cmd > a 2>&1里标准输出和错误交错写不会互相覆盖,因为它们共用同一个文件偏移量。这也是为什么两个独立>a 2>a时输出会打架:两个打开的文件描述各自维护偏移,都截断、都从 0 写,内容自然互相踩。