根据进程退出码退出Shell脚本

388

我有一个执行多个命令的 shell 脚本。如果其中任何一个命令以非零退出代码退出,我该如何使 shell 脚本退出?

我应该如何让 shell 脚本在命令出错时退出呢?

硬方法:在每个命令后测试 $? 的值。简单方法:在Bash脚本的顶部放置 set -e#!/bin/bash -e - mwfearnley
9个回答

504
每次执行命令后,可以在$?变量中找到退出码,因此您可能会看到以下内容:
ls -al file.ext
rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi

在处理管道命令时需要小心,因为$?只会返回管道中最后一个元素的返回码。因此,在以下代码中:

ls -al file.ext | sed 's/^/xx: /"

如果文件不存在,sed管道不会返回错误代码(因为管道中的sed实际上是可以正常工作并返回0的)。在这种情况下,bash shell提供了一个数组PIPESTATUS,它可以帮助解决这个问题。该数组有每个管道组件的一个元素,您可以像${PIPESTATUS[0]}一样单独访问每个元素:
pax> false | true ; echo ${PIPESTATUS[0]}
1

请注意,这将为您获取false命令的结果而不是整个管道。您还可以获取整个列表以便根据需要进行处理:
pax> false | true | false; echo ${PIPESTATUS[*]}
1 0 1

如果您想从流水线中获得最大的错误代码,您可以使用类似以下的内容:
true | true | false | true | false
rcs=${PIPESTATUS[*]}; rc=0; for i in ${rcs}; do rc=$(($i > $rc ? $i : $rc)); done
echo $rc

这会依次遍历每个PIPESTATUS元素,并在其大于前面的rc值时将其存储在rc中。


39
只需一行便携式代码即可具有相同的功能:ls -al file.ext || exit $?[[ ]] 不是便携式的)。 - MarcH
19
MarcH,我认为你会发现[[ ]]在标记了bash的问题中是相当通用的 :-) 奇怪的是,在command.com中无法使用ls,因此它也不具有可移植性,这种说法似乎有些牵强,但与你提出的论点相似。 - paxdiablo
39
我知道这已经很古老了,但需要注意的是,您可以通过数组PIPESTATUS获取管道中命令的退出代码(例如${PIPESTATUS[0]}表示第一个命令的退出代码,${PIPESTATUS[1]}表示第二个命令的退出代码,${PIPESTATUS[*]}表示所有退出状态的列表)。 - DevSolar
11
需要强调的是,优雅和惯用的shell脚本很少需要直接检查$?。通常你想要像这样做:if ls -al file.ext; then : nothing; else exit $?; fi,这当然等同于像@MarcH说的那样:ls -al file.ext || exit $?,但如果thenelse语句比较复杂,那么第一种写法更易于维护。 - tripleee
9
[[ $rc != 0 ]] 会导致出现 0:未找到 或者 1:未找到 的错误。建议修改为 [ $rc -ne 0 ]。同时,可以将 rc=$? 移除,直接使用 [ $? -ne 0 ] - CurtisLeeBolin
显示剩余6条评论

225

如果你想要使用$?,那么你需要在每个命令后面检查它,因为$?会在每个命令退出后更新。这意味着如果你执行一个管道,你只会得到管道中最后一个进程的退出码。

另一种方法是这样做:

set -e
set -o pipefail
如果您将此放在shell脚本的顶部,它看起来像是Bash会为您处理这个问题。正如之前的帖子所提到的,“set -e”将导致Bash在任何简单命令出错时退出。“set -o pipefail”也将导致Bash在管道中的任何命令出错时退出。 有关此问题的更多讨论,请参见此处此处。这里是有关“set”内置的Bash手册部分:这里

6
这真的应该是最佳答案:这比使用“PIPESTATUS”并在各处检查退出代码要容易得多。 - candu
2
#!/bin/bash -e 是启动 shell 脚本的唯一方式。如果您需要实际检查退出状态,您可以始终使用诸如 foo || handle_error $? 的东西。 - Davis Herring
@DavisHerring 在几个方面都是不正确或误导的。 Shell 的 -e 选项有一些令人惊讶的边角案例,因此在一些指南中甚至不鼓励使用它,特别是对于初学者而言。 在 shebang 行上指定它是脆弱的; 在脚本本身中放置 set -e 更加健壮。 - tripleee
@tripleee:我不知道“一些指南”意味着我对这个主题的建议是“不正确的”。你的建议使用set -e是基于运行bash …/foo并且失去了该选项吗?如果是这样,那么如果您选择从外部运行脚本,就有很多误运行脚本的方法... - Davis Herring
没错,但是根据定义,“唯一的方法”是不正确的。例如,https://dev59.com/SmIk5IYBdhLWcg3wDaYX 的被接受答案总结了一些建议,反对使用 set -e - tripleee

