如果在Bash脚本中出现特定条件,如何退出整个脚本?

936

我正在编写一个Bash脚本来测试一些代码。但是如果首次编译代码失败,运行测试看起来很愚蠢,这种情况下我将中止测试。

是否有一种方法可以在不将整个脚本包装在while循环中并使用break的情况下实现这一点?类似于dun dun dun跳转的方式?

9个回答

1086

4
@CMCDragonkai,通常任何非零代码都是有效的。如果您不需要特别的代码,可以一致地使用 1。如果脚本是由另一个脚本运行的,则您可能需要定义自己的一组具有特定含义的状态码。例如,1表示测试失败,2表示编译失败。如果脚本是其他内容的一部分,则可能需要调整代码以匹配该内容使用的实践方法。例如,当作为由automake运行的测试套件的一部分时,代码77用于标记测试被跳过。 - Michał Górny
37
没有,这会关闭窗口,而不仅仅是退出脚本。 - Toni Leigh
13
@ToniLeigh,Bash 没有“窗口”的概念,你可能对你特定的设置(例如终端仿真器)所做的事情感到困惑。 - Michael Foukarakis
9
如果关闭“窗口”,很可能是将 exit # 命令放在了一个函数而不是脚本中。此时应该使用 return # 命令。 - Jamie
5
exit 命令只会退出运行脚本的 bash 进程。如果你使用 ./script.shbash script.sh 运行脚本,你的“窗口”或者 shell 会保持打开状态,因为执行脚本时会创建一个新的 bash 进程。相反地,如果你使用 . script.shsource script.sh 运行脚本,它将在当前的 bash 实例中执行并退出该实例。 - rebane2001
显示剩余2条评论

811

使用set -e

#!/bin/bash

set -e

/bin/command-that-fails
/bin/command-that-fails2

如果脚本执行到第一行失败(返回非零退出码),它将会终止。在这种情况下,command-that-fails2 不会被执行。

如果你要检查每个命令的返回状态,你的脚本会长成这样:

#!/bin/bash

# I'm assuming you're using make

cd /project-dir
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

cd /project-dir2
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

如果使用 set -e,它会像这样:

#!/bin/bash

set -e

cd /project-dir
make

cd /project-dir2
make
任何失败的命令都会导致整个脚本失败,并返回一个退出状态,您可以使用$?检查。如果您的脚本非常长或者要构建很多东西,如果您在每个地方都添加返回状态检查,那么它将变得非常丑陋。

12
使用set -e指令仍然可以使一些带有错误的命令退出脚本,但您可以使用command 2>&1 || echo $?命令实现部分命令出现错误时不停止脚本的功能。请注意,该命令需要进行细微调整以适应上下文。 - Adobe
6
如果一个管道或命令结构返回非零值,set -e 将中止脚本。例如,foo || bar 仅在 foobar 都返回非零值时失败。通常,如果在开头添加 set -e,良好编写的 Bash 脚本将正常工作,并且这种添加可以作为自动化的健全性检查:如果出现任何问题,就中止脚本。 - Mikko Rantalainen
8
如果你将多个命令串联在一起,你可以通过设置set -o pipefail选项来使整个命令链失败,以便在任何一个命令失败时都能及时得到通知。 - Jake Biesinger
21
实际上,没有 set -e 的惯用代码只需是 make || exit $? - tripleee
5
你也需要执行set -u命令。请参考非官方的 Bash 严格模式set -euo pipefail - Pablo Bianchi
显示剩余4条评论

317

有一次一位系统运维员教了我三指爪技巧:

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }

这些函数适用于*NIX操作系统和各种shell。将它们放在脚本(bash或其他)的开头,try() 调用你的语句和代码。

说明

(基于flying sheep的评论)。

  • yell: 将脚本名称和所有参数打印到 stderr
    • $0是脚本的路径;
    • $*是所有参数。
    • >&2表示将>标准输出重定向到管道符号&后的2管道符号&后的1会是stdout本身。
  • dieyell相同,但以非0退出状态退出,这意味着“失败”。
  • try使用布尔运算符||,只有在左侧失败时才计算右侧。
    • $@再次是所有参数,但不同

