在shell中编写try catch finally

101

是否有类似于Java中的try catch finally的Linux bash命令?还是Linux shell总是继续执行下去?

try {
   `executeCommandWhichCanFail`
   mv output
} catch {
    mv log
} finally {
    rm tmp
}

请参见https://dev59.com/ElrUa4cB1Zd3GeqPnc0u。 - Jon Cairns
在Shell脚本中的异常处理 - Abimaran Kugathasan
@KugathasanAbimaran 不得不使用关键字 error-handling 而非 try catch finally,感谢提供的链接! - Jetse
相关:在Bash脚本中引发错误 - codeforester
6个回答

168

根据您的示例,看起来您正在尝试执行类似于始终删除临时文件的操作,无论脚本如何退出。在Bash中,可以使用trap内置命令来捕获EXIT信号以实现此目的。

#!/bin/bash

trap 'rm tmp' EXIT

if executeCommandWhichCanFail; then
    mv output
else
    mv log
    exit 1 #Exit with failure
fi

exit 0 #Exit with success
trap 中的 rm tmp 语句总是在脚本退出时执行,因此文件 "tmp" 总是会尝试被删除。 已安装的陷阱也可以重置;仅使用信号名称调用 trap 将重置信号处理程序。
trap EXIT

更多详细信息,请参阅Bash手册页面:man bash


4
使用“trap”的好处之一是,除了EXIT信号外,您还可以捕获其他信号,特别是SIGINT(Control-C)信号。只需将其附加到trap语句的末尾即可。例如,trap 'rm tmp' EXIT SIGINT - ishmael
10
根据快速测试结果,似乎EXIT处理程序也会在接收到SIGINTSIGTERM信号时被调用。 - Cuadue
3
这绝对是最清洁的做法。 - chesterbr
5
好的回答;可能值得指出的是,触发陷阱的执行不会影响脚本的退出状态——这是与 if/else|| 方法相比的优势,并且更像 C++ 或 Java 中的 try/catch - Toby Speight
2
进一步解释上面的评论:在我的测试中,Control-C会先触发SIGINT,然后是EXIT。因此,对于正在讨论的目的,捕获SIGINT是多余的,实际上捕获两者会导致您的清理命令被执行两次——这很可能不是您想要的。另一方面,捕获SIGTERM会导致程序在收到kill信号时不终止。kill -9跳过所有三个陷阱。因此,在这种情况下不建议捕获SIGTERM。(如果您确实决定捕获SIGTERM出于某种原因,请在陷阱处理程序中调用'exit'。) - benkc
显示剩余2条评论

115

嗯,有点类似:

{ # your 'try' block
    executeCommandWhichCanFail &&
    mv output
} || { # your 'catch' block
    mv log
}

 rm tmp # finally: this will always happen

7
请注意,在使用executeCommandWhichCanFail后,需要使用&&,否则程序会继续执行。即使在其前面加上set -e(我不理解这一点)也是如此。 - AJP
7
简洁明了。但我更喜欢使用trap,因为||不能确保在异常情况(信号)下执行另一部分,而这几乎是人们从finally期望的。 - Petr
你可以考虑使用 (...) 而不是 {...},这样你就不必在每一行上都使用 && 了。 - Alexander Mills
@AJP 这种奇怪的行为至少有文档记录:“如果复合命令或shell函数在忽略-e的情况下执行,则在复合命令或函数体内执行的所有命令都不会受到-e设置的影响,[...]” https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin 即使是( )子shell组也适用,尽管-e通常会传播到子shell。 - rakslice

2
我在脚本中使用以下语法取得了成功:

我在脚本中使用以下语法取得了成功:

# Try, catch, finally
(echo "try this") && (echo "and this") || echo "this is the catch statement!"

# this is the 'finally' statement
echo "finally this"

如果任何一个try语句抛出错误或以exit 1结束,则解释器将转到catch语句,然后再执行finally语句。

如果两个try语句都成功(和/或以exit结束),则解释器将跳过catch语句,然后运行finally语句。

示例_1:

goodFunction1(){
  # this function works great
  echo "success1"
}

goodFunction2(){
  # this function works great
  echo "success2"
  exit
}

(goodFunction1) && (goodFunction2) || echo "Oops, that didn't work!"

echo "Now this happens!"

输出_1

success1
success2
Now this happens!

示例_2

functionThrowsErr(){
  # this function returns an error
  ech "halp meh"
}

goodFunction2(){
  # this function works great
  echo "success2"
  exit
}

(functionThrowsErr) && (goodFunction2) || echo "Oops, that didn't work!"

