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但没有合适的答案。
答案 0 :(得分:15)
来自set -e
的文档:
启用此选项时,如果任何一个简单命令失败 在“后果”中列出的原因 Shell错误或返回退出状态 值> 0,并不属于
while
后的复合列表,until
或if
关键字,而不是AND
或OR
列表的一部分,而不是。!
或false
列表的一部分 以!
保留为前面的管道 一句话,然后贝壳应立即 退出。
在您的情况下,if
是set -e
! { false; echo hi; }
之前的管道的一部分,而是{{1}}的一部分。因此,解决方案是重写代码,使其不是。
换句话说,这里的功能没什么特别之处。尝试:
{{1}}
答案 1 :(得分:11)
您可以直接使用子shell作为函数定义,并将其设置为立即使用set -e
退出。这会将set -e
的范围仅限于函数子shell,以后会避免在set +e
和set -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
作为管道的一部分运行,也不能作为&&
||
命令列表的一部分运行(管道或列表中的最后一个命令除外),或作为if
或while
中的条件,或忽略set -e
的其他上下文中的条件。 此代码也不能出现在任何上下文中,因此如果您在函数中使用此代码,则调用者必须使用相同的subshell / save-exit-status技巧。考虑到限制和难以阅读的语法,使用set -e
语法类似于抛出/捕获异常并不适合一般用途。
trap err_handler_function ERR
与set -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):
在执行
-e
,while
,until
或if
保留字后面的复合列表时,应忽略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
我还没有在野外进行测试,但是在我的头顶上,有很多专家:
实际上并不慢。无论是否使用functrace
选项,我都在一个紧密的循环中运行了该脚本,在1万次迭代之后,时间非常接近。
您可以扩展此DEBUG陷阱以打印堆栈跟踪而无需在$ FUNCNAME和$ BASH_LINENO废话中执行整个循环。您可以免费获得它(实际上是在做回声线)。
不必担心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 $?