在选择 $0 和 BASH_SOURCE 之间的区别是什么?

245

如何在"$0""${BASH_SOURCE[0]}"之间进行选择?

GNU文档中的描述对我帮助不大。

    BASH_SOURCE
    
 An array variable whose members are the source filenames where the
 corresponding shell function names in the FUNCNAME array variable are
 defined. The shell function ${FUNCNAME[$i]} is defined in the file
 ${BASH_SOURCE[$i]} and called from ${BASH_SOURCE[$i+1]}

2
BASH_SOURCE是在bash-3.0-alpha版本中添加的。根据您的测试环境,您可能没有它。我发现早期Solaris和OS X都缺少它。另请参见U&L.SE上的[return: can only `return' from a function or sourced script](https://unix.stackexchange.com/q/468579/56041)。 - jww
36
还有其他人认为“一个数组变量,其成员是源文件名,对应的shell函数名称在FUNCNAME数组变量中定义。 shell函数${FUNCNAME[$i]}在文件${BASH_SOURCE[$i]}中定义,并从${BASH_SOURCE[$i+1]}调用。”这句话完全就是胡言乱语吗?我不知道它的意思...希望有人能解释一下文档/手册实际想表达什么。 有人认为“一个数组变量,其成员是源文件名,对应的shell函数名称在FUNCNAME数组变量中定义。 shell函数${FUNCNAME[$i]}在文件${BASH_SOURCE[$i]}中定义,并从${BASH_SOURCE[$i+1]}调用。”这段话很难理解,请问有没有人能够解释一下它真正的含义? - Charlie Parker
2
文档描述构建堆栈跟踪所需的各个部分时,我意识到它们更有意义。Bash本身没有堆栈跟踪库(至少我找不到)。相反,我必须使用FUNCNAME和BASH_SOURCE来构建自己的堆栈跟踪... - Christopher King
4个回答

343

注意:如果需要符合POSIX标准的解决方案,请参考this answer

${BASH_SOURCE[0]}(或更简单地说,$BASH_SOURCE[1])包含包含脚本的(可能是相对的)路径,在所有调用场景下都是如此,特别是在脚本被引用时也是如此,而这对于$0来说并不是真实的。

此外,正如Charles Duffy指出的那样,$0可以由调用者设置为任意值。
另一方面,如果没有命名文件涉及,则$BASH_SOURCE可以为空;例如:
echo 'echo "[$BASH_SOURCE]"' | bash

以下示例说明了这一点:

脚本foo

#!/bin/bash
echo "[$0] vs. [${BASH_SOURCE[0]}]"

$ bash ./foo
[./foo] vs. [./foo]

$ ./foo
[./foo] vs. [./foo]

$ . ./foo
[bash] vs. [./foo]

$0 是 POSIX shell 规范的一部分,而 BASH_SOURCE 如其名称所示,是特定于 Bash 的。


[1] 选读: ${BASH_SOURCE[0]} vs. $BASH_SOURCE:

Bash允许您使用标量符号引用数组变量的元素0: 您可以写$arr,而不是写${arr[0]}; 换句话说:如果您像引用标量一样引用变量,则会得到索引为0的元素。

使用此功能会隐藏$arr是一个数组的事实,这就是为什么流行的shell代码检查器shellcheck.net在此写作时发出以下警告的原因:

SC2128: 扩展没有索引的数组只会给出第一个元素。

顺便提一下:虽然这个警告很有帮助,但它可能会更加精确,因为您不一定会得到第一个元素:特别是返回的是索引0处的元素,所以如果第一个元素的索引较高 - 这在Bash中是可能的 - 您将得到空字符串;尝试a[1] ='hi'; echo"$a"。(相比之下,zsh则返回所有元素作为单个字符串,用默认情况下的空格分隔第一个字符存储在$IFS中)。

由于其晦涩难懂,您可以选择放弃此功能,但它可预测地工作,并且实际上,您很少或几乎不需要访问除${BASH_SOURCE[@]}数组变量的索引0之外的索引。


可选阅读,第2部分:什么情况下BASH_SOURCE数组变量实际上包含多个元素?:

BASH_SOURCE只有在涉及函数调用的情况下才有多个条目,在这种情况下,它的元素与包含当前调用堆栈上所有函数名称的FUNCNAME数组相对应。

也就是说,在函数内部,${FUNCNAME[0]}包含正在执行的函数的名称,${BASH_SOURCE[0]}包含定义该函数的脚本文件的路径,${FUNCNAME[1]}包含当前正在执行的函数从中调用的函数的名称(如果适用),依此类推。

如果给定的函数直接从定义函数的脚本文件的顶级作用域中调用,则在调用堆栈的级别$i处的函数,${FUNCNAME[$i+1]}包含:

If the script file was invoked directly (e.g., ./script), use "main" (a pseudo function name); If the script file was sourced (e.g. source ./script or . ./script), use "source" (a pseudo function name).

那么$BASH_SOURCE更通用,并在更多情况下起作用? - Alexander Mills
6
是的,如果您正在使用Bash,则$BASH_SOURCE是更好的选择。 - mklement0
@CharlieParker,请查看我添加到答案底部的新部分。 - mklement0
1
(相比之下,zsh总是表现得像个叛逆者,实际上返回的是第一个元素,而不管它的索引。)事实上,在zsh中,未经过脚本化的数组参数扩展会变成整个数组,而不仅仅是第一个项目。如果没有设置shwordsplit,则$ary ~= "${ary[@]}""$ary" ~= "${ary[*]}",并且$=ary ~= ${ary[*]}${ary[@]},不需要引号。 - Mark Reed
1
谢谢,@MarkReed - 我已经更新了答案以修复不正确的陈述,但是我没有像你的评论那样深入探讨,因为原始评论只是一个旁白。 - mklement0

54

这些脚本可能有助于说明。外部脚本调用中间脚本,中间脚本调用内部脚本:

$ cat outer.sh
#!/usr/bin/env bash
./middle.sh
$ cat middle.sh
#!/usr/bin/env bash
./inner.sh
$ cat inner.sh
#!/usr/bin/env bash
echo "\$0 = '$0'"
echo "\${BASH_SOURCE[0]} = '${BASH_SOURCE[0]}'"
echo "\${BASH_SOURCE[1]} = '${BASH_SOURCE[1]}'"
echo "\${BASH_SOURCE[2]} = '${BASH_SOURCE[2]}'"
$ ./outer.sh
$0 = './inner.sh'
$BASH_SOURCE[0] = './inner.sh'
$BASH_SOURCE[1] = ''
$BASH_SOURCE[2] = ''

然而,如果我们将脚本调用更改为source语句:

$ cat outer.sh
#!/usr/bin/env bash
source ./middle.sh
$ cat middle.sh
#!/usr/bin/env bash
source ./inner.sh
$ cat inner.sh
#!/usr/bin/env bash
echo "\$0 = '$0'"
echo "\${BASH_SOURCE[0]} = '${BASH_SOURCE[0]}'"
echo "\${BASH_SOURCE[1]} = '${BASH_SOURCE[1]}'"
echo "\${BASH_SOURCE[2]} = '${BASH_SOURCE[2]}'"
$ ./outer.sh
$0 = './outer.sh'
$BASH_SOURCE[0] = './inner.sh'
$BASH_SOURCE[1] = './middle.sh'
$BASH_SOURCE[2] = './outer.sh'

27

为了实现可移植性,请在定义了${BASH_SOURCE[0]}时使用它,在否则情况下使用$0,这样会得到

${BASH_SOURCE[0]:-$0}

值得注意的是,在比如zsh中,即使脚本通过source命令运行,$0也包含正确的文件路径。


这取决于你想要它做什么。如果你想要运行脚本的名称而不是被引用的文件名,那么$BASH_SOURCE[-1]可能更等同于$0。 - Tom Tanner
@TomTanner 如果我们只考虑bash,那么您的评论是正确的。但这是关于与非Bash shell兼容性的问题。这个技巧的整个重点在于${BASH_SOURCE[0]}在Bash中被评估为非空字符串(并成为整个表达式的结果),而$0仅在shell不是Bash(并且未定义BASH_SOURCE)时被评估。在ZSH中,${BASH_SOURCE[0]}的等效物是$0 - user7610
是的,但是要指出的是,在bash、ksh和(bourne)sh中,${BASH_SOURCE[-1]}相当于$0。我最近遇到了这个特定的问题。在“source”文件中,${BASH_SOURCE[0]}指的是被引用文件的名称,这并不总是你想要的。 - Tom Tanner
我没有测试过除bash和zsh之外的其他shell,所以也许我打算进行的“兼容性操作”可以以更便携的方式完成。 - user7610

25

简述 我建议使用${BASH_SOURCE:-$0}作为最通用的变量。

之前的答案都很好,但它们没有提到使用${BASH_SOURCE[0]}直接的一个注意事项:如果您将脚本作为sh的参数调用,而您的sh没有别名为bash(在我的情况下,Ubuntu 16.04.5 LTS上链接到dash),它可能会失败,因为BASH_SOURCE变量为空/未定义。以下是一个示例:

t.sh:

#!/usr/bin/env bash

echo "\$0:                     [$0]"
echo "\$BASH_SOURCE:           [$BASH_SOURCE]"
echo "\$BASH_SOURCE or \$0:    [${BASH_SOURCE:-$0}]"
echo "\$BASH_SOURCE[0] or \$0: [${BASH_SOURCE[0]:-$0}]"

(成功) 运行:

$ ./t.sh
$0:                    [./t.sh]
$BASH_SOURCE:          [./t.sh]
$BASH_SOURCE or $0:    [./t.sh]
$BASH_SOURCE[0] or $0: [./t.sh]

$ source ./t.sh
$0:                    [/bin/bash]
$BASH_SOURCE:          [./t.sh]
$BASH_SOURCE or $0:    [./t.sh]
$BASH_SOURCE[0] or $0: [./t.sh]

$ bash t.sh
$0:                    [t.sh]
$BASH_SOURCE:          [t.sh]
$BASH_SOURCE or $0:    [t.sh]
$BASH_SOURCE[0] or $0: [t.sh]

最后:
$ sh t.sh
$0:                    [t.sh]
$BASH_SOURCE:          []
$BASH_SOURCE or $0:    [t.sh]
t.sh: 6: t.sh: Bad substitution

简历

正如你所看到的,只有第三个变量:${BASH_SOURCE:-$0} - 在所有调用场景下都能够工作并给出一致的结果。请注意,我们利用了bash的特性,将对未加下标的数组变量的引用等同于第一个数组元素。


2
使用未加下标的数组是一个好主意,但在寻求 POSIX 兼容性的 shell 脚本中使用 bash 特性似乎不是很明智。这意味着在不同的执行 shell 中 sourced 时会有不同的行为,因此在这里避免这种情况并坚持在 POSIX '模式' 下使用 $0 似乎更可取,不是吗? - Zilog80
1
@Zilog80 实际上,我并不追求 POSIX 兼容性,在这里有另一个答案,我只是提供了一种在最可能的情况下使其工作的方式(据我理解),而不关心 /bin/sh 实际上隐藏了哪种 shell 方言(尽管希望它会是 bash :))。 - it-alien
sh t.sh现在已经修复。bash版本5.1.16。 - zw963
通过使用/bin/sh,你正在寻求符合POSIX标准。 - CervEd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接