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

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

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


当前回答

这并不完全是OP想要的,但我经常发现自己需要源代码脚本只是为了加载它的函数(即作为一个库)。例如,用于基准测试或测试目的。

下面是一个适用于所有shell(包括POSIX)的设计:

将所有顶级操作包装在run_main()函数中。 让您的源脚本检查不执行任何操作的初始-no-run参数;没有——no-run,它可以调用run_main。 使用以下方法获取脚本的源代码:

set -- --no-run "$@"
. script.sh
shift

问题是。或者来源是不可能将参数可移植地传递给脚本。POSIX shell忽略参数。并传递调用者的“$@”。

其他回答

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开始)。几乎肯定不是你想要依靠的东西……

我将给出一个特定于bash的答案。Korn shell,对不起。假设脚本名为include2.sh;然后在include2.sh中创建一个名为am_I_sourced的函数。下面是我的include2.sh的演示版本:

am_I_sourced()
{
  if [ "${FUNCNAME[1]}" = source ]; then
    if [ "$1" = -v ]; then
      echo "I am being sourced, this filename is ${BASH_SOURCE[0]} and my caller script/shell name was $0"
    fi
    return 0
  else
    if [ "$1" = -v ]; then
      echo "I am not being sourced, my script/shell name was $0"
    fi
    return 1
  fi
}

if am_I_sourced -v; then
  echo "Do something with sourced script"
else
  echo "Do something with executed script"
fi

现在尝试以多种方式执行它:

~/toys/bash $ chmod a+x include2.sh

~/toys/bash $ ./include2.sh 
I am not being sourced, my script/shell name was ./include2.sh
Do something with executed script

~/toys/bash $ bash ./include2.sh 
I am not being sourced, my script/shell name was ./include2.sh
Do something with executed script

~/toys/bash $ . include2.sh
I am being sourced, this filename is include2.sh and my caller script/shell name was bash
Do something with sourced script

所以这是毫无例外的工作,它没有使用脆弱的$_东西。这个技巧使用了BASH的自省功能,即内置变量FUNCNAME和BASH_SOURCE;请参阅bash手册页中的文档。

只有两个警告:

1)对am_I_called的调用必须发生在源脚本中,而不是在任何函数中,以免${FUNCNAME[1]}返回其他东西。是的…您本可以检查${FUNCNAME[2]},但这样做只会使您的工作更加困难。

2)函数am_I_called必须驻留在源脚本中,如果你想知道被包含的文件的名称。

bash, ksh, zsh的健壮解决方案,包括一个跨shell解决方案,以及一个相当健壮的posix兼容解决方案:

给出的版本号是验证功能的版本号——很可能,这些解决方案也适用于更早的版本——欢迎反馈。 仅使用POSIX特性(例如在dash中,它在Ubuntu中充当/bin/sh),没有可靠的方法来确定脚本是否被引用-请参阅下面的最佳近似方法。

重要的是:

The solutions determine whether the script is being sourced by its caller, which may be a shell itself or another script (which may or may not be sourced itself): Also detecting the latter case adds complexity; if you do not need to detect the case when your script is being sourced by another script, you can use the following, relatively simple POSIX-compliant solution: # Helper function is_sourced() { if [ -n "$ZSH_VERSION" ]; then case $ZSH_EVAL_CONTEXT in *:file:*) return 0;; esac else # Add additional POSIX-compatible shell names here, if needed. case ${0##*/} in dash|-dash|bash|-bash|ksh|-ksh|sh|-sh) return 0;; esac fi return 1 # NOT sourced. } # Sample call. is_sourced && sourced=1 || sourced=0 All solutions below must run in the top-level scope of your script, not inside functions.

下面是一行程序——解释如下;跨shell版本是复杂的,但它应该可以健壮地工作:

Bash(在3.57、4.4.19和5.1.16上验证)

(return 0 2>/dev/null) && sourced=1 || sourced=0

KSH(在93u+上验证)

[[ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ]] && sourced=1 || sourced=0

ZSH(5.0.5验证)

