为什么 ( exit 1 ) 不会退出脚本?

在子shell中执行exit是一个陷阱:

#!/bin/bash

function calc { echo 42; exit 1; }

echo $(calc)

脚本打印42,在返回代码为1的子shell中退出,并继续执行脚本。即使用echo $(CALC) || exit 1替换调用也无济于事,因为不论calc的返回代码如何,echo的返回代码始终为0。而且calc在echo之前执行。

更让人困惑的是通过将exit包装到内置的local中来阻止其效果,就像下面的脚本一样。我在编写一个验证输入值的函数时遇到了这个问题。例如:

我想创建一个名为"year month day.log"的文件,例如今天的20141211.log。日期由用户输入,但用户可能没有提供合理的值。因此,在我的fname函数中,我检查date的返回值以验证用户输入的有效性:

#!/bin/bash

doit ()

{

local FNAME=$(fname "$1") || exit 1

touch "${FNAME}"

}

fname ()

{

date +"%Y%m%d.log" -d"$1" 2>/dev/null

if [ "$?" != 0 ] ; then

echo "fname reports \"Illegal Date\"" >&2

exit 1

fi

}

doit "$1"

看起来不错。让脚本命名为 s.sh。如果用户使用 ./s.sh "Thu Dec 11 20:45:49 CET 2014" 调用脚本,将创建文件 20141211.log。然而,如果用户输入 ./s.sh "Thu hec 11 20:45:49 CET 2014",那么脚本将输出:

fname reports "Illegal Date"

touch: cannot touch ‘’: No such file or directory

这行代码fname…表示在子shell中检测到了错误的输入数据。但是在local …行末尾的exit 1从来没有被触发,因为local指令总是返回0。这是因为local在执行$(fname)之后执行,并覆盖了它的返回码。因此,脚本继续执行并使用空参数调用touch。这个例子很简单,但是在实际应用中,Bash的行为可能会让人感到困惑。我知道,真正的程序员不使用局部变量。☺

为了明确起见:如果没有local,当输入无效日期时,脚本会按预期中止。

修复方法是将该行拆分为:

local FNAME

FNAME=$(fname "$1") || exit 1

奇怪的行为符合bash的man页中local的文档:“除非在函数外部使用local,提供一个无效的名称,或者name是只读变量,否则返回状态为0。”

虽然这不是一个错误,但我觉得bash的行为很反直觉。我知道执行顺序,local不应该掩盖一个有问题的赋值。

我的初始回答包含了一些不准确之处。在与mikeserv进行了深入讨论后(非常感谢),我决定对它们进行修正。