如何检测脚本是否源


216

我有一个脚本,exit如果它是源代码,我不希望它调用。

我想检查一下是否可以,$0 == bash但是如果脚本是从另一个脚本中获取的,或者用户是从其他shell那里获取的,这就会出现问题ksh

有没有可靠的方法来检测脚本是否源?


2
不久前,我遇到了类似的问题,并且在所有情况下都避免了“退出”,从而解决了该问题;在任何一种情况下,“ kill -INT $$”都会安全地终止脚本。
JESii

1
您注意到这个答案了吗?它从被接受后的5年后才给出,但其中包含“电池”。
raratiru'1

Answers:


72

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

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

pathname="$_"必须在脚本的第一行或shebang之后的行上添加与此类似的行或类似的任务(以及稍后的测试和操作)(如果使用的话,应在ksh中使用,以便在ksh下运行)大多数情况)。


10
不幸的是,不能保证它能正常工作。如果用户已设置BASH_ENV$_则脚本的顶部将是最后一个从中运行的命令BASH_ENV
Mikel

29
如果您使用bash执行脚本,例如$ bash script.sh,则$ _将是/ bin / bash而不是./script.sh,这是您期望的,在调用脚本时这样:$ ./script.sh在任何情况下,检测到$_都是一个问题。
Wirawan Purwanto 2012年

2
可以包括其他测试来检查那些调用方法。
暂停,直到另行通知。

8
不幸的是,这是错误的!看到我的答案
F. Hauri 2014年

8
总结一下:尽管这种方法通常有效,但并不可靠;它在以下两种情况下失败:(a)bash script(通过shell可执行文件调用,此解决方案将其错误地报告为),以及(b)(可能性很小)echo bash; . script(如果$_恰好与提供脚本的shell匹配,则此解决方案将其错误报告为:一个subshel​​l)。只有特定于外壳的特殊变量(例如$BASH_SOURCE)才允许使用健壮的解决方案(因此,没有健壮的POSIX兼容解决方案)。它可能的,虽然麻烦,制作一个健壮的跨壳体试验。
mklement0'3

170

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

11
这可能是最干净的方法,因为$ BASH_SOURCE正是用于此目的。
con-f-use

4
请注意,这在OP指定的条件ksh下不起作用。
暂停,直到另行通知。

2
有使用${BASH_SOURCE[0]}而不是仅仅使用的理由$BASH_SOURCE吗?和${0}vs $0
hraban'1

4
BASH_SOURCE是一个数组变量(请参阅手册),其中包含源的堆栈跟踪,其中${BASH_SOURCE[0]}是最新的。花括号在这里用来告诉bash变量名的一部分。$0在这种情况下,它们不是必需的,但也不会造成伤害。;)
Konrad

4
@Konrad,如果展开$array,则${array[0]}默认情况下。那么,又有原因[...]吗?
查尔斯·达菲,

133

对于强大的解决方案bashkshzsh,包括跨壳一个,再加上合理稳健的POSIX兼容的解决方案

  • 给出的版本号是经过验证的功能的版本号-可能的是,这些解决方案可以在更早的版本上运行,也欢迎反馈

  • 使用POSIX功能(例如in dash,与 /bin/sh在Ubuntu上相同),没有可靠的方法来确定是否源脚本-参见以下最佳近似

