如何跳出一个来源于Bash脚本的函数

9
我有一个被引用的Bash脚本,在被引用时,它会运行Bash脚本中的一个函数。如果满足某个条件,该函数应终止脚本。如何在不终止引用脚本的shell的情况下实现此操作?
明确一点:我希望终止操作是由被引用的shell脚本中的函数完成的,而不是由被引用的shell脚本的主体完成的。我能看到的问题是,return仅从函数返回到脚本的主体,而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

1
执行脚本即可。这样,exit 将会退出它所在的子 shell。 - fedorqui
1
有趣。你能举个例子吗?比如一个能终止shell的例子。 - Ludwig Schulze
你要求的是不可能的。你必须重构你的代码。 - Karoly Horvath
5
这段脚本旨在设置大量的环境变量,所以我认为必须将其作为源代码来执行。 - d3pd
@LudwigSchulze 按照您的要求,我已经添加了一个完整的最小脚本示例,当被调用时,它可以说明问题。return不会终止被调用的脚本(只会终止函数),而exit 1则会终止调用的shell。我需要介于两者之间的解决方案! - d3pd
7个回答

9
您可以从源shell脚本中返回POSIX规范 因此,虽然您无法直接从函数中返回以获取所需内容,但是如果您的函数返回非零值(或其他约定的值),则可以从脚本的主体中返回。
例如:
$ 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

1
谢谢你的解决方案。我知道这个方法。我的目的是找到一种在脚本内部的一个函数中终止源脚本的方法,因为额外的复杂性使得在一个最小的示例中不太可能包含,所以将终止程序集中在一个函数中非常理想。 - d3pd
1
@d3pd,你不行。除非使用子shell,否则不可能实现。但是你可以在函数中保留任何逻辑。你只需要在ifensure_environment || return中调用该函数,或者类似的语句。也许你可以通过检查 $? 或其他变量的 RETURN 陷阱来实现一些操作。 - Etan Reisner

3
这是一份配方,告诉你如何通过你的方法实现目标。我不会为你编写代码,只是描述如何完成。
你的目标是在当前bash shell中设置/更改环境变量,有效地通过源化可能复杂的shell脚本来实现。该脚本的某个组件可能决定停止执行此源脚本。使这变得复杂的是,这个决策不一定是顶级的,而可能位于嵌套的函数调用中。因此,“返回”无法帮助,“退出”将终止源壳,这是不希望的。
你的任务由于以下陈述而变得更容易:
“我无法在最小示例中包含的其他复杂性使得在函数中集中终止过程非常有吸引力。”
以下是您的操作步骤:
不要源化真正的脚本来决定要将哪个环境设置为什么(“realscript.bash”),而是源化另一个脚本“ipcscript.bash”。
ipcscript.bash将设置一些进程间通信。这可以是您使用exec打开的某些额外文件描述符上的管道,也可以是临时文件,也可以是其他内容。
ipcscript.bash然后将realscript.bash作为子进程启动。这意味着realscript.bash首先进行的环境更改仅影响bash的该子进程实例的环境。通过将realscript.bash作为子进程启动,您获得了在任何嵌套级别上使用exit终止执行的能力,而不终止源壳。
当决定终止执行时,如你所写,“退出”调用将位于一个集中的函数中。您的终止函数现在需要在退出之前以适当的格式将当前环境写入IPC机制。
ipcscript.bash将从IPC机制中读取环境设置,并在源壳的过程中复制所有设置。

3

这是我喜欢的解决方案(下面会解释其副作用):

#!/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内(作为第一条指令)检查返回代码是否等于CUSTOM_ERROR_CODE,并仅对具有CUSTOM_ERROR_CODE值(此处任意选择13)的源脚本返回。这意味着在任何地方返回CUSTOM_ERROR_CODE(由于shopt -s extdebug,否则只是第一级函数/命令)都应该产生所需的结果,即结束脚本。
副作用: [01] CUSTOM_ERROR_CODE中的错误代码可能会被脚本控制之外的命令使用,从而强制脚本在没有明确指示的情况下结束。这应该很容易避免,但可能会引起一些不适。 [02] 调用shopt -s extdebug可能会导致不需要的行为,具体取决于脚本中的其他因素。详情请参见:https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html [03] 更重要的是,这是在干净的环境中连续三次引用脚本的输出:
# 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调用,但没有定论。在我的测试中它并没有引起问题,但是强烈建议提供任何澄清说明。


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

为了让 $@ 保持其属性,它基本上应该始终被双引号括起来。这里并不是真的很重要,但对我来说,它立即触发了一个新手警报,所以你可能想出于这个原因修复它。 - tripleee
没错,如果可以用 Ruby 的话,我会避免使用 Bash。已更正。 - Ludwig Schulze
吹毛求疵:if $STILL_OK 是一个安全漏洞。最好使用 if [ "$STILL_OK" = "true" ],即使它不够优雅。否则,如果您实现了任何导致此源代码行的路径而没有设置 STILL_OK 的情况,您将执行当前环境中为 STILL_OK 设置的任何内容。可能什么也不会发生,但永远不知道。 - Alfe

2
这段文字的意思是:“不可能。如果你引用一个脚本,就像逐行输入调用(源)shell 中的每一行。你想离开一个不存在的作用域(被引用的脚本),所以它无法被离开。我能想到的唯一方法是将退出请求传回到调用函数并检查它。”
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

你所要求的通常在其他语言中是不可能的。通常每个函数只能终止自身(通过返回),而不能终止其外部更广泛定义的范围(例如它所在的脚本)。 这个规则的一个例外是使用try/catch或类似的异常处理
此外,请考虑以下情况:如果您源代码执行此脚本,则 shell 函数将成为源 shell 中已知的函数。因此,您可以稍后再次调用它们。然后(再次)没有周围的作用域可以终止该函数。

1

这是可能的。

像在任何编程语言中一样,可以“引发异常”,这将向上传播到调用链:

# 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来消除错误,清除


1
有时我会编写一些方便的函数脚本,希望可以在脚本之外使用。如果运行脚本,则会执行其功能。但是如果脚本被引用,则只需将一些函数加载到引用的 shell 中。 我使用以下形式:
#!/bin/bash

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

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

# the rest of the script goes here

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