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

337

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

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

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


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

3

最美观的检测Bash脚本被执行或导入(引入)的方法

我真的认为这是最美观的做法:

从我的if__name__==__main___check_if_sourced_or_executed_best.sh文件中,可以找到一个更好的实现方式。

我的eRCaGuy_hello_world代码库也包含这个文件。

#!/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
    echo "This script is being run."
    main
else
    echo "This script is being sourced."
fi

参考资料:

  1. 另外一个回答详细介绍了上述技巧,包括展示运行输出: 在Bash中,等价于Python的if __name__ == '__main__'的是什么?
  2. 这个回答是我第一次了解"${BASH_SOURCE[0]}" = "$0"

如果你愿意,你还可以探索以下替代方法,但我更喜欢使用上面的代码块。

重要提示: 使用"${FUNCNAME[-1]}"技术不适用于处理嵌套脚本,其中一个脚本调用或源另一个脚本,而if [ "${BASH_SOURCE[0]}" = "$0" ]技术则完全适用。这是使用if [ "${BASH_SOURCE[0]}" = "$0" ]的另一个重要原因。

4种确定bash脚本是否被引用执行的方法

我已经阅读了各种回答并结合其他问题,总结出了4种方法,并将它们放在同一个地方以便概括。

if __name__ == "__main__":

请参考Python中 if __name__ == "__main__": 的用法。

  1. 您可以在我的check_if_sourced_or_executed.sh脚本中,以及我的eRCaGuy_hello_world存储库中看到所有4种技术的完整演示。
  2. 您可以在我的高级bash程序中看到其中一种技术的使用,该程序带有帮助菜单、参数解析、main函数、自动检测执行 vs 源(类似于 Python 中的 if __name__ == "__main__":)等功能。请查看我在这里列出的展示/模板程序(链接在此处),当前称为argument_parsing__3_advanced__gen_prog_template.sh,但如果名称在将来发生更改,我将在上面链接的列表中更新它。

无论如何,这里是 4 种 Bash 技术:

  1. Technique 1 (can be placed anywhere; handles nested scripts): See: https://unix.stackexchange.com/questions/424492/how-to-define-a-shell-script-to-be-sourced-not-run/424495#424495

    if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then
        echo "  This script is being EXECUTED."
        run="true"
    else
        echo "  This script is being SOURCED."
    fi
    
  2. Technique 2 [My favorite technique] (can be placed anywhere; handles nestes scripts): See this type of technique in-use in my most-advanced bash demo script yet, here: argument_parsing__3_advanced__gen_prog_template.sh, near the bottom.

    Modified from: What is the bash equivalent to Python's `if __name__ == '__main__'`?

    if [ "${BASH_SOURCE[0]}" == "$0" ]; then
        echo "  This script is being EXECUTED."
        run="true"
    else
        echo "  This script is being SOURCED."
    fi
    
  3. Technique 3 (requires another line which MUST be outside all functions): Modified from: How to detect if a script is being sourced

    # A. Place this line OUTSIDE all functions:
    (return 0 2>/dev/null) && script_is_being_executed="false" || script_is_being_executed="true"
    
    # B. Place these lines anywhere
    if [ "$script_is_being_executed" == "true" ]; then
        echo "  This script is being EXECUTED."
        run="true"
    else
        echo "  This script is being SOURCED."
    fi
    
  4. Technique 4 [Limitation: does not handle nested scripts!] (MUST be inside a function): Modified from: How to detect if a script is being sourced
    and Unix & Linux: How to define a shell script to be sourced not run.

    if [ "${FUNCNAME[-1]}" == "main" ]; then
        echo "  This script is being EXECUTED."
        run="true"
    elif [ "${FUNCNAME[-1]}" == "source" ]; then
        echo "  This script is being SOURCED."
    else
        echo "  ERROR: THIS TECHNIQUE IS BROKEN"
    fi
    
    1. This is where I first learned about the ${FUNCNAME[-1]} trick: @mr.spuratic: How to detect if a script is being sourced - he learned it from Dennis Williamson apparently.

另请参阅:

  1. [我的回答] 什么是Bash的等效于Python的 if __name__ == '__main__' 的代码?
  2. [我的回答] Unix & Linux:如何定义一个shell脚本以被源引用而非运行

2

$_非常脆弱。在脚本中的第一件事就是要检查它。即使这样做了,也不能保证包含你的Shell名称(如果被引用)或脚本名称(如果被执行)。

例如,如果用户设置了BASH_ENV,那么在脚本顶部,$_包含上一个在BASH_ENV脚本中执行的命令的名称。

我发现最好的方法是像这样使用$0

name="myscript.sh"

main()
{
    echo "Script was executed, running main..."
}

case "$0" in *$name)
    main "$@"
    ;;
esac

很遗憾,在zsh中,由于functionargzero选项默认开启且具有更多功能,这种方法无法直接使用。

为了解决这个问题,我在我的.zshenv文件中添加了unsetopt functionargzero命令。


2

虽然不完全符合OP的要求,但我经常需要引用脚本来加载其函数(即作为库),例如用于基准测试或测试目的。

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

  • 将所有顶级操作都包装在run_main()函数中。
  • 让您引用的脚本检查是否有初始的--no-run参数,该参数不会执行任何操作;如果没有--no-run,则可以调用run_main
  • 使用以下命令source脚本:
set -- --no-run "$@"
. script.sh
shift
< p >使用 . source 的问题在于无法便携地传递参数给脚本。 POSIX shell忽略 . 的参数并始终传递调用者的"$@"。< /p >