单线遵循-以下说明;跨外壳版本很复杂,但是应该可以正常运行:

  • bash(在3.57和4.4.19上验证)

    (return 0 2>/dev/null) && sourced=1 || sourced=0
  • ksh(已在93u +上验证)

    [[ $(cd "$(dirname -- "$0")" && 
       printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] &&
         sourced=1 || sourced=0
  • zsh(在5.0.5上验证)-确保在函数外部调用此函数

    [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] && sourced=1 || sourced=0
  • 跨壳(bash,ksh,zsh)

    ([[ -n $ZSH_EVAL_CONTEXT && $ZSH_EVAL_CONTEXT =~ :file$ ]] || 
     [[ -n $KSH_VERSION && $(cd "$(dirname -- "$0")" &&
        printf '%s' "${PWD%/}/")$(basename -- "$0") != "${.sh.file}" ]] || 
     [[ -n $BASH_VERSION ]] && (return 0 2>/dev/null)) && sourced=1 || sourced=0
  • POSIX兼容 ; 不是一个班轮(单管)因技术原因而无法完全健壮(参见下图):

    sourced=0
    if [ -n "$ZSH_EVAL_CONTEXT" ]; 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|dash) sourced=1;; esac
    fi

说明:


重击

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

注意:该技术是根据user5754163的答案改编而成,因为它比原始解决方案[[ $0 != "$BASH_SOURCE" ]] && sourced=1 || sourced=0[1] 更强大。

  • Bash return仅允许在脚本的顶层作用域中从函数声明语句,并且仅在脚本是source的情况下才声明

    • 如果return非源脚本的顶级范围中使用,则会发出错误消息,并且退出代码设置为1
  • (return 0 2>/dev/null)return子shell中执行并抑制错误消息;之后,退出代码将指示脚本是源(0)还是源(),该脚本1&&||运算符一起使用以相应地设置sourced变量。

    • 必须使用子外壳程序,因为return在源脚本的顶级范围内执行将退出该脚本。
    • @Haozhun的帽子提示,他通过显式地0用作return操作数使命令更加健壮;他指出:根据bash的帮助return [N]:“如果省略N,则返回状态为最后一条命令的返回状态。” 结果,return如果用户外壳程序上的最后一条命令的返回值非零,则较早的版本[仅使用,而没有操作数]会产生错误的结果。

sh

[[ \
   $(cd "$(dirname -- "$0")" && printf '%s' "${PWD%/}/")$(basename -- "$0") != \
   "${.sh.file}" \
]] && 
sourced=1 || sourced=0

特殊变量${.sh.file}有点类似于$BASH_SOURCE;请注意,这${.sh.file}会导致bash,zsh和dash中的语法错误,因此请确保在多shell脚本中有条件地执行它。

不像在bash,$0${.sh.file}不能保证是准确的非采购情况相同,因为$0可能是一个相对路径,而${.sh.file}始终是一个完整的路径,所以$0必须解决一个完整路径比较之前。


sh

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

$ZSH_EVAL_CONTEXT包含有关评估上下文的信息-在函数外部调用此函数。内的执行的脚本['的顶层范围],$ZSH_EVAL_CONTEXT 端部带有:file

警告:在命令替换内,zsh appends :cmdsubst,因此$ZSH_EVAL_CONTEXT:file:cmdsubst$那里进行测试。


仅使用POSIX功能

如果您愿意做出某些假设,则可以基于知道可能正在执行脚本的shell 的二进制文件名,对脚本是否源进行合理,但不是万无一失的猜测。 值得注意的是,这意味着如果您的脚本是由另一个脚本来源的,则此方法将失败。

我的答案中的“如何处理源调用”一节详细讨论了POSIX功能无法处理的极端情况。

依赖于标准的行为$0,其中zsh,例如没有表现。

因此,最安全的方法是将上述健壮的,特定于外壳的方法与所有其余外壳后备解决方案结合起来

斯特凡·德斯纽StéphaneDesneux)的提示和对灵感的回答(将我的跨shell语句表达式转换为sh-compatible if语句,并为其他shell添加处理程序)。

sourced=0
if [ -n "$ZSH_EVAL_CONTEXT" ]; 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|dash) sourced=1;; esac
fi

[1] user1902689发现,通过将[[ $0 != "$BASH_SOURCE" ]]脚本的纯文件名传递给二进制文件来执行位于中的$PATH脚本时会产生误报。例如,因为是那么就,而是完整路径。虽然你通常不会使用这一技术在调用的脚本-你会只是调用它们直接() -它在与结合有用的调试bashbash my-script$0my-script$BASH_SOURCE$PATHmy-script-x