5
我对Unix脚本编程还比较新,你能解释一下上面的函数是如何执行的吗?我看到了一些我不熟悉的新语法。谢谢。 - kaizenCoder
17
yell: $0 是脚本的路径。$* 表示所有参数。>&2 意味着“> 将 stdout 重定向到 & 管道 2”。管道1将是stdout本身。因此,yell在所有参数前加上脚本名并打印到stderr。dieyell的作用相同,但会以非零退出状态退出,这意味着“失败”。try使用布尔或||,仅在左侧未失败时才评估右侧。 $@ 再次表示所有参数,但是含义不同。希望这解释了一切。 - flying sheep
4
我可以理解如何使用 yelldie,但是 try 的用法我不太清楚。你能否举个例子让我更好地理解它的用法? - kshenoy
2
嗯,但是你如何在脚本中使用它们呢?我不明白你所说的“尝试(try)你的语句和代码”的意思。 - TheJavaGuy-Ivan Milosavljević
7
一旦你在脚本顶部添加了这三行,你就可以在你的代码中开始使用 try 函数了。例如:try cp ./fake.txt ./copy.txt - Ryan
显示剩余3条评论

46

如果您使用source调用脚本,可以使用return <x>,其中<x>将是脚本的退出状态(对于错误或假值请使用非零值)。但是,如果您直接使用文件名调用可执行脚本,return语句将会导致一个错误(错误信息为“return:只能从函数或已加载的脚本中`return'”)。

如果使用exit <x>,当使用source调用脚本时,将导致退出启动脚本的shell,但可执行脚本将像预期一样终止。

为了在同一个脚本中处理这两种情况,您可以使用:

return <x> 2> /dev/null || exit <x>

这将处理适当的任何调用,前提是您将在脚本的顶层使用此语句。我建议不要从函数内部直接退出脚本。

注意:<x> 应该只是一个数字。


在一个带有返回/退出的函数脚本中对我不起作用,即是否可能在函数内部退出而不退出shell,但又不需要让函数调用者关心函数返回代码的正确检查? - jan
@jan 调用者对返回值的处理(或不处理)与如何从函数中返回完全是正交的(即无关的),...而且不会退出shell,无论如何调用。这主要取决于调用者代码,这不是本次问答的一部分。您甚至可以为调用者定制函数的返回值,但是本回答不限制此返回值可以是什么... - kavadias
1
这应该是被接受的答案。问题没有指定是否使用“source script.sh”或“./script.sh”。人们经常都使用两者。目前被接受的答案如果使用source就不起作用。这个答案适用于两种情况。 - Starman

17

我经常包含一个名为run()的函数来处理错误。我想要执行的每个调用都传递给此函数,因此在遇到错误时整个脚本退出。与使用set -e解决方案相比,它的优点在于当一行失败时脚本不会悄无声息地退出,并且可以告诉您出了什么问题。在以下示例中,第三行未执行,因为脚本在调用false时退出。

function run() {
  cmd_output=$(eval $1)
  return_value=$?
  if [ $return_value != 0 ]; then
    echo "Command $1 failed"
    exit -1
  else
    echo "output: $cmd_output"
    echo "Command succeeded."
  fi
  return $return_value
}
run "date"
run "false"
run "date"

1
兄弟,不知为何,我真的很喜欢这个答案。我承认它有点复杂,但它似乎非常有用。鉴于我不是bash专家,这让我相信我的逻辑有误,这种方法存在问题,否则,我觉得其他人会更加赞赏它。那么,这个函数有什么问题吗?我应该在这里注意什么吗? - user578994
我不记得为什么要使用eval,但该函数在cmd_output=$($1)的情况下运行良好。 - Joseph Sheedy
我刚刚将这个作为复杂部署流程的一部分实现了,效果非常好。谢谢你,这里有一个评论和一个点赞。 - fuzzygroup
真正了不起的工作!这是最简单和最干净的解决方案,运行良好。对我来说,在FOR循环中的一个命令之前添加了这个选项,因为FOR循环不会选择 set -e 选项。然后,由于该命令带有参数,我使用单引号避免了类似于bash的问题,像这样: runTry 'mysqldump $DB_PASS --user="$DB_USER" --host="$BV_DB_HOST" --triggers --routines --events --single-transaction --verbose $DB_SCHEMA $tables -r $BACKUP_DIR/$tables$BACKUP_FILE_NAME'。注意,我将函数名称更改为runTry。 - Tony-Caffe
1
如果您接受任意输入,那么eval可能是危险的,但除此之外,这看起来非常不错。 - dragon788
@dragon788 函数本身不应该传递任意未经过滤的输入。eval 除了通过另一种解决方案(如 cmd_output=$($1))引入的安全隐患外,是否还会增加其他安全问题? - Joseph Sheedy

7

可以利用短路求值代替if结构:

#!/usr/bin/env bash

echo $[1+1]
echo $[2/0]              # division by 0 but execution of script proceeds
echo $[3+1]
(echo $[4/0]) || exit $? # script halted with code 1 returned from `echo`
echo $[5+1]

注意括号的配对是必要的,因为备选运算符优先级高。$?是一个特殊变量,它记录了最近调用命令的退出代码。

1
如果我执行 command -that --fails || exit $?,它可以不用括号正常工作,那么为什么在 echo $[4/0] 中我们需要使用括号呢? - Anentropic
3
括号与优先级无关,而是用于异常处理。除以零将立即导致 shell 退出,返回代码 1。如果没有括号(也就是简单的 echo $[4/0] || exit $?),bash 永远不会执行 echo,更不用说遵守 || 了。 - bobbogo

2

我有同样的问题,但不能提问,因为那会是一个重复的问题。

接受的答案使用exit,当脚本稍微复杂一些时,它就不起作用了。如果您使用后台进程检查条件,exit只会退出该进程,因为它在子shell中运行。要终止脚本,您必须显式地杀死它(至少这是我知道的唯一方法)。

这里是一个小脚本,演示如何做到这一点:

#!/bin/bash

boom() {
    while true; do sleep 1.2; echo boom; done
}

f() {
    echo Hello
    N=0
    while
        ((N++ <10))
    do
        sleep 1
        echo $N
        #        ((N > 5)) && exit 4 # does not work
        ((N > 5)) && { kill -9 $$; exit 5; } # works 
    done
}

boom &
f &

while true; do sleep 0.5; echo beep; done

这是一个更好的答案,但仍然不完整,因为我真的不知道如何摆脱 boom 部分。


2
#!/bin/bash -x

# exit and report the failure if any command fails
exit_trap () {                                         # ---- (1)
  local lc="$BASH_COMMAND" rc=$?
  echo "Command [$lc] exited with code [$rc]"
}

trap exit_trap EXIT                                    # ---- (2)

set -e                                                 # ---- (3)

解释:

这个问题也涉及如何编写干净的代码。让我们将上面的脚本分成多个部分:


第一部分: exit_trap是一个函数,当任何步骤失败并使用$BASH_COMMAND捕获最后执行的步骤和该步骤的返回代码时,会调用该函数。这个函数可以用于任何清理工作,类似于shutdownhooks

当前正在执行或即将执行的命令,除非shell正在执行trap的结果命令,否则它是陷阱时执行的命令。

文档。


第二部分:

trap [action] [signal]

退出信号的情况下,注册陷阱操作(这里是exit_trap函数)。


第三部分:

如果一个或多个命令的序列返回非零状态,则立即退出。如果失败的命令是紧随while或until关键字的命令列表的一部分,在if语句中的测试的一部分,在任何执行在&&或||列表中的命令的一部分(除了最后一个&&或||之后的命令),管道中除了最后一个命令以外的任何命令,或者如果命令的返回状态被!反转,则shell不会退出。如果除子shell之外的复合命令由于在忽略-e时失败而返回非零状态,则shell不会退出。如果设置了ERR陷阱,则在shell退出之前执行。

文档。


第四部分:
您可以创建一个名为common.sh的文件,并在所有脚本中引用它。
source common.sh

1
你可以通过以下方式关闭程序:

软退出:

pkill -9 -x programname # Replace "programmname" by your programme

执行强制退出,请执行

pkill -15 -x programname # Replace "programmname" by your programme

如果您想了解如何评估关闭程序的条件,您需要自定义您的问题。

2
你把9和15搞混了。信号9是SIGKILL,它是一种“更强硬”的退出方式,可以强制立即终止程序。信号15是SIGTERM,它会礼貌地请求程序终止,并允许程序在需要时进行清理。 - Soren Bjornstad
这对我有用!我需要脚本在嵌套的while循环中调用的函数满足特定条件时停止。exit和return都没用!pkill -15 -x $(basename "$0")这样你就不必在脚本中硬编码程序名称 - undefined

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