如何突破 sourced Bash 脚本的功能

How to break out of a sourced Bash script's function

我有一个 Bash 源脚本。获取此脚本后,它会在 Bash 脚本中运行一个函数。如果满足特定条件,此函数应终止脚本。如何在不终止脚本来源的 shell 的情况下完成此操作?

明确一点:我希望终止操作由来源 shell 脚本中的函数完成,而不是来源 shell 脚本的主体。我可以看到的问题是 return 只是 returns 从函数到脚本的主体,而 exit 1 终止了调用 shell.

以下最小示例说明了问题:

main(){
    echo "starting test of environment..."
    ensure_environment
    echo "environment safe -- starting other procedures..."
}

ensure_environment(){
    if [ 1 == 1 ]; then
        echo "environment problemm -- terminating..."
        # exit 1 # <-- terminates calling shell
        return   # <-- returns only from function, not from sourced script
    fi
}

main

您可以 return 从来源 shell 脚本。 POSIX spec

因此,虽然您不能 return 直接从函数中获取您想要的内容,但您 可以 return 从脚本的主体中获取如果您的函数 return 非零(或其他一些商定的值)。

例如:

$ cat foo.sh
f() {
    echo in f "$@"
}

e() {
    return 2
}

f 1
e
f 2
if ! e; then
    return
fi
f 3
$ . foo.sh
in f 1
in f 2

这个怎么样:通过一个简单的包装器调用所有内容,此处 "ocall",维护全局状态,此处 "STILL_OK"

STILL_OK=true

ocall() {
    if $STILL_OK 
    then
       echo -- "$@" # this is for debugging, you can delete this line
       if "$@"
       then
          true 
       else
          STILL_OK=false
       fi
    fi
}

main(){
    ocall echo "starting test of environment..."
    ocall ensure_environment
    ocall echo "environment safe -- starting other procedures..."
}

ensure_environment(){
    if [ 1 == 1 ]; then
        ocall echo "environment problemm -- terminating..."
        # exit 1 # <-- terminates calling shell
        return 1  # <-- returns from sourced script but leaves sourcing shell running
    fi
}

ocall main

不可能。

如果您获取一个脚本(对于此处涉及的方面),就像在调用(获取)中逐行输入每一行 shell。你想留下一个不存在的范围(源脚本),所以不能留下。

我能想到的唯一方法是将 exit-wish 传递回调用函数并检查它:

main() {
    echo "starting test of environment..."
    [ "$(ensure_environment)" = "bailout" ] && return
    echo "environment safe -- starting other procedures..."
}

ensure_environment() {
    if [ 1 == 1 ]; then
        echo "bailout"
        return
    fi
}

main

您要求的内容通常也无法用其他语言实现。通常每个函数只能终止自身(通过返回),而不是自身之外更广泛的定义范围(就像它所在的脚本)。 An exception to this rule is exception handling 使用 try/catch 或类似的方法。

另请考虑:如果您获取此脚本的源代码,shell 函数将在源代码 shell 中已知。所以你可以稍后再打电话给他们。然后(再次)没有函数可以终止的周围范围。

这是一个如何通过您的方法实现目标的秘诀。我不会为您编写代码,只是描述它是如何完成的。

您的目标是 set/alter 当前 bash shell 中的环境变量,有效地获取可能复杂的 shell 脚本。该脚本的某些组件可能会决定停止执行该源脚本。使这变得复杂的是这个决定不一定是顶级的,但可能位于嵌套函数调用中。 return,然后,没有帮助,exit 将终止采购 shell,这是不希望的。

你的这句话使你的任务变得更容易:

additional complexity that I can't really include in a minimal example makes it very desirable to centralise the termination procedure in a function.

这是你的做法:

您不是获取决定将哪个环境设置为什么的真实脚本 ("realscript.bash"),而是获取另一个脚本“ipcscript.bash”。

ipcscript.bash 将设置一些进程间通信。这可能是你用 exec 打开的一些额外文件描述符的管道,它可能是一个临时文件,它可能是别的东西。

ipcscript.bash 然后将 realscript.bash 作为子进程启动。这意味着,realscript.bash 首先执行的环境更改只会影响 bash 的子进程实例的环境。将 realscript.bash 作为子进程启动,您可以获得在任何嵌套级别使用 exit 终止执行而不终止源 shell.

的能力