54
"set -e" 是最简单的方法之一。只需在程序中的任何命令前添加它即可。

6
@SwaroopCH set -e 命令会在你的脚本中任何一个命令以错误状态退出且你没有处理该错误时中止脚本。 - Andrew
2
"set -e"与"set -o errexit"完全等价,但与前者不同的是,后者可以被搜索。请搜索opengroup + errexit以获取官方文档。 - MarcH

31
如果在Bash中仅调用exit而没有任何参数,它将返回最后一条命令的退出代码。结合“OR”使用,Bash应该只有在前面的命令失败时才调用exit。但我还没有测试过。Bash还将最后一个命令的退出代码存储在变量$?中。

27
[ $? -eq 0 ] || exit $?; # Exit for nonzero return code

4
这不应该是“exit $? ”(没有括号)吗? - malthe

21

http://cfaj.freeshell.org/shell/cus-faq-2.html#11

11.

如何获取在 cmd1|cmd2 中的 cmd1 的退出码?

首先需要注意的是,cmd1 的退出码可能是非零的并且仍不意味着出现了错误。例如,在以下情况下:

cmd | head -1

你可能会看到退出状态为 141(或 ksh93 下的 269)的 cmd1,但这是因为当 head -1 在读取一行后终止时,cmd 被 SIGPIPE 信号中断。

若要知道管道元素的退出状态: cmd1 | cmd2 | cmd3

a. 使用 Z shell (zsh):

退出码被提供在 pipestatus 特殊数组中。 cmd1 的退出码在 $pipestatus[1]cmd3 的退出码在 $pipestatus[3],所以 $? 始终与 $pipestatus[-1] 相同。

b. 使用 Bash:

退出码被提供在 PIPESTATUS 特殊数组中。 cmd1 的退出码在 ${PIPESTATUS[0]}cmd3 的退出码在 ${PIPESTATUS[2]},所以 $? 始终与 ${PIPESTATUS: -1} 相同。

...

更多详情请参见 Z shell


第一个链接已经失效:“我们无法连接到cfaj.freeshell.org服务器。” - Peter Mortensen

19

对于 Bash:

# This will trap any errors or commands with non-zero exit status
# by calling function catch_errors()
trap catch_errors ERR;

#
# ... the rest of the script goes here
#

function catch_errors() {
   # Do whatever on errors
   #
   #
   echo "script aborted, because of errors";
   exit 0;
}

21
最好不要使用"exit 0",因为那表示成功。 - Brent Bradburn
4
exit_code=$?;echo "脚本因错误而中止";exit $exit_code - RaSergiy

11

在Bash中,这很容易。只需使用&&将它们联系在一起:

command1 && command2 && command3

您也可以使用嵌套的if结构:

if command1
   then
       if command2
           then
               do_something
           else
               exit
       fi
   else
       exit
fi

+1 这是我正在寻找的最简单的解决方案。此外,如果您希望从命令中得到非零错误代码,还可以编写 if (! command) - Berci
这是针对连续命令的..如果我想并行启动这三个命令,并在其中任何一个失败时杀死所有进程,该怎么办? - Vasile Surdu

4
#
#------------------------------------------------------------------------------
# purpose: to run a command, log cmd output, exit on error
# usage:
# set -e; do_run_cmd_or_exit "$cmd" ; set +e
#------------------------------------------------------------------------------
do_run_cmd_or_exit(){
    cmd="$@" ;

    do_log "DEBUG running cmd or exit: \"$cmd\""
    msg=$($cmd 2>&1)
    export exit_code=$?

    # If occurred during the execution, exit with error
    error_msg="Failed to run the command:
        \"$cmd\" with the output:
        \"$msg\" !!!"

    if [ $exit_code -ne 0 ] ; then
        do_log "ERROR $msg"
        do_log "FATAL $msg"
        do_exit "$exit_code" "$error_msg"
    else
        # If no errors occurred, just log the message
        do_log "DEBUG : cmdoutput : \"$msg\""
    fi

}

3
很少需要使用 $*;相反,应该使用 "$@" 以保留空格和通配符。 - Davis Herring

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