在Bash中等待退出错误码

4
让我们把这个作为起点考虑:
#!/bin/bash
set -e

echo "Sleeping..."
sleep 2 &

wait

echo "Done"
exit 0

我希望如果后台进程出现错误,wait 命令能够退出整个脚本。例如,引入以下错误:
#!/bin/bash
set -e

echo "Sleeping..."
sleep SOMETHING_STRANGE_AND_WRONG &

wait

echo "Done"
exit 0

执行 echo "Done"。因为使用了 set -e,所以我原本期望 wait 命令会退出脚本。

我知道可以保存 sleep 命令的进程ID(pid),并通过检查后台进程的返回值来实现:

#!/bin/bash
set -e

echo "Sleeping..."
sleep SOMETHING_STRANGE_AND_WRONG &
pid=$!

if wait $pid; then
    echo "Success"
else
    echo "Failure!"
    exit 1
fi

echo "Done"
exit 0

然而,当我的脚本中有几个这样的“同步点”和几个子进程在每个点等待时,这变得很繁琐。
我对错误代码本身并不是很感兴趣,只在意它们不成功。
是否有一种更简洁的方法使wait失败并退出(因为set -e)如果任何它正在等待的子进程没有成功?
编辑:我正在寻找一种解决方案,在此解决方案中,wait如果任何一个子进程失败,则会失败并退出。
#!/bin/bash
set -e

echo "Sleeping..."
sleep SOMETHING_STRANGE_AND_WRONG &
sleep 2 &

wait

echo "Done"
exit 0

我目前是这样解决的(但我觉得很繁琐):

#!/bin/bash
set -e

echo "Sleeping..."
pids=""
sleep SOMETHING_STRANGE_AND_WRONG &
pids+=" $!"
sleep 2 &
pids+=" $!"

for p in $pids; do
    if wait $p; then
        echo "Success"
    else
        echo "Failure"
        exit 1
    fi
done

echo "Done"
exit 0