正如您所写,您对退出的调用将存在于一个集中函数中,当决定终止执行时,该函数会从任何级别调用。您的终止函数现在需要在退出之前将当前环境以合适的格式写入 IPC 机制。

ipcscript.bash将从IPC机制中读取环境设置并重现采购过程中的所有设置shell。

有时我编写的脚本具有方便的功能,我想在脚本之外使用这些功能。在这种情况下,如果脚本是 运行,那么它会执行它的操作。但是如果脚本是 source 的,它只是将一些函数加载到 sourcing shell。 我使用这种形式:

#!/bin/bash

# This function will be sourcable
foo() {
  echo hello world
}

# end if being sourced
if [[ [=10=] == bash ]]; then
  return
fi

# the rest of the script goes here

可能的。

像在任何编程语言中那样做,"raise an exception" 它将向上传播调用链:

# cat r

set -u

err=

inner () {
   # we want to bailaout at this point:
   # so we cause -u to kick in:
   err="reason: some problem in 'inner' function"
   i=$error_occurred
   echo "will not be called"
}

inner1 () {
   echo before_inner
   inner
   echo "will not be called"
}


main () {
   echo before_inner1
   inner1
   echo "will not be called"
}

echo before_func
main || echo "even this is not shown"

# this *will* be called now, like typing next statement on the terminal:
echo after_main
echo "${err:-}" # if we failed

测试:

# echo $$
9655
# . r  || true
before_func
before_inner1
before_inner
bash: error_occurred: unbound variable
after_main
reason: some problem in 'inner' function
# echo $$
9655

您可以通过 2>/dev/null、清除

消除错误

这是我更喜欢的解决方案(它有副作用,解释如下):

#!/usr/bin/env bash
# force inheritance of ERR trap inside functions and subshells
shopt -s extdebug
# pick custom error code to force script end
CUSTOM_ERROR_CODE=13

# clear ERR trap and set a new one
trap - ERR
trap '[[ $? == "$CUSTOM_ERROR_CODE" ]] && echo "IN TRAP" && return $CUSTOM_ERROR_CODE 2>/dev/null;' ERR

# example function that triggers the trap, but does not end the script
function RETURN_ONE() { return 1; }
RETURN_ONE
echo "RETURNED ONE"

# example function that triggers the trap and ends the script
function RETURN_CUSTOM_ERROR_CODE() { return "$CUSTOM_ERROR_CODE"; }
# example function that indirectly calls the above function and returns success (0) after
function INDIRECT_RETURN_CUSTOM_ERROR_CODE() { RETURN_CUSTOM_ERROR_CODE; return 0; }
INDIRECT_RETURN_CUSTOM_ERROR_CODE
echo "RETURNED CUSTOM ERROR CODE"

# clear traps
trap - ERR
# disable inheritance of ERR trap inside functions and subshells
shopt -u extdebug

输出:

# source source_global_trap.sh
RETURNED ONE
IN TRAP
IN TRAP

描述: 简而言之,代码为 ERR 设置了 trap,但是,在 trap 内部(作为第一条指令)检查 return 代码与 CUSTOM_ERROR_CODE 和 returns 来自源脚本,仅用于 CUSTOM_ERROR_CODE 的值(在本例中任意选择为 13)。这意味着 returning CUSTOM_ERROR_CODE 任何地方(由于 shopt -s extdebug,否则只有第一级 functions/commands)应该产生结束脚本的预期结果。

副作用:

[01] CUSTOM_ERROR_CODE 中的错误代码可能被脚本控制之外的命令使用,因此可以在没有明确指示的情况下强制脚本结束这样做。这应该很容易避免,但会引起一些不适。

[02] 调用 shopt -s extdebug 可能会导致不需要的行为,具体取决于脚本中的其他因素。详情在这里:https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html

[03]更重要的是,这是在干净的环境下sourc脚本的输出,三次,一次又一次:

# exec bash

# source source_global_trap.sh
RETURNED ONE
IN TRAP
IN TRAP

# source source_global_trap.sh
RETURNED ONE
IN TRAP
IN TRAP
IN TRAP

# source source_global_trap.sh
RETURNED ONE
IN TRAP
IN TRAP
IN TRAP

关于为什么会发生这种情况(额外的 trap 调用),我有几种理论,但没有确凿的解释。它在我的测试期间没有造成问题,但强烈建议进行任何澄清。