for 循环中的条件 continue 会中断函数中的 errexit

for 循环中的条件 continue 会中断函数中的 errexit

AFAICT,continue在 for 循环中调用另一个函数会破坏errexit语义。在main()函数中,如果函数中出现任何失败,我想继续进行下一次迭代build()

#! /usr/bin/env bash

export PS4='# ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]}() - [${SHLVL},${BASH_SUBSHELL},$?] '
set -o xtrace
set -o errexit

build() {
  local _foo=$1

  if [ "${_foo}" -eq 1 ]; then
    false
  fi

  printf "%s with foo=%s builds ok\\n" "${FUNCNAME[0]}" "${_foo}"
}

main() {
  for i in 1 2 3; do
    build $i || continue
  done
}

main "$@"

然而,在循环continue内部for会导致代码继续相反,在函数内部build(),删除标志的效果errexit

$ ./foo.sh 
# ./foo.sh:5: () - [3,0,0] set -o errexit
# ./foo.sh:23: () - [3,0,0] main
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 1
# ./foo.sh:8: build() - [3,0,0] local _foo=1
# ./foo.sh:10: build() - [3,0,0] '[' 1 -eq 1 ']'
# ./foo.sh:11: build() - [3,0,0] false
# ./foo.sh:14: build() - [3,0,1] printf '%s with foo=%s builds ok\n' build 1
build with foo=1 builds ok
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 2
# ./foo.sh:8: build() - [3,0,0] local _foo=2
# ./foo.sh:10: build() - [3,0,0] '[' 2 -eq 1 ']'
# ./foo.sh:14: build() - [3,0,0] printf '%s with foo=%s builds ok\n' build 2
build with foo=2 builds ok
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 3
# ./foo.sh:8: build() - [3,0,0] local _foo=3
# ./foo.sh:10: build() - [3,0,0] '[' 3 -eq 1 ']'
# ./foo.sh:14: build() - [3,0,0] printf '%s with foo=%s builds ok\n' build 3
build with foo=3 builds ok

正如您在带有 的行中看到的printf,上一行的退出代码,false确实是1(前面括号内的第三个数字),因此它运行时就好像errexit没有到位一样:

# ./foo.sh:14: build() - [3,0,1] printf '%s with foo=%s builds ok\n' build 1

我已经确认删除|| continue会使 shell 在 时退出i=1,因此errexit被传递到 subhshell/函数。

任何帮助将非常感激。

版本

~ $ bash --version                                                            
GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)

更新

关于以下问题有很多好的答案为什么这是。至于如何解决它,我发现这个解决方案是最简单的使脚本达到我想要的效果:将脚本更改为false

false || return $?

当然,缺点是我必须对函数调用的所有命令执行此操作。我可能不得不回到使用run()包装器的旧方法,它执行传递的命令,检查它的返回代码并相应地使脚本失败。我想,做你期望errexit做的事:-)

答案1

-e这似乎与/-errexit中的描述相符bash 文档:

如果失败的命令是紧跟在 while 或until 关键字之后的命令列表的一部分(if 语句中测试的一部分),则 shell 不会退出,在 && 或 || 中执行的任何命令的一部分列表除了最后一个 && 或 || 后面的命令之外,管道中除最后一个命令之外的任何命令,或者命令的返回状态与 ! 反转。
[...]

如果复合命令或 shell 函数在忽略 -e 的上下文中执行,则复合命令或函数体内执行的任何命令都不会受到 -e 设置的影响,即使设置了 -e 并且命令返回故障状态。

这已在这个计算器问题,链接到这封电子邮件并附有以下文字:

> My initial gripe about errexit (and its man page description) is that the 
> following doesn't behave as a newbie would expect it to:
> 
> set -e
> f() {
>   false
>   echo "NO!!"
> }
> f || { echo "f failed" >&2; exit 1; }

Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive.  But
it is standardized, so we have to live with it.

POSIX的描述-e说:

-e
当此选项打开时,如果一个简单命令由于 Shell 错误的后果中列出的任何原因而失败,或者返回退出状态值 >0,并且不是 while、until 或 if 关键字后面的复合列表的一部分, 和不是 AND 或 OR 列表的一部分,并且不是前面带有 ! 的管道。保留字,则 shell 应立即退出。

答案2

来自手动的[强调我的]:

errexit

与...一样-e

-e

如果管道 […](可能由单个简单命令 […]、列表 […] 或复合命令 […] 组成)返回非零状态,请立即退出。如果失败的命令是 [...]&&或列表中执行的任何命令的一部分(最后一个或||后面的命令除外),则 shell 不会退出&&||,[…]

[…]

如果 [...] shell 函数在被忽略的上下文中执行-e,则在 [...] 函数体内执行的任何命令都不会受到该-e设置的影响,即使-e已设置并且命令返回失败状态。[…]

您的shell 函数在被忽略的build $i || continue build上下文中执行。是在函数体内执行的命令,它不受设置的影响,因此它甚至不会阻止运行。-efalse-eprintf

删除|| continue并调用只是build $i将函数的每个部分放在一个上下文中,-e其中不是被忽略,因此整个代码因false和 之后退出false(没有到达printf)。

这似乎errexit是一个全局设置(当不被忽略时)终止整个脚本。人们不能(或至少不能轻易地)让它终止一个函数,但不能终止整个脚本。

相关内容