拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shell重定向与文件描述符高级用法详解

Shell重定向与文件描述符高级用法详解

重定向是 Shell 里最容易绕晕的一块。>、>>、2>&1、&>、exec 3>这些写法,看着都是把输出导到文件,差别却很大。搞不清文件描述符,脚本一上线就出问题:日志文件是空的、错误全跑屏上、该抓的错误没抓到。这一篇把文件描述符和重定向一次讲透。

一、三个标准文件描述符

每个进程一启动,内核就帮它打开三个文件描述符(file descriptor,简称 FD):

FD名字方向默认指向
0stdin 标准输入读键盘
1stdout 标准输出写终端
2stderr 标准错误写终端

程序里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 ... EOFheredoc,把多行内容喂进标准输入
cmd <<< "字符串"here-string,把一个字符串喂进去

heredoc 实例,直接生成一个配置文件:

cat>config.txt<<'EOF' host=127.0.0.1 port=8080 EOF

预期config.txt内容:

host=127.0.0.1 port=8080

EOF加了引号,里面的$不会被展开;不加引号,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>&1ff
cmd 2>&1 > ff终端
cmd > f 2> f2ff2
cmd &> fff

四、>&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干净。

七、⚠️ 常见错误

  1. > file 2>&1写成2>&1 > file,错误全跑屏上,日志里只剩正常输出。
  2. heredoc 结束标记 EOF 顶格、前后无空格,否则认不出。
  3. 重定向是每个命令独立计算的,cmd1 > out | cmd2里 cmd2 读不到 out 文件内容,它读的是 cmd1 的标准输出。
  4. cmd > file是覆盖写,脚本中断重跑会把上次日志清掉;要保留历史用>>。
  5. 两个独立的>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 写,内容自然互相踩。

返回列表