在写 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 命令或函数的参数值里有可能包含空格字符时,就要认真的给它们加上双引号,即使双引号有冗余也没问题。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。