如何检测脚本是否正在被引用

337

我有一个脚本,如果它被源引用,我不想让它调用exit

我考虑检查$0 == bash,但是如果脚本从另一个脚本中被源引用,或者用户从不同的shell(如ksh)中源引用它,则存在问题。

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


4
我曾经遇到过类似的问题,解决方法是在任何情况下都避免使用“exit”命令;无论哪种情况,“kill -INT $$”命令都可以安全地终止脚本。 - JESii
4
你有注意到这个答案吗?它是在被接受的答案5年后给出的,但它具有“电池内置”的特点。 - raratiru
25个回答

279

bash, ksh, zsh 提供健壮的解决方案,包括一个跨shell的解决方案,以及一个相对健壮的符合POSIX标准的解决方案:

  • 给出的版本号是已验证功能的版本号 - 可能这些解决方案也适用于更早的版本,欢迎反馈。

  • 仅使用 POSIX 功能(例如在Ubuntu上作为/bin/shdash),没有一种健壮的方法来确定脚本是否正在被调用 - 请参见下文了解最佳近似方案。

重要提示

  • 这些解决方案确定脚本是否被其调用者调用,其中调用者可能是一个shell本身,也可以是另一个脚本(它本身可能或可能不会被调用):

    • 同时检测后一种情况会增加复杂性;如果您不需要检测当您的脚本被另一个脚本调用时的情况,您可以使用以下相对简单的符合POSIX标准的解决方案:

 # 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
  • 所有的解决方案都必须在脚本的顶层范围内运行而不是在函数内部

  • 一行代码的解决方案如下 - 解释在下面;跨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只允许从函数中使用return语句,并且在脚本的顶级范围内仅在脚本被"source"时才允许使用。

      • 如果在未被"source"的脚本的顶级作用域中使用return,则会发出错误消息,并将退出代码设置为1
    • (return 0 2>/dev/null) 在一个子shell中执行return并抑制错误消息;之后,退出代码指示脚本是否被源化(0)或未被源化(1),这与&&||运算符一起使用以相应地设置sourced变量。

      • 必须使用子shell,因为在已被源化的脚本的顶级作用域中执行return将退出脚本。
      • 致敬@Haozhun,他通过明确使用0作为return操作数使命令更加健壮;他指出:bash帮助中的return [N]:“如果省略了N,则返回状态为最后一个命令的状态。”因此,早期版本(只使用return而没有操作数)如果用户shell上的最后一个命令具有非零返回值,则会产生错误的结果。

    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包含有关评估上下文的信息: 用:分隔的子字符串file仅在脚本被源码引用时存在。

    在被源码引用的脚本顶级范围内,$ZSH_EVAL_CONTEXT:file结尾,这就是此测试限制的内容。在函数内部,会将:shfunc添加到:file; 在命令替换中,会将:cmdsubst添加到:file


    仅使用POSIX功能

    如果您愿意做出某些假设,您可以基于了解可能执行脚本的Shell二进制文件名,合理但并不绝对可靠地猜测您的脚本是否正在被源代码引用。
    值得注意的是,这意味着这种方法无法检测到您的脚本是否被其他脚本源码引用。

    此答案的“如何处理源调用”一节中详细讨论了不能仅使用POSIX功能处理的边缘情况。

    检查二进制文件名依赖于$0的标准行为,例如zsh不会表现出这种行为。

    因此,最安全的方法是将上面的稳健、特定于Shell的方法与$0基于后备方案的结合使用,以处理所有剩余的Shell。

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

    • 在涵盖有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 以自定义的 $0 值作为 登录 shell 启动,并且该值是加上 - 前缀的(无路径的)shell 文件名。感谢 t7e (虽然 shdash 可能不太可能用作 交互式 shell,但您可能需要将其他需要添加到列表中的内容。)

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


    1
    @Thatoneguyfromthemovie,你的示例命令与答案之间的关系不清楚,答案是关于_scripts_的。请注意,在您的命令中,您只会看到echo自己的退出代码,即0 - 不管命令替换($(...))内部命令设置了什么退出代码;尝试echo $(nosuchcommand); echo $? - mklement0
    1
    @t7e,我已经更新了答案,以使其更清晰,即所有片段都需要在脚本的顶层作用域中运行,而不是在函数内部运行。让它们在所有shell中也能在函数内部工作,会增加太多的复杂性。 - mklement0
    2
    好的,我会添加我的答案,因为我很难理解为什么那个函数对我不起作用。 - t7e
    1
    谢谢,@t7e - 请看我在底部的更新。我不知道Ubuntu,但在macOS上也可能发生交互式shell的情况。为了简单和性能,并且由于列表可能很小,我只是简单地将case分支条目('sh|-sh|...)翻倍,而不是依赖使用sed的命令替换。 - mklement0
    1
    @t7e,is_sourced()函数的设计不是为了_退出_,而是为了_返回给调用者_并通过其退出代码(0表示已来源,1表示未来源)传达来源状态;示例语句is_sourced && sourced=1 || sourced=0基于该代码执行。 - mklement0
    显示剩余9条评论

    226
    如果您的Bash版本支持BASH_SOURCE数组变量,请尝试以下操作:
    # man bash | less -p BASH_SOURCE
    #[[ ${BASH_VERSINFO[0]} -le 2 ]] && echo 'No BASH_SOURCE array variable' && exit 1
    
    [[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "script ${BASH_SOURCE[0]} is being sourced ..."
    

    14
    这可能是最为清晰的方式,因为 $BASH_SOURCE 正是为此目的而设计的。 - con-f-use
    5
    请注意,这种方法在ksh下无法运行,而这是提问者指定的条件。 - Dennis Williamson
    3
    在使用${BASH_SOURCE[0]}时,是否有特别的原因,而不是只用$BASH_SOURCE${0}$0 之间有什么区别吗? - hraban
    4
    BASH_SOURCE是一个数组变量(参见手册),它保存了一个源代码跟踪堆栈,其中${BASH_SOURCE[0]}是最新的。这里使用大括号告诉bash哪些是变量名的一部分,对于此情况下的$0来说它们不是必要的,但也不会造成任何影响。 ;) - Konrad
    7
    @Konrad,如果您展开 $array,默认情况下会得到 ${array[0]}。所以,再次问一遍,是否有什么理由[...]? - Charles Duffy
    显示剩余4条评论

    88

    这似乎可以在Bash和Korn之间通用:

    [[ $_ != $0 ]] && echo "Script is being sourced" || echo "Script is a subshell"
    

    脚本的第一行或者shebang后面紧跟着的一行(如果使用shebang,应为ksh),必须包含类似于这样的一行或者像pathname="$_"这样的赋值语句(后面需要测试和操作)才能正常工作。


    14
    很遗憾,它不能保证一定能够工作。如果用户设置了 BASH_ENV,在脚本顶部的 $_ 将会是从 BASH_ENV 中运行的最后一个命令。 - Mikel
    37
    如果你使用bash来执行脚本,例如:$ bash script.sh那么,$_ 将是 /bin/bash 而不是 ./script.sh,这与你期望的情况不同,即以以下方式调用脚本:$ ./script.sh在任何情况下,检测 $_ 都会有问题。 - Wirawan Purwanto
    2
    可以添加额外的测试来检查这些调用方法。 - Dennis Williamson
    8
    很遗憾,那是错误的!请参见我的答案 - F. Hauri - Give Up GitHub
    12
    总结一下:虽然这种方法通常有效,但它并不稳健;在以下两种情况下会失败:(a) bash 脚本(通过 shell 可执行文件调用,该解决方案将其错误地报告为 sourced),以及 (b)(发生的可能性较小)echo bash; . script(如果 $_ 恰好与源代码脚本的 shell 匹配,则此解决方案将其错误地报告为子 shell)。 只有 特定于 shell 的特殊变量(例如 $BASH_SOURCE)可以提供稳健的解决方案(因此没有符合 POSIX 标准的稳健解决方案)。 虽然有点麻烦,但是可以制作一个稳健的跨 shell 测试。 - mklement0
    显示剩余7条评论

    83

    阅读了 @DennisWilliamson 的答案之后,发现存在以下问题:

    由于这个问题涉及到 ,因此该答案中还有关于 的另一部分... 请参见下文。

    的简单方法

    [ "$0" = "$BASH_SOURCE" ]
    

    让我们试一下(现场进行,因为那个 bash 可能会;-):

    source <(echo $'#!/bin/bash
               [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
               echo "process $$ is $v ($0, $BASH_SOURCE)" ')
    process 29301 is sourced (bash, /dev/fd/63)
    
    bash <(echo $'#!/bin/bash
               [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
               echo "process $$ is $v ($0, $BASH_SOURCE)" ')
    process 16229 is own (/dev/fd/63, /dev/fd/63)
    

    我使用source而不是.来提高可读性(因为.source的别名):

    . <(echo $'#!/bin/bash
               [ "$0" = "$BASH_SOURCE" ] && v=own || v=sourced;
               echo "process $$ is $v ($0, $BASH_SOURCE)" ')
    process 29301 is sourced (bash, /dev/fd/63)
    

    请注意,进程在保持源代码的情况下,其进程编号不会改变:

    echo $$
    29301
    

    为什么不要使用$_ == $0比较

    为了确保许多情况,我开始编写一个真实的脚本:

    #!/bin/bash
    
    # As $_ could be used only once, uncomment one of two following lines
    
    #printf '_="%s", 0="%s" and BASH_SOURCE="%s"\n' "$_" "$0" "$BASH_SOURCE"
    [[ "$_" != "$0" ]] && DW_PURPOSE=sourced || DW_PURPOSE=subshell
    
    [ "$0" = "$BASH_SOURCE" ] && BASH_KIND_ENV=own || BASH_KIND_ENV=sourced;
    echo "proc: $$[ppid:$PPID] is $BASH_KIND_ENV (DW purpose: $DW_PURPOSE)"
    

    将以下内容复制到名为testscript的文件中:

    cat >testscript   
    chmod +x testscript
    

    现在我们可以进行测试:

    ./testscript 
    proc: 25758[ppid:24890] is own (DW purpose: subshell)
    

    没问题。

    . ./testscript 
    proc: 24890[ppid:24885] is sourced (DW purpose: sourced)
    
    source ./testscript 
    proc: 24890[ppid:24885] is sourced (DW purpose: sourced)
    

    没问题。

    但是,在添加-x标志之前测试脚本:

    bash ./testscript 
    proc: 25776[ppid:24890] is own (DW purpose: sourced)
    

    或者使用预定义变量:

    env PATH=/tmp/bintemp:$PATH ./testscript 
    proc: 25948[ppid:24890] is own (DW purpose: sourced)
    
    env SOMETHING=PREDEFINED ./testscript 
    proc: 25972[ppid:24890] is own (DW purpose: sourced)
    

    这个不再起作用。

    将注释从第五行移到第六行会使答案更易读:

    ./testscript 
    _="./testscript", 0="./testscript" and BASH_SOURCE="./testscript"
    proc: 26256[ppid:24890] is own
    
    . testscript 
    _="_filedir", 0="bash" and BASH_SOURCE="testscript"
    proc: 24890[ppid:24885] is sourced
    
    source testscript 
    _="_filedir", 0="bash" and BASH_SOURCE="testscript"
    proc: 24890[ppid:24885] is sourced
    
    bash testscript 
    _="/bin/bash", 0="testscript" and BASH_SOURCE="testscript"
    proc: 26317[ppid:24890] is own
    
    env FILE=/dev/null ./testscript 
    _="/usr/bin/env", 0="./testscript" and BASH_SOURCE="./testscript"
    proc: 26336[ppid:24890] is own
    

    更难: 现在...

    因为我不经常使用,所以在阅读了man页面后,这是我的尝试:

    #!/bin/ksh
    
    set >/tmp/ksh-$$.log
    

    复制以下内容到一个 testfile.ksh 文件中:

    cat >testfile.ksh
    chmod +x testfile.ksh
    

    运行它两次:

    ./testfile.ksh
    . ./testfile.ksh
    
    ls -l /tmp/ksh-*.log
    -rw-r--r-- 1 user user   2183 avr 11 13:48 /tmp/ksh-9725.log
    -rw-r--r-- 1 user user   2140 avr 11 13:48 /tmp/ksh-9781.log
    
    echo $$
    9725
    

    看一下:

    diff /tmp/ksh-{9725,9781}.log | grep ^\> # OWN SUBSHELL:
    > HISTCMD=0
    > PPID=9725
    > RANDOM=1626
    > SECONDS=0.001
    >   lineno=0
    > SHLVL=3
    
    diff /tmp/ksh-{9725,9781}.log | grep ^\< # SOURCED:
    < COLUMNS=152
    < HISTCMD=117
    < LINES=47
    < PPID=9163
    < PS1='$ '
    < RANDOM=29667
    < SECONDS=23.652
    <   level=1
    <   lineno=1
    < SHLVL=2
    

    sourced运行中,一些变量被继承了,但是并没有什么实际关联...

    甚至可以检查$SECONDS是否接近于0.000,但这只适用于手动sourced的情况...

    你甚至可以尝试检查父级是什么:

    将此内容放入你的testfile.ksh文件中:

    ps $PPID
    

    比:

    ./testfile.ksh
      PID TTY      STAT   TIME COMMAND
    32320 pts/4    Ss     0:00 -ksh
    
    . ./testfile.ksh
      PID TTY      STAT   TIME COMMAND
    32319 ?        S      0:00 sshd: user@pts/4
    

    或者ps ho cmd $PPID,但这只适用于一级子会话...

    抱歉,在 下我找不到可靠的方法来实现这一点。


    1
    对于通过管道(cat script | bash)读入的脚本,请使用以下代码: [ "$0" = "$BASH_SOURCE" ] || [ -z "$BASH_SOURCE" ] - hakre
    4
    请注意,.不是source的别名,实际上是相反的。source somescript.sh 是 Bash 的特有语法,不具备可移植性;而. somescript.sh 则符合 POSIX 标准,具备可移植性(如果我没记错的话)。 - dragon788
    这打破了你的理论。env -i echo $(source hi); hi的内容:#source me[“$ _”==“0”] && { echo“我成功了,但那是否意味着我被引用?” } || { echo“我失败了,但那是否意味着我没有被引用?” } - That one guy from the movie

    34

    BASH_SOURCE[]的答案(从bash-3.0开始)似乎最简单,尽管BASH_SOURCE[]没有文档说明可以在函数体外工作(它目前可以在不同意man页面的情况下工作)。

    最健壮的方法是像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")'
    

    这相当于检查caller的输出,值mainsource区分了调用者的上下文。使用FUNCNAME[]可以避免捕获和解析caller输出。但是你需要知道或计算正确的本地调用深度。在其他函数或脚本中源自另一个脚本的情况会导致数组(堆栈)更深。(FUNCNAME是一个特殊的bash数组变量,它应该有与调用堆栈对应的连续索引,只要它从未被unset。)

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

    (在bash-4.2及更高版本中,您可以使用更简单的形式${FUNCNAME[-1]}代替数组中的最后一项。感谢Dennis Williamson在下面的评论中提供的改进和简化。)
    然而,根据您所述的问题是“我有一个脚本,如果它被源调用,我不想让它调用'exit'”。这种情况的常见bash习惯用法是:
    return 2>/dev/null || exit
    

    如果脚本正在被引用,则return将终止被引用的脚本并返回到调用者。如果脚本正在执行,则return将返回错误(重定向),而exit将像平常一样终止脚本。如果需要,returnexit均可接受退出代码。遗憾的是,这在ksh中不起作用(至少在我这里的AT&T衍生版本中不起作用),如果在函数或点源脚本之外调用,则将return视为等同于exit更新:在现代版的ksh中,您可以检查特殊变量.sh.level,它设置为函数调用深度。对于被调用的脚本,初始设置为空白,而对于点源脚本,它将被设置为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开始)。几乎肯定不是您想依赖的东西...

    如何使用 ${FUNCNAME[(( ${#FUNCNAME[@]} - 1 ))]} 来获取堆栈中的最后(底部)项?然后针对 "main" 进行测试(取反为 OP)对我来说是最可靠的。 - Adrian Günter
    如果我设置了PROMPT_COMMAND,那么当我运行source sourcetest.sh时,它会显示为FUNCNAME数组的最后一个索引。反转检查(查找main作为最后一个索引)似乎更加健壮:is_main() { [[ ${FUNCNAME[@]: -1} == "main" ]]; } - dimo414
    1
    手册说明,FUNCNAME 仅在函数中可用。但是,通过 declare -p FUNCNAME 进行测试后,bash 的行为不同。v4.3 在函数外部会报错,而 v4.4 则会返回 declare -a FUNCNAME。无论何种情况下,${FUNCNAME[0]} 在主脚本中(如果被执行)都会返回 main,而 $FUNCNAME 则不会返回任何内容。此外,有很多脚本在函数外“滥用” $BASH_SOURCE,因此我怀疑这种情况可能不会或不会被更改。 - Tino
    如果你只想返回v. exit,使用kill -1 $$。还有其他原因为什么人们想知道他们是否被引用...例如,子shell可以为({massively,}parallel builds)创建环境。在这种情况下,只需源一个新版本或手动运行作业的环境。/耸耸肩,它有效...总是有解决方法,只是当你是那个总是碰到边角情况的人时,这很糟糕。哈哈 - That one guy from the movie
    使用和推荐${FUNCNAME[-1]}技术一年后,我已经完全撤回了。它在嵌套脚本中无法正常工作!您必须根据嵌套的级别手动更新索引,这既不可预见也不实用。然而,使用if [ "${BASH_SOURCE[0]}" = "$0" ]的技术在运行或源另一个脚本的嵌套脚本中非常有效。因此,我已经大量更新了我的答案,并将我最喜欢的技术放在了这里的顶部。 - Gabriel Staples

    27

    编辑注:该答案的解决方案非常稳健,但仅限于bash。它可以简化为
    (return 2>/dev/null)

    简短概述

    尝试执行return语句。如果脚本没有被引用,将会引发错误。您可以捕捉该错误并按您需要进行处理。

    将以下内容放入一个文件中,比如test.sh:

    #!/usr/bin/env sh
    
    # Try to execute a `return` statement,
    # but do it in a sub-shell and catch the results.
    # If this script isn't sourced, that will raise an error.
    $(return >/dev/null 2>&1)
    
    # What exit code did that give?
    if [ "$?" -eq "0" ]
    then
        echo "This script is sourced."
    else
        echo "This script is not sourced."
    fi
    

    直接执行:

    shell-prompt> sh test.sh
    output: This script is not sourced.
    

    请提供来源:

    shell-prompt> source test.sh
    output: This script is sourced.
    

    对于我来说,在zsh和bash中都可以使用。

    说明

    return语句将在尝试在函数外执行它或者脚本未被引用时抛出错误。在shell提示符下尝试执行以下命令:

    shell-prompt> return
    output: ...can only `return` from a function or sourced script
    

    您不需要看到那个错误信息,所以您可以将输出重定向到dev/null:
    shell-prompt> return >/dev/null 2>&1
    

    现在检查退出代码。0表示正常(没有发生错误),1表示发生了错误:
    shell-prompt> echo $?
    output: 1
    

    您还希望在子shell中执行return语句。当return语句运行时,它会返回。如果您在子shell中执行它,则会从该子shell返回,而不是从您的脚本中返回。要在子shell中执行,请将其包装在$(...)中:

    shell-prompt> $(return >/dev/null 2>$1)
    

    现在,你可以看到子shell的退出代码,应该是1,因为子shell内部发生了错误:

    shell-prompt> echo $?
    output: 1
    

    这在我的0.5.8-2.1ubuntu2版本中失败了。$ readlink $(which sh)dash$ . test.sh此脚本被引用。$ ./test.sh此脚本被引用。 - Phil Rutschman
    3
    POSIX标准未规定在顶层使用return应该做什么(http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_24_03)。`dash` shell将在顶层使用的return视为exit。其他像bashzsh这样的shell不允许在顶层使用return,而这正是此技术所利用的特性。 - user5754163
    如果在子shell之前删除$,则它可以在sh中工作。也就是说,使用(return >/dev/null 2>&1)代替$(return >/dev/null 2>&1) - 但这样在bash中停止工作。 - Eponymous
    @Eponymous:由于dash在Ubuntu上的作用类似于sh,因此该解决方案在sh中通常不起作用。该解决方案对我来说在Bash 3.2.57和4.4.5中有效 - 在(...)之前是否有$都可以(尽管没有好的理由使用$)。 - mklement0
    2
    如果在一个坏的退出命令之后立即“source”脚本,没有明确的返回值,则会导致中断。建议进行增强编辑。 - DimG
    即使return在源脚本中编写,但它被调用的上下文是顶层的,因此它应该总是失败。显然,它不会失败,这是一个错误,因此有一天可能会被修复,到那时,这将停止工作。 - Jan Hudec

    21

    提供参考,阅读其他答案后,我为自己想出了以下解决方案:

    更新:实际上,有人发现另一个答案中的错误已经被更正,这也影响到了我的答案。如果您感兴趣,可以查看编辑内容。

    这适用于所有以#!/bin/bash起始的脚本,但是可能会被不同的shell引用,用于学习一些保存在main函数之外的信息(例如设置)。

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

    #!/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 "$@"
    

    您可以使用以下代码代替最后2行,以避免在其他shell中设置BASH_SOURCE并允许set -emain中工作(我认为这种方法不太易读):

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

    这个脚本-配方具有以下属性:
    • 如果以普通方式在bash中执行,则会调用main。请注意,这不包括像bash -x script(其中script不包含路径)这样的调用,请参见下文。

    • 如果由bash引用,则只有当调用脚本具有相同名称时才会调用main。(例如,如果它通过bash -c'someotherscript"$@"'main-script args..源自自身或通过main-script必须是test视为$BASH_SOURCE)。)

    • 如果被除bash之外的其他shell引用/执行/读取/eval,则不会调用mainBASH_SOURCE始终与$0不同)。

    • 如果bash从标准输入读取脚本,则不会调用main,除非您将$0设置为空字符串,如:( exec -a''/bin/bash) <script

    • 如果使用evaleval"`cat script`"所有引号都很重要!)从其他脚本中评估,则调用main。如果直接从命令行运行eval,则类似于上一个案例,在该案例中,脚本从标准输入读取。(BASH_SOURCE为空,而$0通常为/bin/bash,如果没有被强制更改为完全不同的东西。)

    • 如果未调用main,则会返回true$?=0)。

    • 这不依赖于意外行为(以前我写过未记录的内容,但我没有发现您无法取消设置或更改BASH_SOURCE的任何文档),因为:

      • BASH_SOURCE是保留数组。但是,允许BASH_SOURCE=".$0"更改它将打开一个非常危险的问题,因此我的期望是,这必须没有影响(除非在某个未来版本的bash中出现一些丑陋的警告)。
      • 没有文档说明BASH_SOURCE在函数之外起作用。但是相反的情况(它仅在函数中起作用)也没有记录。观察结果是它可以工作(已测试使用bash v4.3和v4.4,不幸的是我不再有bash v3.x),而且相当多的脚本将中断,如果$BASH_SOURCE停止按预期工作。因此,我的期望是,BASH_SOURCE在未来版本的bash中保持不变。
      • 相反(很好的发现,顺便说一句!)考虑(return 0),如果被引用,则返回0,如果没有被引用,则返回1这对我来说有点出乎意料, 根据那里的读数,POSIX表示从子shell返回的行为未定义(而此处的return显然来自子shell)。也许这个功能最终
        因此,除了一些特殊情况,只有在脚本以通常的方式执行时才会调用main函数。通常这就是您想要的,特别是因为它缺乏复杂难以理解的代码。

        Note that it is very similar to the Python code:

        if __name__ == '__main__': main()
        

        Which also prevents calling of main, except for some corner cases, as you can import/load the script and enforce that __name__='__main__'

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

        如果你有一些能被多个shell调用的东西,它必须是兼容的。然而(请参阅其他答案),由于没有(易于实现的)便携式方法来检测源,因此您应该更改规则

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

        这解决了所有情况,但以下情况除外:

        • /bin/bash未安装或不起作用(例如在启动环境中)
        • 如果您将其管道传输到像curl https://example.com/script | $SHELL这样的shell中
        • (注意:只有当您的bash足够新时才是真实的。据报道,此方法对某些变体无效。因此,请务必检查它是否适用于您的情况。)

        但是,我想不出任何真正需要同时引用完全相同的脚本的原因!通常,您可以将其包装以手动执行main。就像这样:

        • $SHELL -c '. 脚本 && 主函数'
        • { curl https://example.com/脚本 && echo && echo 主函数; } | $SHELL
        • $SHELL -c 'eval "`curl https://example.com/脚本`" && 主函数'
        • echo 'eval "`curl https://example.com/脚本`" && 主函数' | $SHELL

        注释


    已测试通过ksh和bash-4.3。不错。很遗憾,由于其他答案已经收集了多年的赞,所以你的答案可能会很艰难。 - hagello
    @Tino: 关于“可能也会被不同的shell调用”,在macOS上,/bin/sh实际上是以POSIX模式运行的bash,将值分配给BASH_SOURCE会_破坏_您的脚本。 在其他shell(dash、ksh、zsh)中,通过将其作为文件参数_直接传递给shell可执行文件_来调用脚本会导致故障(例如,zsh <your-script>会使您的脚本错误地认为它是_被调用的_)。 (您已经提到了在所有shell中都会出现管道代码故障。) - mklement0
    @Tino:顺便说一下:虽然在原则上,从所有类POSIX的shell中使用“.<your-script>”(源)是可行的,但只有当脚本明确编写为仅使用POSIX功能时才有意义,以防止特定于一个shell的功能破坏其他shell中的执行;因此,使用_Bash_ shebang行(而不是#!/bin/sh)会令人困惑-至少没有显眼的注释。 相反,如果您的脚本仅适用于Bash(即使仅仅是因为没有考虑哪些功能可能不可移植),最好在非Bash shell中_refuse_执行。 - mklement0
    @Tino:由于尝试分配给$BASH_SOURCE,因此在v3.2.57中子shell始终失败,因此当您从Bash / Bash作为sh调用时,无论您是否源脚本,都将始终执行main - mklement0
    1
    @mklement0 再次感谢,已经添加了一个注释说明存在问题。对于其他读者: 当在bash v3.x中被调用时,它不应该执行main,但在这种情况下它确实执行了!当通过/bin/sh进行调用时,即使是bash --posix,在这种情况下也会发生同样的情况,这显然是错误的。 - Tino
    显示剩余4条评论

    13

    这个在脚本后面运行,不依赖于变量 _:

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

    3
    我认为这个答案是这里少数符合POSIX标准的答案之一。显而易见的缺点是你必须知道文件名,并且如果两个脚本具有相同的文件名,则无法工作。 - JepZ
    即使需要脚本文件名,这是一个好的简单答案。 - Dig
    这样怎么样:is_sourced() { case $(basename "${0#-}") in sh|bash|ash|dash|ksh) return 0;; *) return 1;; esac } - zerox

    6
    我将提供一个针对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的自省设施,即内置变量FUNCNAMEBASH_SOURCE;请参阅它们在bash手册页面中的文档。
    只有两个注意事项:
    1)对am_I_called的调用必须发生在被引用的脚本中,但不能在任何函数内部,否则${FUNCNAME[1]}会返回其他内容。是的...你可以检查${FUNCNAME[2]}——但那只会让你的生活更难。
    2)如果想要找出所包含文件的名称,am_I_called函数必须位于被引用的脚本中。

    1
    澄清:此功能需要BASH版本3+才能正常工作。在BASH 2中,FUNCNAME是一个标量变量而不是数组。此外,BASH 2没有BASH_SOURCE数组变量。 - Wirawan Purwanto

    3
    我想对Dennis的非常有帮助的答案提出一个小的修正建议,以使其更具可移植性:
    [ "$_" != "$0" ] && echo "Script is being sourced" || echo "Script is a subshell"
    

    因为 (在我看来有点过于苛刻的) Debian 兼容 POSIX shell dash 不认识 [[。此外,在该 shell 中,文件名包含空格时可能需要使用引号进行保护。


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