Bash shell 获取使用路径的正确方法

当前路径

当前路径,听上去好像很明确,很好理解。不就是“当前路径”嘛?”./” 不就表示当前目录吗、”./file_name” 不就表示当前目录文件吗、”../file2”不就表示上级目录的文件么… 这个还需要发篇文章来讲解?
答:当然需要。并且我之所以说它迷是有原因的,请看下面的例子。

创建这样存放结构的三个文件:

.
          ├── dir
          │   ├── s2.sh
          │   └── text
          └── s1.sh
          

s1.sh 文件内容:

#!/usr/bin/env bash
          
          ./dir/s2.sh
          

s2.sh 文件内容:

#!/usr/bin/env bash
          
          cat ./text
          

text 文件内容:

我是一个文本文件,我是内容!
          
          

注意:记得 s1.sh 和 s2.sh 这两个 shell 脚本文件要加 x 权限,不然没法直接执行。

如果你到 dir 目录下,单独执行 s2.sh ,那么结果一定是这样:

hentioe@CL-BUDGIE:/code/dir$ ./s2.sh
          我是一个文本文件,我是内容!
          

成功执行了 s2.sh ,将 text 内容输出出来。当然我们的目的不是如此。可以看到 s1.sh 执行了 dir/s2.sh ,我们的目的是执行 s1.sh 脚本,然后间接的执行 s2.sh 得到 text 的内容,这才是创建这样三个文件的目的。

当我们在 s1.sh 所在目录执行 ./s1 时… 看到的却是:

hentioe@CL-BUDGIE:/code$ ./s1.sh
          cat: ./text: No such file or directory
          

为什么会提示找不到 text 文件呢?既然通过 s2 脚本打印的 text 文件,那么和 s2.sh 在同一个目录并且使用 ./text 作为路径的 text 文件应该是找得到的啊?

所以我说,当前路径这个东西其实有点迷。并且也没有那么容易使用。如果你真的要在发布的软件包的脚本中引用自带的文件,千万不要直接使用 ./ 或者 ../等作为当前路径参考,那是大错特错的。

$0 参数

在 Bash shell 中,位置参数是从 $1 开始的,但其实还存在一些隐藏的特殊参数和变量。例如 $0 表示所执行的脚本名称,当然它并不表示“文件名”,它更像是将执行所用的路径作为参数传递进来了。

创建 s3.sh 文件:

#!/usr/bin/env bash
          
          echo "Usage: $0"
          

调用方式1:

hentioe@CL-BUDGIE:/code$ ./s3.sh
          Usage: ./s3.sh
          

调用方式2:

hentioe@CL-BUDGIE:/code$ /code/s3.sh
          Usage: /code/s3.sh
          

调用方式3:

hentioe@CL-BUDGIE:/codel/dir$ ../s3.sh
          Usage: ../s3.sh
          

无论是相对的,还是绝对的,这个参数都指向执行文件。当然,从第三个调用就能看出来,相对路径时,是相对于当前执行(命令)目录,而非执行文件所在目录,这在程序中一般叫做 workdir(工作目录)。也就是说,s1 脚本调用时之所以找不到 text 文件是因为 workdir 下没有 text 这个文件,而 s2.sh 是通过 s1.sh 调用的,所以 workdir 并不是 s2.sh 的目录路径。即便是直接调用 s2.sh ,只要并非 ./s2.sh 调用,workdir 都不会在 s2 脚本文件的所在目录,都会出现文件找不到。这就是原因所在,以及在脚本中直接使用 ./ 和 ../ 的危害。

获取绝对路径

我们知道,Linux 中的相对路径 . 或者 .. 都是链接(但并非符号链接),并且是一种特殊的硬链接,之所以特殊是因为给目录创建硬链接是不允许的,至于为什么不允许跟本文讨论的主题无关(以后有机会可能会发文讲述 Linux 中的链接)。

当我们使用 readlink 获得一个包含 . 或 .. 链接的“规范文件名”(可以理解为包含文件名的绝对路径)时:

hentioe@CL-BUDGIE:/code/$ readlink -f .
          /code
          
hentioe@CL-BUDGIE:/code/$ readlink -f ./dir/text
          /code/dir/text
          

会发现我们得到了想要的绝对路径。同理,在 shell 脚本中 readlink -f “$0” 即可将 $0 这个可能是相对路径的值转换为绝对路径(创建 s4.sh):

#!/usr/bin/env bash
          
          echo "Usage: $0"
          readlink -nf "$0"; echo
          

调用结果:

hentioe@CL-BUDGIE:/code$ ./s4.sh
          Usage: ./s4.sh
          /code/s4.sh
          

获取目录路径

很多时候,我们得到脚本自己的绝对路径是没用的。因为我们需要一个目录作为“参考”,读取、调用或者操作基于参考目录路径的其它文件。所以我们还需要得到文件的目录路径。

这时候,我们需要用到 dirname 命令。它的作用是从一个包含文件名的路径中“剥离”出目录部分,从而得到文件的所在目录的路径。因为它的功能和用法非常简单和容易理解,就不举例了。
如果我们将 readlink 和 dirname 协作用于 $0 参数上,便可得到了执行脚本的所在目录的绝对路径(s5.sh):

#!/usr/bin/env bash
          
          echo "Usage: $0"
          full_filename="$(readlink -nf "$0")"
          full_homepath="$(dirname "$full_filename")"
          echo "Parent dir: $full_homepath"
          

调用结果:

hentioe@CL-BUDGIE:/code$ ./s5.sh
          Usage: ./s5.sh
          Parent dir: /code
          

改造 S2 脚本

文件结构不变,改造 s2.sh 脚本内容;

#!/usr/bin/env bash
          
          # 获取目录路径
          full_homepath="$(dirname "$(readlink -nf "$0")")"
          # 使用绝对路径操作文件
          cat "$full_homepath/text"
          

此时,无论通过怎样的方式调用 s2.sh ,通过相对/绝对路径调用也好,通过 s1.sh 或者其它脚本调用也好,text 文件都能被找到。因为你总能获取到正确的“参考目录”,即:s2.sh 文件的所在目录的绝对路径。

最后

如果你看到这篇文章,请千万记住发布软件时,在软件的 shell 脚本中不要用 . 或者 .. 操作文件了。你甚至可以要求用户配置环境变量,然后脚本中直接读取环境变量中的绝对路径,但是千万不要再使用相对路径了。因为不仅仅坑了自己,也给用户造成了麻烦。