1
如此全面的答案表示感谢。
DrUseful

75

阅读@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改用off .来提高可读性(作为.的别名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,但可以确保仅手动获取案例...

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

将其放入您的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,但这仅适用于一个级别的会话。

抱歉,在以下情况下,我找不到可靠的方法


[ "$0" = "$BASH_SOURCE" ] || [ -z "$BASH_SOURCE" ]用于通过管道(cat script | bash)读取的脚本。
hakre

2
请注意,这.不是的别名source,实际上是相反的。source somescript.sh是Bash-ism,不是便携式的,. somescript.sh是POSIX和便携式IIRC。
dragon788 '18

32

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的评论,因此得到了改进和简化。)

但是,您所说的问题是“ 我有一个脚本,如果要获取它,我不希望它调用'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还提供$-了壳状态的另一个指示(尽管不完美)。


笔记:

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

如何${FUNCNAME[(( ${#FUNCNAME[@]} - 1 ))]}获得堆栈中的最后一个(底部)项目?然后对我来说,针对“ main”(否定OP)进行测试是最可靠的。
阿德里安·昆特

如果有PROMPT_COMMAND集合,则在运行时显示为FUNCNAME数组的最后一个索引source sourcetest.sh。反转检查(寻找main最后一个索引)似乎更可靠:is_main() { [[ ${FUNCNAME[@]: -1} == "main" ]]; }
dimo414 '17

1
手册页指出,FUNCNAME仅在函数中可用。根据我对的测试declare -p FUNCNAMEbash其行为有所不同。v4.3给出函数外部错误,而v4.4给出declare -a FUNCNAME。Both(!)在主脚本中返回mainfor ${FUNCNAME[0]}(如果已执行),$FUNCNAME则不给出任何内容。并且:那里有太多使用$BASH_SOURCE外部函数“滥用”的脚本,我怀疑这是否可以更改。
蒂诺

24

编者注:该答案的解决方案功能强大,但bash仅适用于-。可以简化为
(return 2>/dev/null)

TL; DR

尝试执行一条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

您还希望return在子外壳程序内执行该语句。当return语句运行它。。。好 。。。返回。如果在子外壳程序中执行它,它将从该子外壳程序中返回,而不是从脚本中返回。要在子外壳中执行,请将其包装在中$(...)

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 This script is sourced. $ ./test.sh This script is sourced.
Phil Rutschman

3
POSIX没有指定return在顶层应该做的事情(pubs.opengroup.org/onlinepubs/9699919799/utilities/…)。该dashshell对一个return在顶部水平exit。其他外壳在顶层不允许bashzsh不允许return,这是这种漏洞利用技术的功能。
user5754163 '16

如果您删除$子外壳程序之前的命令,它将在sh中工作。也就是说,使用(return >/dev/null 2>&1)而不是$(return >/dev/null 2>&1)-,但是它将停止在bash中工作。
同名的

@Eponymous:例如,由于dash在该解决方案不起作用sh的情况下,它在Ubuntu上起作用,因此该解决方案通常不适用于sh。该解决方案确实在Bash 3.2.57和4.4.5中对我有用-有或没有$以前(...)(尽管从来没有充分的理由$)。
mklement0

2
return如果source没有正确的命令,在脚本执行时,一个明显的返回值就会中断。建议进行增强编辑。
DimG

12

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

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

这适用于所有以开头的#!/bin/bash脚本,但可能来自不同的shell,也可以学习一些保留在main函数外部的信息(如设置)。

根据下面的评论,此答案显然不适用于所有bash变体。还没有对系统,其中/bin/sh基于bash。IE bash在MacOS上无法运行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 "$@"

您可以使用以下代码(我认为可读性较差)代替最后两行,以免BASH_SOURCE在其他shell中设置并允许set -e在其中运行main

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

此脚本食谱具有以下属性:

  • 如果以bash常规方式执行,main则被调用。请注意,这不包括类似的呼叫bash -x script(其中script不包含路径),请参见下文。

  • 如果源为bashmain则仅在调用脚本碰巧具有相同名称时才调用。(例如,如果它本身就是来源或必须通过bash -c 'someotherscript "$@"' main-script args..何处main-script来来源,则test显示为$BASH_SOURCE)。

  • 如果eval不是由以外的shell来执行/执行/读取/ 编辑的bashmain则不会调用(BASH_SOURCE始终与$0)。

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

  • 如果在其他脚本中bash使用来评估evaleval "`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可以在功能之外使用。但是,相反的内容(仅适用于函数)均未记录。可以观察到的是,它可以正常工作(通过bashv4.3和bashv4.4进行了测试,不幸的是我再没有v3.x了),并且如果$BASH_SOURCE停止运行,则太多脚本将被破坏。因此,我的期望是,BASH_SOURCE与的未来版本一样bash
    • 相比之下(不错的发现,顺便说一句!)考虑( return 0 ),它给出了0是否来源和1是否来源。 POSIX说,这不仅对我来说有点意外,而且(根据那里的阅读资料)POSIX说,return来自子shell的行为是不确定的(return这里显然来自子shell)。也许此功能最终会得到足够的广泛使用,以致无法再进行更改,但是bash在这种情况下,AFAICS很有可能会出现某些将来版本意外更改返回行为的情况。
  • 不幸的bash -x script 1 2 3是没有运行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可以视为一项功能。
  • 请注意,( exec -a none script )调用mainbash不会将其传递$0给脚本,为此,您需要使用-c最后一点所示的方法)。

因此,除某些特殊情况外,main仅在以常规方式执行脚本时才调用。 通常,这就是您想要的,尤其是因为它缺少复杂的难以理解的代码。

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

if __name__ == '__main__': main()

main除某些特殊情况外,这还可以防止调用,因为您可以导入/加载脚本并强制执行__name__='__main__'

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

如果您有某些东西(可以由多个外壳提供),那么它必须兼容。但是(请阅读其他答案),由于没有(易于实现)可移植的方式来检测信号source,因此您应该更改规则

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

解决了所有情况,但以下情况下,脚本无法直接运行:

  • /bin/bash 未安装或无法正常运行(即在引导环境中)
  • 如果您将其通过管道输入到类似 curl https://example.com/script | $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

笔记


经过ksh和bash-4.3测试。真好 遗憾的是,鉴于其他答案已经收集了数年的票数,您的答案将很难过。
hagello

谢谢你的回答。我感谢使用IF语句进行更长的,“可读性更差”的测试,因为可以很好地处理这两种情况,以至少产生非静默故障。就我而言,我需要一个脚本作为来源,或者以其他方式通知用户他们在不使用源代码时的错误。
蒂姆·理查森

@Tino:至于“也可能来自不同的外壳”:在macOS上/bin/sh(实际上是bash在POSIX模式下),分配给BASH_SOURCE 会破坏脚本。在其他炮弹(dashkshzsh),通过将它作为一个文件参数调用脚本直接到shell可执行故障(比如,zsh <your-script>将让你的脚本错误地认为这是来源)。(你已经提到,管道代码故障,在所有的炮弹。)
mklement0

@Tino:顺便说一句:尽管. <your-script>(采购)原则上可以在所有类似POSIX的外壳中工作,但是只有明确编写脚本以仅使用POSIX功能才有意义,以防止特定于一个外壳的功能破坏执行在其他炮弹中;因此,使用Bash shebang行(而不是#!/bin/sh)会造成混淆-至少没有明显的注释。相反,如果您的脚本仅打算从Bash运行(即使仅由于不考虑哪些功能可能不可移植而已),则最好在非Bash Shell中拒绝执行。
mklement0

1
@ mklement0再次感谢,添加了说明,这是一个问题。 对于其他读者:当使用bash v3.x采购时,不应执行main,但是在这种情况下,它将执行此操作!而当通过采购/bin/sh,也就是bash --posix,同样的情况在这种情况下,这是完全错误的为好。
Tino

6

这将在脚本的更高版本中起作用,并且不依赖于_变量:

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

1
我认为这个答案是这里少数POSIX兼容的之一。明显的缺点是您必须知道文件名,如果两个脚本具有相同的文件名,则文件名将不起作用。
JepZ

5

我将给出一个针对BASH的答案。可恩壳,对不起。假设您的脚本名称是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 2012年

4

我想对丹尼斯的非常有帮助的答案提出一些小建议,以使其更便于携带,我希望:

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

因为[[(未被肛门保留的恕我直言)Debian POSIX兼容外壳无法识别dash。同样,在所述外壳中,可能还需要引号来防止包含空格的文件名。


2

$_很脆。您必须先将其检查为脚本中的第一件事。即使这样,也不能保证包含外壳程序的名称(如果已获取)或脚本的名称(如果已执行)。

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

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

name="myscript.sh"

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

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

不幸的是,由于zsh的功能functionargzero超出了其名称所建议的范围,并且默认情况下处于启用状态,因此无法在zsh中立即使用。

为解决此问题,我放入unsetopt functionargzero.zshenv


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+uksh ./myscript.sh对我来说效果很好(带有我的声明)-您使用的是哪个版本?
mklement0

我担心没有办法可靠地确定是否仅使用POSIX功能来获取脚本:您的尝试假定使用Linux(/proc/$$/cmdline)且dash仅专注于(sh例如,在Ubuntu上也是如此)。如果您愿意做出某些假设,则可以检查$0一种可移植的合理(但不完整)的测试。
mklement0

但是,对于基本方法++来说- 在我的回答的附录中,我自由地将其调整为支持sh/ 的最佳便携式近似值dash
mklement0

0

我认为ksh和bash中都没有任何可移植的方式来执行此操作。在bash中,您可以使用caller输出检测到它,但是我不认为ksh中存在等效的东西。


$0工程bashksh93pdksh。我不必ksh88测试。
Mikel

0

我需要一种可以在bash.version> = 3的[mac,linux]上使用的单线工具,而这些答案都不符合要求。

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

1
bash解决方案效果很好(您可以简化为$BASH_SOURCE),但是该ksh解决方案并不可靠:如果您的脚本是由另一个脚本提供的,则会得到误报。
mklement0

0

直截了当:要评估变量“ $ 0”是否等于命令行管理程序的名称。


像这样:

#!/bin/bash

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


通过SHELL

$ bash check_source.sh 
First Parameter: check_source.sh

The script WAS NOT sourced.

通过SOURCE

$ source check_source.sh
First Parameter: bash

The script was sourced.



要以100%可移植的方式检测脚本是否源代码是非常困难的。

关于我的经验(使用Shellscripting已有7年),这是唯一安全的方法(不依赖带有PID的环境变量,由于它是VARIABLE,因此不安全),您应该:

  • 扩展您的可能性
  • 如果需要,请使用开关/外壳。

这两个选项都无法自动缩放,但这是更安全的方法。



例如:

当您通过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

2
Downvoted,因为这是完全错误的: /bin/bash -c '. ./check_source.sh'The script WAS NOT sourced.。同样的错误:ln -s /bin/bash pumuckl; ./pumuckl -c '. ./check_source.sh'->The script WAS NOT sourced.
Tino

2
蒂诺(Tino),您的不赞成票改变了整个情况,并做出了巨大贡献。谢谢!
ivanleoncz

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

这是从其他一些答案中衍生出来的,涉及“通用”跨壳支持。诚然,这与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只是在确保此机制可以扩展到子进程中使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.