如何检测脚本是否被引用

我有一个脚本,如果它被引用,我不希望它调用exit

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

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

86931 次浏览

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

如果您的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 ..."

这似乎在Bash和Korn之间是可移植的:

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

与此类似的行或像pathname="$_"这样的赋值(带有稍后的测试和操作)必须位于脚本的第一行或shebang之后的行(如果使用了shebang,则应该用于ksh,以便它在大多数情况下工作)。

我想建议对丹尼斯的回答非常有用进行一个小的修正,让它更易于携带,我希望:

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

因为Debian的POSIX兼容 shell, dash不能识别[[。同样,在shell中,可能需要使用引号来防止文件名中包含空格。

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

## 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

$_相当脆弱。你必须检查它作为你在脚本中做的第一件事。即使这样,它也不保证包含shell的名称(如果是源的)或脚本的名称(如果是执行的)。

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

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

name="myscript.sh"


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


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

不幸的是,这种方式在zsh中并不能开箱使用,因为functionargzero选项的功能比其名称所暗示的要多,并且默认情况下是打开的。

为了解决这个问题,我将unsetopt functionargzero放在我的.zshenv中。

我将给出一个特定于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 必须驻留在源脚本中,如果你想知道被包含的文件的名称。

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")'

这相当于检查caller的输出,值mainsource区分调用者的上下文。使用FUNCNAME[]可以节省捕获和解析caller输出的时间。你需要知道或计算你的本地调用深度是正确的。像脚本来自另一个函数或脚本这样的情况会导致数组(堆栈)更深。(FUNCNAME是一个特殊的bash数组变量,它应该有对应于调用堆栈的连续索引,只要它永远不是unset。)

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

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

然而,你的问题是“我有一个脚本,我不希望它调用'退出',如果它是来源”。对于这种情况,常见的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开始)。几乎肯定不是你想要依靠的东西……

看了@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)

这已经不管用了。

将注释从第5行移到第6行会给出更易读的答案:

./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

更难:现在…

由于我不经常使用,在阅读了一些手册页后,有我的尝试:

#!/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

采购运行中继承了一些变量,但没有真正相关的…

你甚至可以检查$SECONDS是否接近0.000,但这确保只有manualy采购的情况…

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

把它放入你的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,但这只适用于一个级别的子会话…

对不起,我找不到一个可靠的方法来做到这一点,在

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

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

__ABC0, __ABC1, zsh的健壮解决方案,包括cross-shell,加上合理健壮的posix兼容解决方案:

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

  • 使用POSIX特性(例如在dash中,它在Ubuntu上充当/bin/sh),有没有robust方式来确定脚本是否被引用-请参阅下面的最佳近似

重要的:

  • 解决方案确定脚本是否由调用者获取,它可能是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
      
  • < p > 以下所有解决方案必须在脚本的顶级作用域中运行 不是在函数内部

一行程序 follow -解释如下;跨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兼容;由于技术原因而不是一行程序(单一管道)和Not fully robust(见底部):
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语句,并且在脚本的顶层作用域中,仅当脚本为采购时才允许return语句。

    • 如果return非源代码脚本的顶层作用域中使用,则会发出错误消息,并将退出码设置为1
  • (return 0 2>/dev/null)亚层中执行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中附加。


仅使用POSIX特性

如果你愿意做一些假设,你可以根据知道二进制文件名的shell可能正在执行你的脚本来确定你的脚本是否被引用。
值得注意的是,这意味着该方法当你的脚本被另一个脚本获取时,不检测这种情况.

.

“如何处理源调用”一节;在中,这个答案讨论了不能被POSIX特征处理的边缘情况。

检查二进制文件名依赖于__ABC0的标准行为,例如,zsh没有而不是

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

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

  • 在覆盖了特定于shell的测试的shell中:健壮地工作。

  • 在所有其他shell中:仅当脚本从这样的shell(而不是从另一个脚本)中获得直接时才能正常工作。

Stéphane Desneux他的答案致敬(将我的跨shell语句表达式转换为__abc0兼容的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。 (虽然shdash可能不太可能被用作互动外壳,但你可能需要将其他外壳添加到列表中)


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

编者注:这个答案的解决方案工作稳健,但是bash-only。它可以精简为
(return 2>/dev/null)。< /一口> < / p >

博士TL;

尝试执行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表示OK(没有发生错误),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

我遵循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

请随意添加异国情调的贝壳支持:)

