我有一个脚本,我不希望它调用退出,如果它是来源。

我想检查是否$0 == bash,但这有问题,如果脚本是从另一个脚本,或者如果用户从不同的shell,如ksh源。

是否有一种可靠的方法来检测脚本是否被引用?


当前回答

FWIW,在阅读了所有其他的答案后,我想出了以下解决方案:

更新:事实上,有人在另一个答案中发现了一个已经更正的错误,这也影响了我的答案。我认为这里的更新也是一个改进(如果你好奇,请参阅编辑)。

这适用于所有以#!开头的脚本。/bin/bash,但也可以由不同的shell来获取一些信息(如设置),这些信息保存在主函数之外。

根据下面的评论,这里的答案显然不适用于所有bash变体。同样不适用于/bin/sh基于bash的系统。也就是说,它失败的bash v3。在MacOS上。(目前我不知道如何解决这个问题。)

#!/bin/bash

# Function definitions (API) and shell variables (constants) go here
# (This is what might be interesting for other shells, too.)

# this main() function is only meant to be meaningful for bash
main()
{
# The script's execution part goes here
}

BASH_SOURCE=".$0" # cannot be changed in bash
test ".$0" != ".$BASH_SOURCE" || main "$@"

你可以使用以下(在我看来可读性较差)代码来代替最后两行,在其他shell中不设置BASH_SOURCE,并允许set -e在main中工作:

if ( BASH_SOURCE=".$0" && exec test ".$0" != ".$BASH_SOURCE" ); then :; else main "$@"; fi

这个脚本配方有以下属性:

