如何在shell函数中获得“set -e”的效果和实用性?

时间:2010-11-01 20:59:09

标签: shell sh

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之外询问the same question但没有合适的答案。

10 个答案:

答案 0 :(得分:15)

来自set -e的文档:

  

启用此选项时,如果任何一个简单命令失败   在“后果”中列出的原因   Shell错误或返回退出状态   值> 0,并不属于   while后的复合列表,   untilif关键字,而不是   ANDOR列表的一部分,而不是。!false列表的一部分   以!保留为前面的管道   一句话,然后贝壳应立即   退出。

在您的情况下,ifset -e ! { false; echo hi; } 之前的管道的一部分,而是{{1}}的一部分。因此,解决方案是重写代码,使其不是。

换句话说,这里的功能没什么特别之处。尝试:

{{1}}

答案 1 :(得分:11)

您可以直接使用子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

答案 2 :(得分:7)

这有点像kludge,但你可以这样做:

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

如果你的shell支持export -f(bash),这将有效。

请注意,这将终止脚本。回声 在f中的假不会执行,也不会在身体上执行 的if,但是if之后的语句将被执行。

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

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

答案 3 :(得分:7)

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

禁用set -e,然后在启用了set -e的子shell中运行函数调用。将子shell的退出状态保存在变量中,重新启用set -e,然后测试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"

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

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

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

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

set -e不会在子shell中生效,因为它会记住它在if的条件下。我认为作为子shell会改变它,但只是在一个单独的文件中并在其上运行一个完整的单独的shell将起作用。

答案 4 :(得分:3)

这是按设计和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指定并要求上述行为(请参阅:Desired Action):

  

在执行-ewhileuntilif保留字后面的复合列表时,应忽略elif设置使用!保留字,或除最后一个之外的AND-OR列表的任何命令。

答案 5 :(得分:3)

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

准备好准备好了吗?

这是一种利用DEBUG陷阱来实现此目的的方法,该陷阱在每个命令之前运行 ,并且会产生错误,例如来自其他语言的整个exception / 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选项,我都在一个紧密的循环中运行了该脚本,在1万次迭代之后,时间非常接近。

  2. 您可以扩展此DEBUG陷阱以打印堆栈跟踪而无需在$ FUNCNAME和$ BASH_LINENO废话中执行整个循环。您可以免费获得它(实际上是在做回声线)。

  3. 不必担心shopt -s inherit_errexit陷阱。

答案 6 :(得分:1)

使用&&运算符加入函数中的所有命令。这不是太麻烦,而且会给你想要的结果。

答案 7 :(得分:1)

我知道这不是你提出的要求,但你可能会或可能不会意识到你所寻求的行为是建立在“制造”之内的。失败的“make”进程的任何部分都会中止运行。然而,这是一种完全不同的“编程”方式,而不是shell脚本。

答案 8 :(得分:1)

您需要在子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

答案 9 :(得分:0)

如果不是子shell(例如,您需要做一些疯狂的事情,例如设置变量),那么您只需检查可能失败的每个命令并通过附加Option Explicit Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) Private Sub Text1_LostFocus() Sleep 100 DoEvents Text1.SetFocus End Sub 来处理它。这会导致函数在失败时返回错误代码。

这很丑,但是行得通

from tkinter import *
import tkinter.messagebox as tm
import tkinter as tk

class SampleApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self._frame = None
        self.switch_frame(LoginFrame)

    def switch_frame(self, frame_class):
        """Destroys current frame and replaces it with a new one."""
        new_frame = frame_class(self)
        if self._frame is not None:
            self._frame.destroy()
        self._frame = new_frame
        self._frame.pack()


class LoginFrame(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent


        self.label_username = Label(self, text="Username")
        self.label_password = Label(self, text="Password")

        self.entry_username = Entry(self)
        self.entry_password = Entry(self, show="*")

        self.label_username.grid(row=0, sticky=E)
        self.label_password.grid(row=1, sticky=E)
        self.entry_username.grid(row=0, column=1)
        self.entry_password.grid(row=1, column=1)



        self.logbtn = Button(self, text="Login", command=self._login_btn_clicked)
        self.logbtn.grid(columnspan=2)



    def _login_btn_clicked(self):
        # print("Clicked")
        username = self.entry_username.get()
        password = self.entry_password.get()

        # print(username, password)

        if username == "abc" and password == "123":
            self.parent.switch_frame(PageOne)
        else:
            tm.showerror("Login error", "Incorrect username or password")

class PageOne(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        tk.Label(self, text="This is page one").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Return to login page",
                  command=lambda: parent.switch_frame(LoginFrame)).pack()

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

给予

|| return $?