正确获取shell脚本所在目录

1. why,为什么我们要获取脚本所在的目录

编写脚本时,经常需要知道脚本所在目录的位置,以便引用其他的资源或调用其他的脚本。因为这些资源的目录位置相对于当前脚本的位置是固定的。有同学就会问,既然它们的相对位置是固定的,那为什么我们不直接使用相对路径呢?因为你并不一定是在脚本所有的目录去运行脚本的!举个例子:
有个工程的目录结构如下:

1
2
3
4
5
release
-script
-ftp.sh
-ftp.conf
-release.sh

现在,我需要在 release 目录下执行 release.sh 脚本。release.sh 脚本会调用同级目录下的 script/ftp.sh 去做一些工作。ftp.sh 需要用到 ftp.conf 配置文件。你会怎样实现呢?像下面这样子?
release.sh:

1
2
3
4
#!/bin/bash

echo "call release.sh"
./script/ftp.sh

ftp.sh:

1
2
3
4
5
#!/bin/bash

echo "call ftp.sh"
echo "working path = $(pwd)"
cat ./ftp.conf

ftp.conf:

1
this is ftp.conf content

这样子是有问题的,因为你是在 release 目录执行的 release.sh,所以当前所在的目录就是在 release 目录,而在 release.sh 中调用 script/ftp.sh,相当于起了一个子进程去执行 script/ftp.sh,子进程会继承父进程的环境变量!$PWD也不例外。也就是说 script/ftp.sh 脚本的 cat ./ftp.conf 中的 . 代表的是 release 目录!所以会找不到 ./ftp.conf:

1
2
3
4
5
qiushao@qiushao-PC:~/project/shell/release$ ./release.sh 
call release.sh
call ftp.sh
working path = /home/qiushao/project/shell/release
cat: ./ftp.conf: No such file or directory

你或许会想,可不可以这么写呢: cat ./script/ftp.sh,可以是可以,但这样子损失了可移植性!假如 script 下面的脚本是一套通用的功能,其他工程也可以使用。那要移植到其他项目可能就需要修改这个路径。因为有可能其他项目是这么调用的:./foobar/script/ftp.sh。所以这种方法不推荐。
那我们可不可以先进入 script 目录,再调用 ftp.sh,执行完 ftp.sh 后,再返回之前的目录呢,就像这样:

1
2
3
cd script
./ftp.sh
cd -

这样是可以,但增加了使用者的工作量,且使用者有可能会忘了准备工作和扫尾工作。所以这种方法也不推荐。
最好的方法就是在 ftp.sh 中获取 ftp.sh 所在的目录的绝对路径:$SCRIPT_PATH,然后在 ftp.sh 中可以这么访问 ftp.conf:

1
cat $SCRIPT_PATH/ftp.conf

那么问题来了,我们怎么才能正确的获取到脚本所在的目录的绝对路径呢?

2. how,如何正确获取脚本所在目录的绝对路径

经过上面的讨论,我们已经确认必须要获取脚本所在的路径了。下面我们就讨论怎样才能正确的获取脚本所在目录的绝对路径。

2.1. pwd

如果你是一个新手,可能会想,获取脚本所在的路径还不简单,pwd 命令啊。too young too simple。请看官方对 pwd 命令的解释:

print name of current/working directory

这个命令是用来打印当前的工作路径,这里没有任何意思说明,这个目录就是脚本存放的目录。工作路径是与运行脚本的目录,以及在脚本中的 cd 命令相关的。并不是固定的。前面的例子可以说明这一点。所在这种方法是错误的。

2.2. $0

另一个误人子弟的答案,是 $0,这个也是不对的,这个$0是Bash环境下的特殊变量,其真实含义是:

Expands to the name of the shell or shell script. This is set at shell initialization. If bash is invoked with a file of commands, $0 is set to the name of that file. If bash is started with the -c option, then $0 is set to the first argument after the string to be executed, if one is present. Otherwise, it is set to the file name used to invoke bash, as given by argument zero.
翻译如下:$0会扩展成 shell 或者 shell 脚本的名称。这是在 shell 初始化的时候设置的。如果 bash 是以调用一个脚本文件或命令启动的,$0就会被设置为脚本文件的名称。如果 bash 是以通过 -c参数启动的,那么$0就会被设置为将要被执行的字符串后面的第一个参数,如果有参数的话。除此以外,$0会被设置成调用bash的那个文件的名称。

如果看了解释有点晕的话,还是通过几个例子来看吧:
还是用上面的例子,只将 script/ftp.sh 修改如下

1
2
3
4
#!/bin/bash

echo "call ftp.sh"
echo "$0"

执行 ./release.sh,结果如下:

1
2
3
call release.sh
call ftp.sh
./script/ftp.sh

看到没,$0 的值是 ./script/ftp.sh,也就是脚本的名称,这个名称并不一定是绝对路径。它是跟调用方式相关的。因为我们是这样的调用的:./script/ftp.sh,所以它就是:./script/ftp.sh。你可能会想,如果是这样的话,那我们就可以获取脚本的相对路径,然后进入这个路径,再调用 pwd 命令就可以获得脚本所在的路径啦:
ftp.sh:

1
2
3
4
5
#!/bin/bash

echo "call ftp.sh"
current_path=$(cd $(dirname $0);pwd)
echo "current_path=$current_path"

执行结果:

1
2
3
4
qiushao@qiushao-PC:~/project/shell/release$ ./release.sh 
call release.sh
call ftp.sh
current_path=/home/qiushao/project/shell/release/script

嘿,好像是正确了。但别高兴太早,要是我们换一个调用方式呢,把 release.sh 改成这样:

1
2
3
4
5
#!/bin/bash

echo "call release.sh"
source ./script/ftp.sh
#./script/ftp.sh

执行结果就是:

1
2
3
4
qiushao@qiushao-PC:~/project/shell/release$ ./release.sh 
call release.sh
call ftp.sh
current_path=/home/qiushao/project/shell/release

这下傻了吧。为什么会这样呢。这里需要用到一个知识点:** 直接调用脚本(./script/ftp.sh)会起一个新的子进程去执行脚本,使用source ./script/fpt.sh的话,是在当前进行中执行脚本内容! **。另外如果你有注意前面关于$0的解释的话,就会注意到一句话:

$0是在 shell 进程初始化的时候设置的。

结合这两个知识点就能解释为什么使用 source ./script/ftp.sh 的方法会出错了。因为 source 一个脚本并不会另起一个新的 shell 进程。而 $0 是在这个 shell 进程启动时就设置的了,所以 $0 的值是 ./release 而不是 ./script/ftp.sh。

2.3. 正确的方法

在尝试过了各种各样的错误方法之后,终于找到了正确的方法:

1
SCRIPT_DIR=$(cd $(dirname ${BASH_SOURCE[0]}); pwd)

$BASH_SOURCE是一个数组,它的第0个元素是脚本的名称。具体请查看 man bash,搜索 BASH_SOURCE。不过这种方法的局限性在于只适用于 bash 其他 shell 不支持这个变量,例如 csh, zsh。还好,bash 是我们最常用的 shell。