如何获得“set -e”的效果和用处?在 shell 函数内?

发布于 2024-11-03 00:37:12 字数 1435 浏览 5 评论 0原文

set -e(或以#!/bin/sh -e 开头的脚本)对于在出现问题时自动轰炸非常有用。它使我不必对每个可能失败的命令进行错误检查。

如何在函数内获得与此等效的内容?

例如,我有以下脚本,该脚本在出现错误时立即退出,并显示错误退出状态:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

输出符合预期:

the following command could fail:

现在我想将其包装到函数中:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

预期输出:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

实际输出:(

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

即函数忽略set -e

这可能是预期的行为。我的问题是:如何在 shell 函数内获得 set -e 的效果和用处?我希望能够进行一些设置,这样我就不必单独检查每个调用的错误,但脚本将在遇到错误时停止。它应该根据需要展开堆栈,直到我检查结果为止,或者如果我没有检查结果,则退出脚本本身。这就是 set -e 已经做的事情,只是它不嵌套。

我发现同样的问题 在 Stack Overflow 外询问,但没有合适的答案。

set -e (or a script starting with #!/bin/sh -e) is extremely useful to automatically bomb out if there is a problem. It saves me having to error check every single command that might fail.

How do I get the equivalent of this inside a function?

For example, I have the following script that exits immediately on error with an error exit status:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

The output is as expected:

the following command could fail:

Now I'd like to wrap this into a function:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Expected output:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Actual output:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(ie. the function is ignoring set -e)

This presumably is expected behaviour. My question is: how do I get the effect and usefulness of set -e inside a shell function? I'd like to be able to set something up such that I don't have to individually error check every call, but the script will stop on encountering an error. It should unwind the stack as far as is needed until I do check the result, or exit the script itself if I haven't checked it. This is what set -e does already, except it doesn't nest.

I've found the same question asked outside Stack Overflow but no suitable answer.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

多孤肩上扛 2024-11-10 00:37:12

我最终选择了这个,这显然是有效的。我一开始尝试了导出方法,但后来发现我需要导出脚本使用的每个全局(常量)变量。

禁用 set -e,然后在启用了 set -e 的子 shell 中运行函数调用。将子shell的退出状态保存在变量中,重新启用set -e,然后测试该变量。

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

您不能将 f 作为管道的一部分运行,也不能作为 || 命令列表的 && 的一部分运行(除非管道或列表中的最后一个命令),或作为 ifwhile 中的条件,或其他忽略 set -e 的上下文。 此代码也不能出现在任何这些上下文中,因此,如果您在函数中使用此代码,调用者必须使用相同的 subshel​​l / save-exit-status 技巧。考虑到限制和难以阅读的语法,这种使用 set -e 实现类似于抛出/捕获异常的语义并不真正适合一般用途。

trap err_handler_function ERRset -e 具有相同的限制,因为它不会在 set -e 不会触发的上下文中触发错误t 在命令失败时退出。

您可能认为以下内容可行,但事实并非如此:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e 在子 shell 内不起作用,因为它记住它位于 if 的条件内。我认为作为一个子 shell 会改变这一点,但只有在一个单独的文件中并在其上运行一个完整的单独 shell 才可以工作。

I eventually went with this, which apparently works. I tried the export method at first, but then found that I needed to export every global (constant) variable the script uses.

Disable set -e, then run the function call inside a subshell that has set -e enabled. Save the exit status of the subshell in a variable, re-enable set -e, then test the var.

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

You can't run f as part of a pipeline, or as part of a && of || command list (except as the last command in the pipe or list), or as the condition in an if or while, or other contexts that ignore set -e. This code also can't be in any of those contexts, so if you use this in a function, callers have to use the same subshell / save-exit-status trickery. This use of set -e for semantics similar to throwing/catching exceptions is not really suitable for general use, given the limitations and hard-to-read syntax.

trap err_handler_function ERR has the same limitations as set -e, in that it won't fire for errors in contexts where set -e won't exit on failed commands.

You might think the following would work, but it doesn't:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e doesn't take effect inside the subshell because it remembers that it's inside the condition of an if. I thought being a subshell would change that, but only being in a separate file and running a whole separate shell on it would work.

凶凌 2024-11-10 00:37:12

来自 set -e 的文档:

启用此选项后,如果任何简单命令失败
后果中列出的原因
Shell 错误或返回退出状态
值> 0,并且不属于
while 之后的化合物列表,
untilif 关键字,并且不是
ANDOR 列表的一部分,并且不是
前面带有 ! 保留的管道
字,那么外壳应立即
退出。

在您的情况下,false 是管道的一部分,前面是 ! 的一部分 if。因此,解决方案是重写您的代码,使其不再存在。

换句话说,这里的函数没有什么特别的。尝试:

set -e
! { false; echo hi; }

From documentation of set -e:

When this option is on, if a simple command fails for any of the
reasons listed in Consequences of
Shell Errors or returns an exit status
value > 0, and is not part of the
compound list following a while,
until, or if keyword, and is not a
part of an AND or OR list, and is not
a pipeline preceded by the ! reserved
word, then the shell shall immediately
exit.

In your case, false is a part of a pipeline preceded by ! and a part of if. So the solution is to rewrite your code so that it isn't.

In other words, there's nothing special about functions here. Try:

set -e
! { false; echo hi; }
薄荷港 2024-11-10 00:37:12

您可以直接使用子 shell 作为函数定义,并使用 set -e 将其设置为立即退出。这会将 set -e 的范围限制为仅函数子 shell,并且稍后可以避免在 set +eset -e 之间切换。

此外,您可以在 if 测试中使用变量赋值,然后在附加的 else 语句中回显结果。

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a

You may directly use a subshell as your function definition and set it to exit immediately with set -e. This would limit the scope of set -e to the function subshell only and would later avoid switching between set +e and set -e.

In addition, you can use a variable assignment in the if test and then echo the result in an additional else statement.

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a
横笛休吹塞上声 2024-11-10 00:37:12

这有点混乱,但你可以这样做:

export -f f
if sh -ec f; then 
...

如果你的 shell 支持 export -f (bash 支持),这将起作用。

请注意,这不会终止脚本。回声
f 中的 false 后不会执行,主体也不会执行
if 的一部分,但是 if 后面的语句会被执行。

如果您使用的 shell 不支持 export -f,您可以
通过在函数中运行 sh 来获取所需的语义:

f() { sh -ec '
  echo This will execute
  false
  echo This will not
  '
}

This is a bit of a kludge, but you can do:

export -f f
if sh -ec f; then 
...

This will work if your shell supports export -f (bash does).

Note that this will not terminate the script. The echo
after the false in f will not execute, nor will the body
of the if, but statements after the if will be executed.

If you are using a shell that does not support export -f, you can
get the semantics you want by running sh in the function:

f() { sh -ec '
  echo This will execute
  false
  echo This will not
  '
}
时光磨忆 2024-11-10 00:37:12

注意/编辑:正如评论者指出的那样,这个答案使用 bash,而不是像他的问题中使用的 OP 那样的 sh。当我最初发布答案时,我错过了这个细节。无论如何,我都会保留这个答案,因为某些路人可能会对它感兴趣。

你准备好了吗?

这是一种利用 DEBUG 陷阱来实现此目的的方法,该陷阱在每个命令之前运行,并且会产生类似于其他语言中的整个异常/try/catch 习惯用法的错误。看看吧。我已经让你的例子又加深了一次“调用”。

#!/bin/bash

# Get rid of that disgusting set -e.  We don't need it anymore!
# functrace allows RETURN and DEBUG traps to be inherited by each
# subshell and function.  Plus, it doesn't suffer from that horrible
# erasure problem that -e and -E suffer from when the command 
# is used in a conditional expression.
set -o functrace

# A trap to bubble up the error unless our magic command is encountered 
# ('catch=$?' in this case) at which point it stops.  Also don't try to
# bubble the error if were not in a function.
trap '{ 
    code=$?
    if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
        # If were in a function, return, else exit.
        [[ $FUNCNAME ]] && return $code || exit $code
    fi
}' DEBUG

my_function() {
    my_function2
}

my_function2() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

# the || isn't necessary, but the 'catch=$?' is.
my_function || catch=$?
echo "Dealing with the problem with errcode=$catch (⌐■_■)"

echo "run this all the time regardless of the success of my_function"

和输出:

the following command could fail:
Dealing with the problem with errcode=1 (⌐■_■)
run this all the time regardless of the success of my_function

我还没有在野外测试过这个,但是在我的脑海中,有很多优点:

  1. 实际上并没有那么慢。我在使用和不使用 functrace 选项的紧密循环中运行了该脚本,并且在 10 000 次迭代下时间非常接近。

  2. 您可以扩展此 DEBUG 陷阱来打印堆栈跟踪无需对 $FUNCNAME 和 $BASH_LINENO 进行整个循环。你可以免费得到它(除了实际做回声线)。

  3. 不必担心 shopt -s Heritage_errexit 问题。

Note/Edit: As a commenter pointed out, this answer uses bash, and not sh like the OP used in his question. I missed that detail when I originaly posted an answer. I will leave this answer up anyway since it might be interested to some passerby.

Y'aaaaaaaaaaaaaaaaaaallll ready for this?

Here's a way to do it with leveraging the DEBUG trap, which runs before each command, and sort of makes errors like the whole exception/try/catch idioms from other languages. Take a look. I've made your example one more 'call' deep.

#!/bin/bash

# Get rid of that disgusting set -e.  We don't need it anymore!
# functrace allows RETURN and DEBUG traps to be inherited by each
# subshell and function.  Plus, it doesn't suffer from that horrible
# erasure problem that -e and -E suffer from when the command 
# is used in a conditional expression.
set -o functrace

# A trap to bubble up the error unless our magic command is encountered 
# ('catch=$?' in this case) at which point it stops.  Also don't try to
# bubble the error if were not in a function.
trap '{ 
    code=$?
    if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
        # If were in a function, return, else exit.
        [[ $FUNCNAME ]] && return $code || exit $code
    fi
}' DEBUG

my_function() {
    my_function2
}

my_function2() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

# the || isn't necessary, but the 'catch=$?' is.
my_function || catch=$?
echo "Dealing with the problem with errcode=$catch (⌐■_■)"

echo "run this all the time regardless of the success of my_function"

and the output:

the following command could fail:
Dealing with the problem with errcode=1 (⌐■_■)
run this all the time regardless of the success of my_function

I haven't tested this in the wild, but off the top of my head, there are a bunch of pros:

  1. It's actually not that slow. I've ran the script in a tight loop with and without the functrace option, and times are very close to each other under 10 000 iterations.

  2. You could expand on this DEBUG trap to print a stack trace without doing that whole looping over $FUNCNAME and $BASH_LINENO nonsense. You kinda get it for free (besides actually doing an echo line).

  3. Don't have to worry about that shopt -s inherit_errexit gotcha.

蝶舞 2024-11-10 00:37:12

使用 && 运算符连接函数中的所有命令。这并不是太麻烦,并且会给出您想要的结果。

Join all commands in your function with the && operator. It's not too much trouble and will give the result you want.

別甾虛僞 2024-11-10 00:37:12

这是由设计和 POSIX 规范决定的。我们可以在 man bash 中读到:

如果复合命令或 shell 函数在 -e 被忽略的上下文中执行,则在复合命令或函数体内执行的任何命令都不会受到 -e 的影响 设置,即使设置了 -e 并且命令返回失败状态。如果复合命令或 shell 函数在忽略 -e 的上下文中执行时设置了 -e,则在复合命令或包含以下内容的命令之前,该设置不会产生任何效果:函数调用完成。

因此,您应该避免在函数中依赖set -e

给出以下示例 Austin Group

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

函数中的 set -e 被忽略,因为该函数是 AND-OR 列表中的命令,而不是最后一个命令。

上述行为是 POSIX 指定和要求的(请参阅:所需操作):

执行whileuntilif之后的复合列表时,应忽略-e设置,或 elif 保留字,以 ! 保留字开头的管道,或 AND-OR 列表中除最后一个以外的任何命令。

This is by design and POSIX specification. We can read in man bash:

If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

therefore you should avoid relying on set -e within functions.

Given the following exampleAustin Group:

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

the set -e is ignored within the function, because the function is a command in an AND-OR list other than the last.

The above behaviour is specified and required by POSIX (see: Desired Action):

The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.

饮惑 2024-11-10 00:37:12

我知道这不是您所要求的,但您可能知道也可能不知道您寻求的行为内置于“make”中。 “make”过程的任何部分失败都会中止运行。不过,这是一种与 shell 脚本完全不同的“编程”方式。

I know this isn't what you asked, but you may or may not be aware that the behavior you seek is built into "make". Any part of a "make" process that fails aborts the run. It's a wholly different way of "programming", though, than shell scripting.

躲猫猫 2024-11-10 00:37:12

您需要在子 shell 中调用函数(在方括号 () 内)才能实现此目的。

我想你想这样写你的脚本:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

然后输出是(根据需要):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

You will need to call your function in a sub shell (inside brackets ()) to achieve this.

I think you want to write your script like this:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Then the output is (as desired):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
白鸥掠海 2024-11-10 00:37:12

如果子shell不是一个选项(假设你需要做一些疯狂的事情,比如设置一个变量),那么你可以检查每个可能失败的命令并通过附加 || 来处理它。返回 $?。这会导致函数在失败时返回错误代码。

它很丑陋,但它确实

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

有效

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

If a subshell isn't an option (say you need to do something crazy like set a variable) then you can just check every single command that might fail and deal with it by appending || return $?. This causes the function to return the error code on failure.

It's ugly, but it works

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

gives

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
我的奇迹 2024-11-10 00:37:12

基于@antak的答案的可重用bash函数:

function try {
  # Run the args in a `set -e` mode.
  # Error code should be checked with `((ERR))`.
  # Please don't check it directly with `if try`, `try ||`, etc.
  # Explained: https://github.com/denis-ryzhkov/cheatsheets/tree/main/bash/try

  set +e
  (set -e; "$@")
  declare -gx ERR=$?
  set -e
}

使用示例:

function func {
  echo "func args: $@"
  func-bug
  echo func-never
}

try func arg1 arg2
if ((ERR))
then echo "func failed"
else echo "func succeeded"
fi

输出:

func args: arg1 arg2
./test.sh: line 3: func-bug: command not found
func failed

详细说明此处

Reusable bash function based on the answer by @antak:

function try {
  # Run the args in a `set -e` mode.
  # Error code should be checked with `((ERR))`.
  # Please don't check it directly with `if try`, `try ||`, etc.
  # Explained: https://github.com/denis-ryzhkov/cheatsheets/tree/main/bash/try

  set +e
  (set -e; "$@")
  declare -gx ERR=$?
  set -e
}

Usage example:

function func {
  echo "func args: $@"
  func-bug
  echo func-never
}

try func arg1 arg2
if ((ERR))
then echo "func failed"
else echo "func succeeded"
fi

Output:

func args: arg1 arg2
./test.sh: line 3: func-bug: command not found
func failed

Details are explained here.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文