Bash中与Python的`if __name__ == '__main__'`等价的语句是什么?

49
在Bash中,我想要既能够源化脚本又能执行文件。Bash的等效于Python的if __name__ == '__main__'是什么?
我没有在Stackoverflow上找到关于这个主题的现成问题/解决方案(我怀疑我提出的方式不符合现有问题/答案的要求,但这是我根据我的Python经验所能想到的最明显的方式来表达问题)。

p.s. 关于可能是重复问题(如果我有更多时间,我会写一个更简短的回答):

链接到的问题问的是“如何检测脚本是否被引用”,但这个问题是问“如何创建一个既可以被引用又可以作为脚本运行的bash脚本”。对于这个问题的回答可能会使用前一个问题的一些方面,但还有其他要求/问题,如下:

  • 一旦您检测到脚本正在被引用,最好的方法是不运行脚本(除了导入所需函数之外),以避免意外副作用,例如添加/删除/修改环境/变量)
  • 一旦您检测到脚本正在运行而不是被引用,实现脚本的规范方式是什么(将其放入一个函数中?或者只是在if语句后面放置它?如果将其放在if语句后面,它会有副作用吗?
  • 我发现大多数关于Bash的谷歌搜索都没有涵盖这个主题(既可以被引用又可以执行的Bash脚本),实现此功能的规范方式是什么?这个主题没有被涵盖是因为不鼓励或者说不好吗?有需要注意的问题吗?

我不太熟悉Python,所以不太明白你的问题。 :) 你的意思是想检测脚本是直接运行还是通过使用“source”或点运算符在另一个脚本中包含运行吗? - ghoti
例如,我有一个名为script1.sh的脚本,其中包含一个名为xyz()的函数,我想在另一个脚本script2.sh中使用它。当我从script2中源代码script1时,script1会以某种方式退出,这会阻止我在script2中调用xyz()。(关于我提到的Python位的很好的解释在此提供here) - Trevor Boyd Smith
可能是如何检测脚本是否被引用的重复问题。 - Andrea Corbellini
参见:从shell脚本导入函数。我在那里添加了一个完整的Bash库创建和导入示例,供有兴趣的人参考。 - Gabriel Staples
6个回答

40

解决方案:

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

我添加了这个答案,因为我想要一个写成 Python 的 if __name__ == '__main__' 风格但是在 Bash 中使用的答案。

关于使用 BASH_SOURCE$_ 。我使用 BASH_SOURCE,因为它似乎比 $_ 更健壮(链接1链接2)。


以下是我用两个 Bash 脚本测试/验证过的示例:

带有 xyz() 函数的 script1.sh:

#!/bin/bash

xyz() {
    echo "Entering script1's xyz()"
}