echo "Now this happens!"

输出_2

main.sh: line 3: ech: command not found
Oops, that didn't work!
Now this happens!

示例三

functionThrowsErr(){
  # this function returns an error
  echo "halp meh"
  exit 1
}

goodFunction2(){
  # this function works great
  echo "success2"
}

(functionThrowsErr) && (goodFunction2) || echo "Oops, that didn't work!"

echo "Now this happens!"

输出_3

halp meh
Oops, that didn't work!
Now this happens!

请注意,函数的顺序会影响输出。如果您需要分别尝试和捕获两个语句,请使用两个try catch语句。
(functionThrowsErr) || echo "Oops, functionThrowsErr didn't work!"
(goodFunction2) || echo "Oops, good function is bad"

echo "Now this happens!"

输出

halp meh
Oops, functionThrowsErr didn't work!
success2
Now this happens!

如果其中一个“try语句”引发错误(抛出异常),那怎么办?它如何处理错误(异常)? - faza
如果try语句中的一个返回错误或者存在"exit 1"命令,解释器将会执行catch语句,然后执行finally语句。但是,如果try语句通过"exit"命令或者成功完成而退出,那么解释器将直接执行finally语句,而不会执行catch语句。 - DogeCode
我更新了我的答案,并提供了示例,展示了在不同的情况下如何处理错误,具体取决于你的程序流程所需。 - DogeCode

1

mv 命令需要两个参数,因此您可能真正想要的是使用 cat 命令查看输出文件的内容:

echo `{ execCommand && cat output ; } || cat log`
rm -f tmp

-1

另一种方法是:

set -e;  # stop on errors

mkdir -p "$HOME/tmp/whatevs"

exit_code=0

(
  set +e;
  (
    set -e;
    echo 'foo'
    echo 'bar'
    echo 'biz'
  )
  exit_code="$?"
)

rm -rf "$HOME/tmp/whatevs"

if [[ "exit_code" != '0' ]]; then
   echo 'failed';
fi 

虽然上述内容并没有比以下内容更有优势:

set -e;  # stop on errors

mkdir -p "$HOME/tmp/whatevs"

exit_code=0

(
    set -e;
    echo 'foo'
    echo 'bar'
    echo 'biz'
    exit 44;
    exit 43;

) || {
   exit_code="$?"  # exit code of last command which is 44
}

rm -rf "$HOME/tmp/whatevs"

if [[ "exit_code" != '0' ]]; then
   echo 'failed';
fi 

-1

警告:退出陷阱并非总是被执行。自从我写下这个答案以来,我遇到了一些情况,在这些情况下,我的退出陷阱不会被执行,导致文件丢失,但我还没有找到原因。

问题出现在我使用Ctrl+C停止Python脚本时,Python脚本又执行了一个使用退出陷阱的Bash脚本,实际上应该导致退出陷阱在Bash中被执行,因为退出陷阱在Bash中是在SIGINT信号时被执行的。

因此,虽然trap .. exit对于清理很有用,但仍有一些场景无法执行,最明显的是停电和接收到SIGKILL信号。


我经常发现随着我添加额外选项或进行其他更改,bash脚本变得越来越大。当一个bash脚本包含许多函数时,使用“trap EXIT”可能会变得不太容易。

例如,考虑作为调用的脚本

dotask TASK [ARG ...]

每个 TASK 可能包含子步骤,在这种情况下,希望在中间执行清理操作。

在这种情况下,使用子 shell 生成作用域退出陷阱是有帮助的,例如:

function subTask (
    local tempFile=$(mktemp)
    trap "rm '${tempFile}'" exit
    ...
)

然而,与子shell一起工作可能会很棘手,因为它们无法设置父shell的全局变量。

此外,编写单个退出陷阱通常是不方便的。例如,清理步骤可能取决于函数在遇到错误之前走了多远。能够进行RAII样式的清理声明将是很好的选择:

function subTask (
    ...
    onExit 'rm tmp.1'
    ...
    onExit 'rm tmp.2'
    ...
)

使用类似的东西似乎是显而易见的

handlers=""
function onExit { handlers+="$1;"; trap "$handlers" exit; }

为了更新陷阱,但这对于嵌套的子shell来说是失败的,因为它会导致父shell处理程序的过早执行。客户端代码必须在子shell开始时显式重置handlers变量。

[多个bash陷阱使用相同的信号]中讨论的解决方案,通过使用trap -p EXIT的输出来修补陷阱,同样会失败:尽管子shell不继承EXIT陷阱,trap -p exit将显示父shell的处理程序,因此需要手动重置。


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