[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0

交叉shell (bash, ksh, zsh)

(
  [[ -n $ZSH_VERSION && $ZSH_EVAL_CONTEXT =~ :file$ ]] || 
  [[ -n $KSH_VERSION && "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ]] || 
  [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null)
) && sourced=1 || sourced=0

posix兼容;由于技术原因,不是一行程序(单一管道),并且不完全健壮(见底部):

sourced=0
if [ -n "$ZSH_VERSION" ]; then 
  case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
elif [ -n "$KSH_VERSION" ]; then
  [ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && sourced=1
elif [ -n "$BASH_VERSION" ]; then
  (return 0 2>/dev/null) && sourced=1 
else # All other shells: examine $0 for known shell binary filenames.
     # Detects `sh` and `dash`; add additional shell filenames as needed.
  case ${0##*/} in sh|-sh|dash|-dash) sourced=1;; esac
fi

解释


bash

(return 0 2>/dev/null) && sourced=1 || sourced=0

注意:该技术改编自user5754163的答案,因为它比原来的解决方案更健壮,[[$0 != "$BASH_SOURCE"]] && sourced=1 || sourced=0[1]

Bash allows return statements only from functions and, in a script's top-level scope, only if the script is sourced. If return is used in the top-level scope of a non-sourced script, an error message is emitted, and the exit code is set to 1. (return 0 2>/dev/null) executes return in a subshell and suppresses the error message; afterwards the exit code indicates whether the script was sourced (0) or not (1), which is used with the && and || operators to set the sourced variable accordingly. Use of a subshell is necessary, because executing return in the top-level scope of a sourced script would exit the script. Tip of the hat to @Haozhun, who made the command more robust by explicitly using 0 as the return operand; he notes: per bash help of return [N]: "If N is omitted, the return status is that of the last command." As a result, the earlier version [which used just return, without an operand] produces incorrect result if the last command on the user's shell has a non-zero return value.


ksh

[[ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ]] && sourced=1 || sourced=0

特殊变量${.sh。file}有点类似于$BASH_SOURCE;注意${.sh。File}会在bash、zsh和dash中导致语法错误,所以一定要在多shell脚本中有条件地执行它。

与bash不同,$0和${.sh。file}不能保证是相同的-在不同的时候,一个可能是相对路径或仅仅是文件名,而另一个可能是完整的文件名;因此,$0和${.sh。File}在比较之前必须解析为全路径。如果完整路径不同,则隐含了源。


zsh

[[ $ZSH_EVAL_CONTEXT =~ :file$) ]] && sourced=1 || sourced=0

$ZSH_EVAL_CONTEXT包含关于计算上下文:子字符串文件的信息,用:分隔,只有在脚本被引用时才会出现。

在源脚本的顶级作用域中,$ZSH_EVAL_CONTEXT以:file结尾,这就是这个测试的限制。在函数内部,:shfunc被追加到:file;在命令替换中追加:cmdsubst。


仅使用POSIX特性

如果您愿意做出某些假设,那么您可以根据可能正在执行脚本的shell的二进制文件名,对脚本是否被引用做出合理但并非万无一错的猜测。 值得注意的是,这意味着当您的脚本被另一个脚本引用时,这种方法不会检测到这种情况。

本回答中的“如何处理源调用”一节只详细讨论了POSIX特性无法处理的边缘情况。

检查二进制文件名依赖于$0的标准行为,例如,zsh就没有这种行为。

因此,最安全的方法是将上述健壮的、特定于shell的方法(不依赖于$0)与所有剩余shell的基于$0的回退解决方案结合起来。

简而言之:解决方案如下:

在包含特定于shell的测试的shell中:稳定地工作。 在所有其他shell中:仅当脚本直接来自这样的shell(而不是来自另一个脚本)时才能正常工作。

向Stéphane Desneux和他的灵感答案致敬(将我的跨shell语句表达式转换为sh兼容的if语句,并为其他shell添加一个处理程序)。

sourced=0
if [ -n "$ZSH_VERSION" ]; then 
  case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
elif [ -n "$KSH_VERSION" ]; then
  [ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && sourced=1
elif [ -n "$BASH_VERSION" ]; then
  (return 0 2>/dev/null) && sourced=1 
else # All other shells: examine $0 for known shell binary filenames.
  # Detects `sh` and `dash`; add additional shell filenames as needed.
  case ${0##*/} in sh|-sh|dash|-dash) sourced=1;; esac
fi

注意,为了健壮性,每个shell二进制文件名(例如sh)都表示了两次——一次是原样,第二次是,前缀是-。这是为了考虑诸如macOS这样的环境,其中交互式shell作为登录shell启动,其自定义值为$0,该值是前缀为-的(无路径)shell文件名。谢谢,t7e。 (虽然sh和dash可能不太可能用作交互式shell,但您可能需要将其他shell添加到列表中。)


[1] user1902689发现[[$0 != "$BASH_SOURCE"]]在通过将其文件名传递给bash二进制文件执行位于$PATH中的脚本时,会产生假阳性;例如,bash my-script,因为$0只是my-script,而$BASH_SOURCE是完整路径。虽然您通常不会使用这种技术来调用$PATH中的脚本—您只会直接调用它们(my-script)—当与-x结合使用时,它对调试很有帮助。

这是从其他一些关于“通用”跨壳支持的答案衍生出来的。不可否认,这与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只是为了确保这个机制可以扩展到子进程。

我需要一个在[mac, linux]上使用bash的一行程序。版本>= 3,这些答案都不符合要求。

[[ ${BASH_SOURCE[0]} = $0 ]] && main "$@"