1
“我原本期望[foo]因为set -e而退出脚本”的长异常列表,正是为什么建议你不要使用set -e的原因。” - chepner
如果时间紧迫,可以跳过下面的寓言,查看更多关于此内容的信息,请参见[BashFAQ#105](http://mywiki.wooledge.org/BashFAQ/105)。 - Charles Duffy
3个回答

3
因为您的shebang行是Bash,所以我将首先给出一个Bash特定(非POSIX可移植)的答案(以及下面较不优雅的可移植版本)。
Bash有一种简洁/优雅/健壮的方式,可以在每个子进程完成时作出响应,而不是按硬编码循环顺序。 POSIX可移植版本必须使用硬编码循环,并且在可移植性方面已经尽力了。对于两个版本,只需进行小的调整即可在遇到第一个失败时处理并退出,或者等到所有任务完成后再处理并退出,然后无论如何都可以在所有子进程退出后等待父进程退出(当不这样做可能会导致竞争条件或僵尸进程时非常有用)。
Bash的非可移植"wait"的相关要点如下:
- optflag -f:强制 "wait" 等待指定的进程结束后才返回其状态,而不是在它改变状态时返回其状态(具体取决于您的使用情况)。 - optflag -n:等待来自id列表中的单个进程的退出状态,如果没有提供id,则等待任何进程完成并返回其退出状态。 - 退出状态127:如果没有提供参数且shell没有未等待的子进程,则返回127;如果提供的所有参数都不是shell的子进程,则返回127。
以下是关键逻辑片段。
Bash版本:
set -e
declare -i err=0 werr=0
while wait -fn || werr=$?; ((werr != 127)); do
  err=$werr
done

POSIX可移植 shell 版本:

set -e
werr=0
err=0
for pid in $pids; do
  wait $pid || werr=$?
  ! [ $werr = 127 ] || break
  err=$werr
done

两个版本都包括处理功能,可以直接处理或在所有子项退出后处理,并且可以选择等待父级退出或不等待(请参见取消注释的行)。

Bash版本

#!/usr/bin/env bash

sleep 2 &
sleep SOMETHING_STRANGE_AND_WRONG &
sleep 1 &

set -e
declare -i err=0 werr=0
while wait -fn || werr=$?; ((werr != 127)); do
  err=$werr
  ## To handle *as soon as* first failure happens uncomment this:
  #((err == 0)) || break
done
## If you want to still wait for children to finish before exiting
## parent (even if you handle the failed child early) uncomment this:
#trap 'wait || :' EXIT
if ((err == 0)); then
  echo "Success"
else
  echo "Failure!"
  exit $err
fi

POSIX可移植Shell版本:

#!/usr/bin/env sh

pids=''
sleep 2 & pids="$pids $!"
sleep SOMETHING_STRANGE_AND_WRONG & pids="$pids $!"
sleep 1 & pids="$pids $!"

set -e
werr=0
err=0
for pid in $pids; do
  wait $pid || werr=$?
  ! [ $werr = 127 ] || break
  err=$werr
  ## To handle *as soon as* first failure happens uncomment this:
  #[ $err = 0 ] || break
done
## If you want to still wait for children to finish before exiting
## parent (even if you handle the failed child early) uncomment this:
#trap 'wait || :' EXIT
if [ $err = 0 ]; then
  echo "Success"
else
  echo "Failure!"
  exit $err
fi

2
更简短的方式可能是:
wait || exit $?

或者如果需要消息,如果失败的进程尚未记录,则可以记录。
wait || { echo "background failed: $?" >&2; exit 1;}

或者可以使用一个函数来代替。
exit_fail() {
    echo "$1" >&2
    exit 1
}

...
wait || exit_fail "background failed: $?"

1
这似乎无法扩展到多个子进程,例如如果我启动 sleep SOMETHNG_STRANGE_AND_WRONG& 然后跟着 sleep 2&,并使用 wait || exit $? 等待,脚本仍然输出 "Done"。 - Gauthier
1
wait i mean wait $pid - Nahuel Fouilleul
|| exit 1语义与set -e完全相同,但是它是显式的。它断言如果命令退出状态为0,则脚本将继续执行。与set -e相反,它是隐式的,如果脚本在失败的命令之后应该继续执行,可以使用|| true - Nahuel Fouilleul
然而,如果失败的话,等待所有子进程可能是安全的。 - Nahuel Fouilleul
陷阱 'kill $(jobs -p)' EXIT - Nahuel Fouilleul
显示剩余4条评论

1
我手动在一个函数中实现了该功能。
# Wait for subprocesses with pids passed as arguments, exit if any failed.
# $1 info_string: to show the command that the failure/success relates to.
# $* list of pids to wait for and check.
wait_and_check () {
    info_string=$1
    shift
    for p in $*; do
        if wait $p; then
            echo "$info_string process $p success"
        else
            echo "$info_string process $p Failure!"
            exit 1
        fi
    done
}

使用方法:

pids=""
for p in $bunch_of_stuff ; do
    stuff_function $p &
    pids+=" $!"
done

wait_and_check "stuff" $pids

这似乎是许多人都需要类似的东西,所以我很惊讶没有现成的解决方案。
一个缺点是很难检查进程的错误代码。

当需要列表时,请考虑使用适当的bash数组而不是包含空格的字符串。使用pids=( )进行初始化;使用pids+=( "$1" )进行追加;使用${pids[@]}进行展开;并且使用"$@"而不是 $*。当前代码不必要地脆弱--如果由函数在其他地方设置了 IFS=0,则1014的pid将在未引用扩展中拆分为两个条目,即 114;并且如果您某种方式有一个值可以解析为glob,则展开将其替换为匹配文件名的列表。 - Charles Duffy
就问题中所要求的“等待”具有“set -e”行为而言,使用带有所有分支中的“echo”的“if/then/else/fi”块似乎与期望的简洁相矛盾,此时可以使用“wait || exit”。 - Charles Duffy
请注意,退出而不杀死所有子进程会导致竞争条件,因为在新运行之后,旧进程和新进程可能会在相同的数据上运行。 - Nahuel Fouilleul
@CharlesDuffy,您如何使用||结构来显示错误消息?我对if-then-else很熟悉,因为我将其隐藏在函数中,但我很好奇它在实际的bashese中会是什么样子。 - Gauthier
处理特定子进程的常见习语,同时避免竞争和僵尸进程的出现,是首先在“直到错误或最后一次迭代”循环中执行“wait $pid”,或者更优雅地在Bash中执行“wait -n”(或“wait -fn”,具体取决于所需的语义)在“直到错误或没有剩余”循环中执行,然后对于所有尾随的子进程执行一个全局的“wait”(或者如果您不再关心剩余子进程的错误,则执行“wait || :”)。请参见我的答案(刚刚添加)以获取一个可行的示例。 - rowanthorpe
显示剩余3条评论

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