If executed by bash the normal way, main is called. Please note that this does not include a call like bash -x script (where script does not contain a path), see below. If sourced by bash, main is only called, if the calling script happens to have the same name. (For example, if it sources itself or via bash -c 'someotherscript "$@"' main-script args.. where main-script must be, what test sees as $BASH_SOURCE). If sourced/executed/read/evaled by a shell other than bash, main is not called (BASH_SOURCE is always different to $0). main is not called if bash reads the script from stdin, unless you set $0 to be the empty string like so: ( exec -a '' /bin/bash ) <script If evaluated by bash with eval (eval "`cat script`" all quotes are important!) from within some other script, this calls main. If eval is run from commandline directly, this is similar to previous case, where the script is read from stdin. (BASH_SOURCE is blank, while $0 usually is /bin/bash if not forced to something completely different.) If main is not called, it does return true ($?=0). This does not rely on unexpected behavior (previously I wrote undocumented, but I found no documentation that you cannot unset nor alter BASH_SOURCE either): BASH_SOURCE is a bash reserved array. But allowing BASH_SOURCE=".$0" to change it would open a very dangerous can of worms, so my expectation is, that this must have no effect (except, perhaps, some ugly warning shows up in some future version of bash). There is no documentation that BASH_SOURCE works outside functions. However the opposite (that it only works in functions) is neither documented. The observation is, that it works (tested with bash v4.3 and v4.4, unfortunately I have no bash v3.x anymore) and that quite too many scripts would break, if $BASH_SOURCE stops working as observed. Hence my expectation is, that BASH_SOURCE stays as is for future versions of bash, too. In contrast (nice find, BTW!) consider ( return 0 ), which gives 0 if sourced and 1 if not sourced. This comes a bit unexpected not only for me , and (according to the readings there) POSIX says, that return from subshell is undefined behavior (and the return here is clearly from a subshell). Perhaps this feature eventually gets enough widespread use such that it can no more be changed, but AFAICS there is a much higher chance that some future bash version accidental changes the return behavior in that case. Unfortunately bash -x script 1 2 3 does not run main. (Compare script 1 2 3 where script has no path). Following can be used as workaround: bash -x "`which script`" 1 2 3 bash -xc '. script' "`which script`" 1 2 3 That bash script 1 2 3 does not run main can be considered a feature. Note that ( exec -a none script ) calls main (bash does not pass it's $0 to the script, for this you need to use -c as shown in the last point).

因此,除了一些极端情况外,main只在脚本以通常的方式执行时才被调用。通常这就是你想要的,特别是因为它缺乏复杂的难以理解的代码。

注意,它与Python代码非常相似: 如果__name__ == '__main__': main() 这也阻止了main的调用,除了一些角落的情况,如 你可以导入/加载脚本并强制__name__='__main__'

为什么我认为这是一个解决挑战的好方法

如果您有一些可以由多个shell提供源代码的东西,那么它必须是兼容的。然而(请阅读其他答案),由于没有(容易实现的)可移植的方法来检测来源,您应该更改规则。

通过强制脚本必须由/bin/bash执行,您可以做到这一点。

这解决了所有的情况,但在以下情况下,脚本不能直接运行:

/bin/bash未安装或无法运行(即在引导环境中) 如果你将它管道到一个shell,比如curl https://example.com/script | $ shell (注意:这只适用于最近的bash。据报道,这种配方对某些变体无效。所以一定要检查它是否适用于你的情况。)

然而,我想不出任何真正的原因,在哪里你需要它,以及能力来源完全相同的脚本并行!通常,您可以将其包装起来手动执行main。像这样:

$SHELL -c '。脚本&& main {curl https://example.com/script && echo && echo main;} | $ shell $SHELL -c“eval”“curl https://example.com/script”“&& main” echo 'eval ' ' curl https://example.com/script ' ' && main' | $SHELL

笔记

如果没有其他答案的帮助,这个答案是不可能的!甚至是错误的——最初促使我发表这篇文章的原因。 更新:由于在https://stackoverflow.com/a/28776166/490291中发现的新发现而编辑

其他回答

这在后面的脚本中起作用,不依赖于_变量:

## Check to make sure it is not sourced:
Prog=myscript.sh
if [ $(basename $0) = $Prog ]; then
   exit 1  # not sourced
fi

or

[ $(basename $0) = $Prog ] && exit

@mklement0答案的一个小补充。这是我在脚本中使用的自定义函数,用于确定它是否被引用:

replace_shell(){
   if [ -n "$ZSH_EVAL_CONTEXT" ]; then 
      case $ZSH_EVAL_CONTEXT in *:file*) echo "Zsh is sourced";; esac
   else
      case ${0##*/} in sh|dash|bash) echo "Bash is sourced";; esac
   fi
}

在函数中,zsh的“$ZSH_EVAL_CONTEXT”的输出是toplevel:file:shfunc,而不仅仅是toplevel:file;因此,*:file*应该解决这个问题。

BASH_SOURCE[]的答案(bash-3.0及更高版本)似乎最简单,尽管BASH_SOURCE[]并没有在函数体之外工作(它目前恰好可以工作,这与手册页不一致)。

Wirawan Purwanto建议的最健壮的方法是在函数中检查FUNCNAME[1]:

function mycheck() { declare -p FUNCNAME; }
mycheck

然后:

$ bash sourcetest.sh
declare -a FUNCNAME='([0]="mycheck" [1]="main")'
$ . sourcetest.sh
declare -a FUNCNAME='([0]="mycheck" [1]="source")'

This is the equivalent to checking the output of caller, the values main and source distinguish the caller's context. Using FUNCNAME[] saves you capturing and parsing caller output. You need to know or calculate your local call depth to be correct though. Cases like a script being sourced from within another function or script will cause the array (stack) to be deeper. (FUNCNAME is a special bash array variable, it should have contiguous indexes corresponding to the call stack, as long as it is never unset.)

function issourced() {
    [[ ${FUNCNAME[@]: -1} == "source" ]]
}

(在bash-4.2及以后版本中,可以使用更简单的形式${FUNCNAME[-1]}来代替数组中的最后一项。感谢Dennis Williamson下面的评论,改进和简化了。)

然而,你的问题如所述是“我有一个脚本,我不希望它调用'退出',如果它是来源”。这种情况下常见的bash习惯用法是:

return 2>/dev/null || exit

如果脚本是源脚本,则return将终止源脚本并返回给调用者。

如果脚本正在执行,那么return将返回一个错误(重定向),exit将正常终止脚本。如果需要,return和exit都可以使用退出码。

遗憾的是,这在ksh中不起作用(至少在我这里的AT&T派生版本中不起作用),如果在函数或点源脚本外部调用return,它将return视为等同于exit。

更新:在当前版本的ksh中,您可以检查特殊变量.sh。设置为函数调用深度的级别。对于一个被调用的脚本,它最初将被取消设置,对于一个点源脚本,它将被设置为1。

function issourced {
    [[ ${.sh.level} -eq 2 ]]
}

issourced && echo this script is sourced

这并不像bash版本那样健壮,您必须在正在测试的文件的顶层或已知函数深度中调用issourced()。

(你可能也对github上的这段代码感兴趣,它使用了ksh纪律函数和一些调试陷阱技巧来模拟bash FUNCNAME数组。)

这里的标准答案是:http://mywiki.wooledge.org/BashFAQ/109还提供了$-作为shell状态的另一个指示器(尽管并不完善)。


注:

可以创建名为“main”和“source”的bash函数(覆盖内置),这些名称可能出现在FUNCNAME[]中,但只要只测试该数组中的最后一项,就不会有歧义。 我对pdksh没有一个好的答案。我能找到的最接近的方法只适用于pdksh,其中脚本的每个源都打开一个新的文件描述符(原始脚本从10开始)。几乎肯定不是你想要依靠的东西……

这是从其他一些关于“通用”跨壳支持的答案衍生出来的。不可否认,这与https://stackoverflow.com/a/2942183/3220983非常相似,尽管略有不同。这样做的缺点是,客户端脚本必须尊重如何使用它(即先导出一个变量)。它的优点是简单,而且可以在“任何地方”工作。这里有一个模板供你剪切和粘贴:

# NOTE: This script may be used as a standalone executable, or callable library.
# To source this script, add the following *prior* to including it:
# export ENTRY_POINT="$0"

main()
{
    echo "Running in direct executable context!"
}

if [ -z "${ENTRY_POINT}" ]; then main "$@"; fi

注意:我使用export只是为了确保这个机制可以扩展到子进程。

我认为在ksh和bash中没有任何可移植的方法来做到这一点。在bash中,您可以使用调用器输出来检测它,但我认为ksh中不存在等效的输出。