main() {
    xyz
    echo "Entering script1's main()"
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

尝试调用函数xyz()的script2.sh:

#!/bin/bash

source script1.sh

xyz    

$0 本身并不是完全可靠的(http://mywiki.wooledge.org/BashFAQ/028#Why_.240_is_NOT_an_option),所以我不确定这个方法一定会起作用。 - Etan Reisner
@EtanReisner 如果您认为您有更好的解决方案,请随意编写您自己的答案。(目前已经有60多个赞同票,用于使用$0解决两个不同问题(这里这里 - Trevor Boyd Smith
我并不是这样认为。我只是想指出$0是不可靠的,这可能会在这里引起问题(但在大多数情况下不应该)。 - Etan Reisner
${BASH_SOURCE[0]}${0}相比,在使用igncr时表现更好: - Steve Pitchers
点赞。经过一年的学习和使用,我终于同意比较 ${BASH_SOURCE[0]}$0 确实是最好的技术,正如这个答案所示,因为这种技术可以正确处理嵌套脚本,其中一个脚本调用或源另一个脚本。如果你像我一样,想让它看起来更像 Python,那么请查看我的新答案,我认为它可以使代码更美观、自我记录。 - Gabriel Staples

10

使用 FUNCNAME

FUNCNAME 是一个仅在函数内部可用的数组,其中FUNCNAME [0] 是函数名称,FUNCNAME [1] 是调用者的名称。因此,在顶层函数中,FUNCNAME [1] 在已执行脚本中将是main,在来源脚本中将是source

#!/bin/bash

get_funcname_1(){
    printf '%s\n' "${FUNCNAME[1]}"
}

_main(){
    echo hello
}

if [[ $(get_funcname_1) == main ]]; then
    _main
fi

示例运行:

$ bash funcname_test.sh 
hello
$ source funcname_test.sh 
$ _main
hello

我不确定是如何偶然发现这个的。man bash 没有提到 source 值。

替代方法:在函数外使用 FUNCNAME[0]

这仅适用于4.3和4.4,并且未记录。

if [[ ${FUNCNAME[0]} == main ]]; then
    _main
fi

9

没有更好的选择,我通常使用这个:

#!/bin/bash

main()
{
    # validate parameters

    echo "In main: $@"

    # your code here
}

main "$@"

如果您想知道此脚本是否正在被引用,请将您的main调用包装在内即可。

if [[ "$_" != "$0" ]]; then
    echo "Script is being sourced, not calling main()" 
else
    echo "Script is a subshell, calling main()"
    main "$@"
fi

参考资料:如何检测脚本是否被引用


9

return 2> /dev/null

如果想用 Bash 的习惯方法来实现,可以这样使用 return

_main(){
    echo hello
}

# End sourced section
return 2> /dev/null

_main

示例运行:

$ bash return_test.sh 
hello
$ source return_test.sh 
$ _main
hello

如果脚本是被引用的,return会返回到父级(当然),但如果脚本被执行,return将产生一个被隐藏的错误,并且脚本将继续执行。
我已在GNU Bash 4.2到5.0上进行了测试,并且这是我首选的解决方案。
警告:这在大多数其他shell中不起作用。
此内容基于mr.spuratic's answer关于如何检测脚本是否被引用的部分内容。

4

我真的认为这是在Bash中实现与if __name__ == "__main__"等效的方式最具Pythonic和美感的方法:

来自eRCaGuy_hello_world存储库中的hello_world_best.sh

#!/usr/bin/env bash

main() {
    echo "Running main."
    # Add your main function code here
}

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    # This script is being run.
    __name__="__main__"
else
    # This script is being sourced.
    __name__="__source__"
fi

# Only run `main` if this script is being **run**, NOT sourced (imported)
if [ "$__name__" = "__main__" ]; then
    main "$@"
fi

要运行它,请运行./hello_world_best.sh。这是一个示例运行和输出:
eRCaGuy_hello_world/bash$ ./hello_world_best.sh 
Running main.

要引用(导入)它,请运行 . ./hello_world_best.sh. hello_world_best.shsource hello_world_best.sh 等命令。当您引用它时,在这种情况下您将不会看到任何输出,但是它会让您可以访问其中的函数,以便您可以手动调用 main 函数等。如果想了解更多关于“sourcing”的含义,请参见我的回答:Unix:source和export之间有什么区别?。这里是一个包括在引用文件后手动调用 main 函数的样本运行和输出:
eRCaGuy_hello_world/bash$ . hello_world_best.sh 
eRCaGuy_hello_world/bash$ main
Running main.

重要提示:

我曾经认为基于"${FUNCNAME[-1]}"的技术更好,但事实并非如此!事实证明,该技术并不能很好地处理嵌套脚本,其中一个脚本调用或源另一个脚本。为了使其正常工作,您必须 1)将"${FUNCNAME[-1]}"放在一个函数中,2)根据嵌套脚本的层次改变 FUNCNAME 数组中的索引,这是不切实际的。因此,请不要使用我以前推荐的"${FUNCNAME[-1]}"技术。相反,请使用我现在推荐的if [ "${BASH_SOURCE[0]}" = "$0" ]技术,因为它能够完美地处理嵌套脚本!

更进一步

  1. 我刚刚添加了一个非常详细的示例:如何在Bash中编写、导入和使用库?,该示例使用了上面的技术。我还展示了Python中相对导入的详细示例,以便它们会在任何人克隆到计算机上的仓库中自动工作,而无需重新配置任何内容。

参考资料

  1. 这里是我第一次了解到 "${BASH_SOURCE[0]}" = "$0" 的主要答案。
  2. 我第一次了解到 ${FUNCNAME[-1]} 技巧的地方:@mr.spuratic: 如何检测脚本是否被引用 - 他显然是从 Dennis Williamson 那里学来的。
  3. 在过去的一年中,我进行了大量的个人研究、试验和努力。
  4. 我的代码:eRCaGuy_hello_world/bash/if__name__==__main___check_if_sourced_or_executed_best.sh
  5. 我的其他相关但更长的答案:如何检测脚本是否被引用

3

我一直在我的脚本底部使用以下结构:

[[ "$(caller)" != "0 "* ]] || main "$@"

在脚本中,其它所有内容都是在函数中定义或者是全局变量。

caller 的文档说明为“返回当前子程序调用的上下文。” 当脚本被引用时,caller 的结果从引用此脚本的行号开始。如果此脚本未被引用,则从 "0 " 开始。

我使用 !=|| 而不是 =&& 的原因是后者会导致在被引用时脚本返回 false。这可能会导致运行在 set -e 下的外部脚本退出。

请注意,我只知道此方法适用于 bash。对于 posix shell,它不能正常工作。我不知道其他 shell,例如 ksh 或 zsh 是否可以使用。


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