获取脚本自身路径
在写 gar 脚本的时候,我需要在 gar 脚本在运行时确定它自身在文件系统中所处目录的路径。基于该路径,可将 gar.css 文件部署到文档项目的根目录下,因为 gar.css 与 gar 脚本在同一目录下,后者需要根据自身的位置方能找到它,否则就只能由 gar 脚本的用户提供 gar.css 的路径,有所不便。
BASH_SOURCE
有人说,用以下语句可获得脚本自身路径
my_path=$(cd $(dirname "BASH_SOURCE[0]") && pwd)我思考了一会。真的思考了……甚至还看了看 Bash reference manual 里对 BASH_SOURCE 的描述:

这个描述里有 FUNCNAME,我又在 manual 里找到了以下描述:

谁知道它们在说什么呢?目前,我只知道 BASH_SOURCE 是个数组变量,存储了一组源文件名(Source filename)。百闻不如一见,我需要 echo 一下 ${BASH_SOURCE[0]}:
#!/bin/bash
echo ${BASH_SOURCE[0]}我将上述脚本取名为 foo,将其置于我定义的一个专用于存放脚本的目录内,并赋其可执行权限,然后执行 foo,
$ foo
/home/garfileo/.my-scripts/foo换一种方式执行 foo,
$ bash /home/garfileo/.my-scripts/foo
/home/garfileo/.my-scripts/foo再换一种方式,
$ bash $(foo)
/home/garfileo/.my-scripts/foo再换一种方式,
$ source $(foo)
/home/garfileo/.my-scripts/foo在 Bash 的语法里,$(...) 称为命令替换,即开辟一个子 Shell,在其中执行括号内的命令,并以文本的形式返回结果。
上述试验揭示的是,foo 脚本在运行时,可以确定自己身处何处。foo 可以,gar 也可以。
BASH_SOURCE[0] 与 $0 有什么区别?
似乎通过命令行的第一个参数 $0 也能令一个脚本获知自身所处位置。将 foo 改为
#!/bin/bash
echo $0重新做一遍试验:
$ foo
/home/garfileo/.my-scripts/foo
$ bash $(foo)
/home/garfileo/.my-scripts/foo
$ source $(foo)
bash上述试验中,仅仅是在使用 source 执行 foo 时,得到的结果不是 foo 脚本的位置,而是 bash。
source 命令有何特别之处?
source 命令
source 命令可载入指定的 Shell 脚本(可以是 Bash,也可以其他 Shell),并将其作为当前正在运行的 Shell 的一部分。因此,上一节的试验
$ source $(foo)输出的是 bash,这是因为 foo 脚本中的内容被 source 载入到了当前的 bash 里,成为了后者的一部分,因而 $0 便不是 foo,而是 bash。
source 命令就是 Bash 世界里的吸星大法或北冥神功。
由于存在 source 这样的命令,因此用 ${BASH_SOURCE[0]} 获得脚本的自身路径更为稳健。
函数栈
大致理解了 ${BASH_SOURCE[0]} 的奥义,我依然有些纠结 Bash reference manual 给出的解释:
函数${FUNCNAME[$i]}在文件${BASH_SOURCE[$i]}里定义,在${BASH_SOURCE[$i+1]}中被调用。
是什么意思呢?
具体一下,即:函数 ${FUNCNAME[0]} 在文件 ${BASH_SOURCE[0]} 里定义,在 ${BASH_SOURCE[1]} 中被调用。是什么意思呢?
我需要修改 foo,在其中定义一个简单的函数:
#!/bin/bash
function test {
echo ${BASH_SOURCE[*]}
echo ${FUNCNAME[*]}
}其中,${X[*]} 的意思是,将数组 X 里的所有的东西合并为一段文本(或一段字串)。
然后,创建脚本 bar,用它 source foo,并调用函数 text:
#!/bin/bash
source foo
test然后执行
$ bash ./bar
/home/garfileo/.my-scripts/foo ./bar
test main根据这个输出结果,很容易推断出
${BASH_SOURCE[0]是/home/garfileo/.my-scripts/foo;${BASH_SOURCE[1]是bar;${FUNCNAME[0]}是test;${FUNCNAME[1]}是main。
于是,可断言:函数 test 在文件 foo 里被定义,在文件 bar 里被调用。
为了更明白一些,修改 bar 脚本:
#!/bin/bash
source foo
function bar {
test
}
bar再次执行 bar:
$ bash ./bar
/home/garfileo/.my-scripts/foo ./bar ./bar
test bar main可以据此推断出,main 函数是函数栈最底部的函数,它是 Bash 所有函数的调用者。
数组
BASH_SOURCE 和 FUNCNAME 都是数组。它们跟我自己随便定义的数组
blab=(a b c d e f)在本质上并无区别。${blab[0]},${blab[1]},${blab[*]}……虽然看上去奇怪,但皆为从数组里获取元素的语法。
${blab[0]} 从 blab 里获取第 1 个元素。${blab[1]} 从 blab 里获取第 2 个元素。依次类推。虽然 Bash 数学不是很好,更像个文科生,但是在数组的下标方面,支持算术,例如 ${blab[1+2]} 从 blab 里获取第 4 个元素。
上文已有讲述,${blab[*]} 从 blab 里获取所有元素,并组织成一段文本。能够获取数组所有元素的语法,还有 ${blab[@]},但是它得到的是多段的文本,相邻的文本段以 IFS 定义的符号隔开。IFS 是 Bash 的一个环境变量,它的值默认是空格。因此,多段文本即空格作为间隔符号的文本。
多段文本,在 Bash 的循环语句里很是常见。例如
for i in a b c d e
do
echo $i
done这段代码可在终端窗口(命令行窗口)里输出
a
b
c
d
e由于 ${blab[@]} 能获取 blab 中的所有元素并以分段文本的形式将其给出,因此上述循环语句等价于以下代码
for i in ${blab[@]}
do
echo $i
done但是,如果数组是下面这样
blab=(a b c d "e f g" h)${blab[@]} 并不认为 "e f g" 是数组里的一个元素,而是三个。要约束一下它的放纵,就需要用双引号。例如
for i in "${blab[@]}"
do
echo $i
done这段代码会产生输出:
a
b
c
d
e f g
h在数组元素的访问语句外围加双引号的行为,似乎没有什么规律可言。这样的形式,就类似于英文里某些单词的过去时态那样特殊。
命令或函数的参数
命令或函数的参数,也是数组,只是在访问其中元素的语法更简化了。例如,$0, $1, $2, ...,可以访问第 1 个,第 2 个,第 3 个,第……个参数。使用 $@ 可访问全部的参数,但是得到的是分段文本。使用 $* 也可以访问全部的参数,但是得到的是一段文本。
我觉得挺实用的是数组的切片语法。例如
${blab[@]:2}表示获取 blab 里第 3 个元素及其之后的所有元素,结果以分段文本的形式给出。
${blab[@]:2:4}表示获取 blab 里第 3 个元素及其之后的 3 个元素,一共是 4 个元素。
同理,对于命令或函数的参数,也有类似的语法:
${@:2}
${@:3:4}身在何处
好像已经有些偏离了问题的出发点太远。现在,回到起点:
my_path=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)我曾一度怀疑这行代码有些罗嗦,写成
my_path=$(dirname ${BASH_SOURCE[0]})不行吗?
例如,根据前面的试验,${BASH_SOURCE[0]} 可以给出 foo 脚本所在位置的绝对路径:
/home/garfileo/.my-scripts/foo倘若对这个路径执行 dirname,变可得到 foo 脚本的位置,即
$ dirname /home/garfileo/.my-scripts/foo
/home/garfileo/.my-scripts用同样的办法,便可在 gar 脚本里完全可以确定它自身在何处了,为何还要 cd 到 $(dirname ${BASH_SOURCE[0]},然后再 pwd 呢?
上述想法之所以能得手,是一种巧合。因为我将 foo 脚本放到了系统 PATH 设定的路径里。Bash 在执行 foo 的时候,能够得到它的绝对路径,并保存至 ${BASH_SOURCE[0]}。倘若执行 foo 的时候,使用的是相对路径,上述想法便会失灵。所以,不应该投机取巧,老老实实先跳转到 $(dirname ${BASH_SOURCE[0]},成功后,再用 pwd 输出当前的工作目录。由于 Bash 的命令替换开启的是当前 Shell 的子 Shell,而在子 Shell 里切换工作目录,并不会影响当前 Shell。
事实上,考虑到有时会遇到目录或文件的名字里包含空格的情况,获取脚本自身路径更稳健的写法应该是:
my_path="$(cd "$(dirname "${BASH_SOURCE[0]"})" && pwd)"记住,空格是 Bash 命令和函数的天敌。凡是怀疑 Bash 命令或函数的参数值里有可能包含空格字符时,就要认真的给它们加上双引号,即使双引号有冗余也没问题。