非常有趣的答案。尝试实现这一点是我寻找检测脚本是否被引用的解决方案的原因。谢谢。 - ncarrier

1

我按照mklement0紧凑表达式的方式进行了操作。

这很不错,但我注意到在以下情况下,它可能会在ksh中失败:

/bin/ksh -c ./myscript.sh

它认为自己有来源,但实际上并没有,因为它执行了一个子shell。但以下表达式可以用来检测这个问题:

/bin/ksh ./myscript.sh

此外,即使表达式很紧凑,语法也不兼容所有shell。因此,我最终使用了以下代码,适用于bash、zsh、dash和ksh。
SOURCED=0
if [ -n "$ZSH_EVAL_CONTEXT" ]; then 
    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && SOURCED=1
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
    [[ $0 != "$BASH_SOURCE" ]] && SOURCED=1
elif grep -q dash /proc/$$/cmdline; then
    case $0 in *dash*) SOURCED=1 ;; esac
fi

随意添加外来的 shell 支持 :)


ksh 93+u 中,ksh ./myscript.sh 对我来说很好用(与我的语句一起)- 你使用的是哪个版本? - mklement0
我担心没有可靠的方式可以仅使用POSIX功能来确定脚本是否正在被源文件调用:您的尝试假定了Linux(/proc/$$/cmdline),并且仅关注于dash(在Ubuntu上也充当sh) 。如果您愿意做出某些假设,可以检查$0以进行合理但不完整的测试,这是一种可移植的方法。 - mklement0
++ 是基本方法,但我已经自行修改以适应最佳便携式近似支持 sh / dash 的情况,并在我的答案中添加了附录。 - mklement0
由于提到了“与所有shell兼容”和“异类shell”,因此${0##*/}$()不可移植至至少一个旧的Bourne shell(我仍然必须使用它,即使这是必要的,这是一种耻辱):echo ${0##*/}会产生“bad substitution”的结果,而echo $(dirname $0)则会出现syntax error: '(' unexpectedecho $0会输出-sh - kbulgrien

1

对于@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*可以解决这个问题。请注意保留HTML标签。

1
这个问题的解决方法是不要编写需要知道这种情况才能正确执行的代码。做法是将代码放入函数中,而不是放入需要被调用的脚本的主体中。
函数内部的代码可以直接返回01。这样只会终止函数的执行,控制权就会返回到调用该函数的地方。
无论是从被调用脚本的主体、顶层脚本的主体还是其它函数中调用该函数,这种方式都适用。
使用源文件引入“库”脚本,这些脚本仅定义函数和可能的变量,但不实际执行任何其他顶层命令:
. path/to/lib.sh # defines libfunction
libfunction arg

或者:
path/to/script.sh arg # call script as a child process

而不是:

. path/to/script.sh arg  # shell programming anti-pattern

1

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


$0bashksh93pdksh 中可用。我没有 ksh88 进行测试。 - Mikel

0

我最终检查了[[ $_ == "$(type -p "$0")" ]]

if [[ $_ == "$(type -p "$0")" ]]; then
    echo I am invoked from a sub shell
else
    echo I am invoked from a source command
fi

当使用curl ... | bash -s -- ARGS即时运行远程脚本时,$0将仅为bash而不是正常的/bin/bash,因此我使用type -p "$0"来显示bash的完整路径。

测试:

curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath | bash -s -- /a/b/c/d/e /a/b/CC/DD/EE

source <(curl -sSL https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath)
relpath /a/b/c/d/e /a/b/CC/DD/EE

wget https://github.com/jjqq2013/bash-scripts/raw/master/common/relpath
chmod +x relpath
./relpath /a/b/c/d/e /a/b/CC/DD/EE

0
似乎适用于bash、zsh、ksh和dash。
我注意到在使用dash执行的文件中进行源代码检测时出现了问题。
executingName="${0##*/}"
separator='+'
SHS="${separator?}sh${separator?}dash${separator?}-sh${separator?}-dash${separator?}"
sourced="${BASH_VERSION:+$( 
  ( 2>/dev/null return 0 ) || test 0 -eq ${??} ;
)${??
}}${ZSH_VERSION:+$(
  test "${ZSH_EVAL_CONTEXT##*:file}" != "${ZSH_EVAL_CONTEXT-}" ;
)${??
}}${KSH_VERSION:+$(
  test "${executingName?
    }" = "${0-}" -o "${executingName?
    }" != "${.sh.file##*/}"
)${??}}$(
  test "${SHS#*${separator?}${executingName?
    }${separator?}}" != "${SHS?
    }" && printf '%s' "${??}"
)"
printf '%9s' "$(
  test 0 -ne ${sourced:-8} && printf "unsourced" || printf "sourced"
)"

主要基于https://dev59.com/GXE85IYBdhLWcg3wpFEa#28776166,其中包含有关参数扩展的“cheatsheet”信息https://steinbaugh.com/posts/posix.html,并将case-esac更改为我在https://dev59.com/F3I_5IYBdhLWcg3wBuRs#43912605上看到的替换方法。

0
这应该符合sh标准,特别是符合busybox ash(它没有上述方便的bash式一行命令)。
这与上面jim mcnamara的解决方案有相同的思路,但不是将文件名硬编码为变量,而是获取它并比较基本名称。 代码来自如何在busybox ash中获取源文件名
THIS_FILE="$(lsof | grep '^'$$ | tail -n1 | awk '{print $3}')"
[ "${0##*/}" != "${THIS_FILE##*/}" ] && sourced='yes' || sourced='no'

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