1

在写 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 的描述:

BASH_SOURCE

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

FUNCNAME

谁知道它们在说什么呢?目前,我只知道 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_SOURCEFUNCNAME 都是数组。它们跟我自己随便定义的数组

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 命令或函数的参数值里有可能包含空格字符时,就要认真的给它们加上双引号,即使双引号有冗余也没问题。


garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。


引用和评论

0 条评论