BASH的文件名扩展挖的一个坑

124 阅读3分钟

问题

这两天看到一个问题,整理之后是这样:

“对于一般情况,在 bash 里执行find / -name test*,会得到预期结果吗?”

这里有坑。如果一眼就看出来了,基础扎实,可以忽略后文了。

过程

执行一把,看看结果:

bash-4.2# find / -name test*
/usr/bin/test
/sys/devices/virtual/net/ip6tnl0/testing
/sys/devices/virtual/net/eth0/testing
/sys/devices/virtual/net/lo/testing
/sys/devices/virtual/net/tunl0/testing
/sys/fs/ext4/features/test_dummy_encryption_v2

似乎没问题,真的没问题吗?加点料再执行一把:

bash-4.2# touch test1; find / -name test*
/test1

发现问题了,在当前目录下加了一个文件 test1,结果变得奇怪了。那我删了:

bash-4.2# rm test1; find / -name test*
/usr/bin/test
/sys/devices/virtual/net/ip6tnl0/testing
/sys/devices/virtual/net/eth0/testing
/sys/devices/virtual/net/lo/testing
/sys/devices/virtual/net/tunl0/testing
/sys/fs/ext4/features/test_dummy_encryption_v2

又似乎没问题了。于是发现了不得了的东西,当前目录下的文件会影响 find 的结果。

是不是 find 的问题呢?留意到 find 的说明:

-name pattern
              Base of file name matches shell pattern ...(给删掉了,太长不看)  Don't
              forget to enclose the pattern in quotes in order to
              protect it from expansion by the shell.

真相了,原来是 bash 的问题。

按照说明,加料重试一把。

bash-4.2# touch test1; find / -name 'test*'
/usr/bin/test
/sys/devices/virtual/net/ip6tnl0/testing
/sys/devices/virtual/net/eth0/testing
/sys/devices/virtual/net/lo/testing
/sys/devices/virtual/net/tunl0/testing
/sys/fs/ext4/features/test_dummy_encryption_v2
/test1

现在得到了预期的结果,但是还留了一个疑问,为什么当前目录下的文件会影响输出的结果?

结论

前面知道了,find 的-name正确用法是使用引号防止被 shell 扩展。了解这个扩展,也就能回答上面这个疑问了。bash 的扩展有许多种,跟这有关的是文件名扩展。

文件名扩展(Filename Expansion)是指 bash 会把没有被引号引起来的且和*, ?, [组成的单词当成一个匹配模式,并且会把这个匹配模式替换成匹配结果(按字母顺序排序的文件名列表)。

符号*, ?, [的含义是:

  1. *匹配任何字符串,包括空字符串;
  2. ?匹配任何单个字符;
  3. [...]匹配任何一个封闭的字符,或者指定字符类。

举个例子:

bash-4.2# mkdir -p ~/tmp/test; cd ~/tmp/test; touch a.txt b.txt b.exe
bash-4.2# echo *txt        #1
a.txt b.txt
bash-4.2# echo b*          #2
b.exe b.txt
bash-4.2# echo n*          #3
n*

这个例子准备了一个目录,只有三个文件,分别是 a.txt, b.txt 和 b.exe。

  1. 执行echo *txt的时候,bash 认为*txt是一个匹配模式,于是把它替换成匹配结果a.txt b.txt,这行命令变成echo a.txt b.txt,输出结果就是a.txt b.txt了;
  2. 执行echo b*的时候,和 1 同理。命令变成echo b.exe b.txt,输出结果就是b.exe b.txt了;
  3. 执行echo n*的时候,bash 认为n*是一个匹配模式,也想把它替换成匹配结果,由于没有匹配项,这个操作失败了,因此 bash 保留这个单词,这行命令没有改变,输出结果就是n*了。

上一段里的 3 正好可以解释上一节留下来的问题(为什么当前目录下的文件会影响输出的结果?)。对于这行命令find / -name test*:

  1. 如果当前目录没有test*的匹配项,bash 的文件名扩展会失败,这行命令依然是find / -name test*
  2. 如果当前目录有test*的匹配项(例子里是 test1),bash 的文件名扩展会成功,这行命令其实是find / -name test1

bash 有几种方法控制文件名扩展失败时的行为:

  1. 使用shopt -s nullglob,失败时删除这个单词;
  2. 使用shopt -s failglob,失败时报错且不再执行当前命令。

最后就是避免文件名扩展的几种方法了:

  1. 使用引号(单引号'或者双引号")引起来,比如'*txt'"*txt"
  2. 使用反斜杠\转义,比如\*txt
  3. 使用set -f临时关闭文件名扩展。