开门见山:你必须计算变量“0”是否等于你的Shell的名称。

< p > < br > 这样的:< / p >
#!/bin/bash


echo "First Parameter: $0"
echo
if [[ "$0" == "bash" ]] ; then
echo "The script was sourced."
else
echo "The script WAS NOT sourced."
fi
< p > < br > 通过壳:

$ bash check_source.sh
First Parameter: check_source.sh


The script WAS NOT sourced.
< p > 通过源:

$ source check_source.sh
First Parameter: bash


The script was sourced.

< br >


使用100%便携方式来检测脚本是否被引用是相当困难的。

根据我的经验(使用Shellscripting 7年),唯一安全的方法(不依赖于环境变量pid等,这是不安全的,因为它是变量),你应该:

  • 扩展你的“如果”的可能性
  • 使用开关/箱子,如果你想。

这两个选项都不能自动缩放,但这是更安全的方式。


< br >

例如: < br >

当您通过SSH会话获取脚本源代码时,变量"$0"(当使用source时)返回的值为-bash

#!/bin/bash


echo "First Parameter: $0"
echo
if [[ "$0" == "bash" || "$0" == "-bash" ]] ; then
echo "The script was sourced."
else
echo "The script WAS NOT sourced."
fi

#!/bin/bash


echo "First Parameter: $0"
echo
if [[ "$0" == "bash" ]] ; then
echo "The script was sourced."
elif [[ "$0" == "-bash" ]] ; then
echo "The script was sourced via SSH session."
else
echo "The script WAS NOT sourced."
fi

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

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

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

根据下面的评论,这里的答案显然不适用于所有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 -emain中工作:

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

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

  • 如果由bash以正常方式执行,则调用main。请注意,这并不包括像bash -x script这样的调用(其中script不包含路径),见下文。

  • 如果由bash派生,则只有在调用脚本恰好具有相同的名称时才会调用main。(例如,如果它在main-script必须在的地方来源自身或通过bash -c 'someotherscript "$@"' main-script args..,则test将视为$BASH_SOURCE)。

  • 如果由bash以外的shell获取/执行/读取/evaled,则不会调用main (BASH_SOURCE始终不同于$0)。

  • 如果bash从stdin读取脚本,则不会调用main,除非将$0设置为空字符串,如下所示

  • 如果在其他脚本中使用basheval (eval "`cat script`" 所有引号都是重要的!)求值,则调用main。如果eval直接从命令行运行,这与前面的情况类似,脚本从stdin读取。(BASH_SOURCE是空的,而$0通常是/bin/bash,如果不是强制的完全不同。)

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

  • 这不依赖于意外行为(之前我写的是无文档的,但我发现没有文档表明你不能unset也不能改变BASH_SOURCE):

    • BASH_SOURCE是一个bash保留数组。但是允许BASH_SOURCE=".$0"改变它将打开一个非常危险的蠕虫罐,所以我的预期是,这必须没有任何影响(除非,也许,一些丑陋的警告出现在bash的未来版本中)。
    • 没有文档说明BASH_SOURCE在函数之外工作。然而,相反的情况(它只在函数中工作)也没有文档。观察到,它工作(用bash v4.3和v4.4测试,不幸的是我没有bash v3。如果$BASH_SOURCE像观察到的那样停止工作,那么相当多的脚本将会中断。因此,我的期望是,BASH_SOURCEbash的未来版本也保持不变。
    • 相比之下(不错的发现,顺便说一下!)考虑( return 0 ),如果是源则给出0,如果没有源则给出1这不仅对我来说有点出乎意料,和(根据那里的读数)POSIX说,return from subshell是未定义的行为(这里的return显然来自subshell)。也许这个特性最终得到足够广泛的使用,以至于它不能再被改变,但是AFAICS有更高的可能性,在这种情况下,将来的某个bash版本意外地改变了返回行为。
    • 李< / ul > < / >
    • 不幸的是,__ABC0不能运行main(比较script 1 2 3,其中script没有路径)。以下可以作为变通方法:

      • bash -x "`which script`" 1 2 3
      • bash -xc '. script' "`which script`" 1 2 3
      • bash script 1 2 3不能运行main可以被认为是一个特性。
      • 李< / ul > < / >
      • 注意,( exec -a none script )调用main (bash不会将它的$0传递给脚本,为此你需要使用-c,如上一点所示)。

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

      注意,它与Python代码非常相似:

      if __name__ == '__main__': main()
      
      这也防止调用main,除了一些极端情况,如 你可以导入/加载脚本并强制__name__='__main__'

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

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

      通过强制脚本必须由/bin/bash执行,你确实做到了这一点。

      这个解决除以下以外的所有情况在这种情况下脚本不能直接运行:

      • /bin/bash未安装或失效(即在引导环境中)
      • 如果你像curl https://example.com/script | $SHELL那样将它管道到一个shell
      • (注意:这只在你的bash足够近的时候才成立。据报道,这种配方对某些变体无效。所以一定要检查它是否适用于你的情况。)

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

      • $SHELL -c '. script && 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中发现的新发现而编辑

最后我检查了[[ $_ == "$(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

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

解决这个问题的方法不是编写需要知道这些事情才能正确运行的代码。做到这一点的方法是将代码放入函数中,而不是放入需要源代码的脚本主线中。

函数内部的代码只能是return 0return 1。这只终止了函数,因此控制返回到调用该函数的任何对象。

无论从源脚本的主线调用函数,还是从顶级脚本的主线调用函数,或者从另一个函数调用函数,都是如此。

利用资源引进“图书馆”;只定义函数和变量,但实际上不执行任何其他顶级命令的脚本:

. 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

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

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

  • 将所有顶级操作包装在run_main()函数中。
  • 让您的源脚本检查初始--no-run参数,该参数执行任何操作;没有 --no-run,它可以调用run_main
  • source脚本使用:
set -- --no-run "$@"
. script.sh
shift

.source的问题是不可能将参数可移植地传递给脚本。POSIX shell忽略.的参数,无论如何都会传递调用者的"$@"

确定bash脚本是采购还是执行的4种方法

我已经阅读了关于这个问题和其他一些问题的一堆答案,并提出了4种我想要总结并放在一个地方的方法。

技巧4是我最喜欢的,因为它看起来最像Python的版本:

if __name__ == "__main__":

参见:什么if __name__ = =“__main__":做什么?了解它在Python中的作用。

  1. 你可以在我的eRCaGuy_hello_world repo中的check_if_sourced_or_executed.sh脚本中看到下面所有4种技术的完整演示。
  2. 你可以看到下面的技术4在我的高级bash程序中使用,带有帮助菜单、参数解析、main函数、自动执行 vs 检测(类似于Python中的__name__ == "__main__":)等,请参阅我的演示/模板程序在这个列表中。它目前被称为argument_parsing__3_advanced__gen_prog_template.sh,但如果这个名称在未来发生变化,我将在上面的链接列表中更新它

不管怎样,这里有4个Bash技术:

  1. 技巧一(可以放在任何地方): 看到:https://unix.stackexchange.com/questions/424492/how-to-define-a-shell-script-to-be-sourced-not-run/424495#424495 < / p >

    if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then
    echo "  This script is being EXECUTED."
    run="true"
    else
    echo "  This script is being SOURCED."
    fi
    
  2. 技巧2 [我的第二喜欢](可以放在任何地方): 修改自:如果__name__ == '__main__' ', bash等价于Python's '是什么?

    if [ "${BASH_SOURCE[0]}" == "$0" ]; then
    echo "  This script is being EXECUTED."
    run="true"
    else
    echo "  This script is being SOURCED."
    fi
    
  3. 技巧3(需要另一行必须是所有函数): 修改自:如何检测脚本是否被引用

    # 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. 技巧4 [我的第一个最爱](必须是内部一个函数): 修改自:如何检测脚本是否被引用
    Unix和Linux:如何定义要获取而不运行的shell脚本。< / p >

    在我最先进的bash演示脚本中可以看到这种技术的使用,这里:argument_parsing__3_advanced__gen_prog_template.sh,在靠近底部的名为run_check的函数中。

    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. [我的回答]bash等价于Python的if __name__ == '__main__'是什么?
  2. [我的回答]Unix和Linux:如何定义要获取而不运行的shell脚本

@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*应该修复这个问题。

使用shebang行并检查它是否正在执行。

你的脚本应该有一个shebang行#!/path/to/shell,说明它应该在哪个shell中运行。否则,您还会遇到其他跨shell兼容性问题。

因此,您只需要通过尝试在源时不起作用的命令来检查它是否正在执行。

如。对于Bash脚本:

#!/usr/bin/env bash


if (return 0 2>/dev/null); then
echo "Script was sourced."
fi

这个方法也适用于zsh和sh只是改变shebang行。