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

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

我有一个脚本,当我想要它时它不会退出。

具有相同错误的示例脚本是:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

我假设看到输出:

:~$ ./test.sh
1
:~$

但我实际上看到:

:~$ ./test.sh
1
2
:~$

命令链接是否()以某种方式创建了范围?exit如果不是脚本,退出的是什么呢?

答案1

()在子 shell 中运行命令,因此exit您可以退出子 shell 并返回到父 shell。{}如果要在当前 shell 中运行命令,请使用大括号。

来自 bash 手册:

(列表)list 在子shell 环境中执行。影响 shell 环境的变量分配和内置命令在命令完成后不再有效。返回状态是list的退出状态。

{ 列表; }list 只是在当前 shell 环境中执行。列表必须以换行符或分号结束。这称为组命令。返回状态是list的退出状态。请注意,与元字符 ( 和 ) 不同,{ 和 } 是保留字,并且必须出现在允许识别保留字的位置。由于它们不会导致断词,因此必须用空格或其他 shell 元字符将它们与列表分隔开。

值得一提的是,shell 语法非常一致,并且子 shell 还参与其他()构造,例如命令替换(也使用旧式`..`语法)或进程替换,因此以下内容也不会从当前 shell 退出:

echo $(exit)
cat <(exit)

虽然很明显,当命令显式放置在 内部时(),会涉及到子 shell,但不太明显的事实是,它们也会在这些其他结构中生成:

  • 命令在后台启动

    exit &
    

    不退出当前 shell 因为(之后man bash

    如果命令由控制运算符 & 终止,则 shell 会在子 shell 的后台执行该命令。 shell 不等待命令完成,返回状态为 0。

  • 管道

    exit | echo foo
    

    仍然仅从子 shell 退出。

    但是不同的 shell 在这方面的行为不同。例如,bash将管道的所有组件放入单独的子 shell 中(除非您lastpipe在未启用作业控制的调用中使用该选项),但 AT&Tkshzsh在当前 shell 中运行最后一部分(POSIX 允许这两种行为)。因此

    exit | exit | exit
    

    在 bash 中基本上不执行任何操作,但由于以下原因退出 zsh最后 exit

  • coproc exit也运行exit在子shell中。

答案2

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

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

脚本打印42,退出子外壳带有返回代码1,并继续执行脚本。即使将调用替换为 也echo $(CALC) || exit 1无济于事,因为echo无论 的返回码如何, 的返回码都是 0 calc。并且calc在 之前执行echo

exit更令人费解的是,通过将其包装到内置脚本中来阻止效果local,如以下脚本所示。当我编写一个函数来验证输入值时,我偶然发现了这个问题。例子:

我想创建一个名为“年月日.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 中检测到错误的输入数据。但exit 1行尾的local …永远不会被触发,因为local指令总是返回0。这是因为local被执行 $(fname)从而覆盖其返回码。因此,脚本会继续并touch使用空参数进行调用。这个示例很简单,但 bash 的行为在实际应用程序中可能会非常混乱。我知道,真正的程序员不会使用当地人。☺

明确地说:如果没有local,当输入无效日期时,脚本将按预期中止。

修复方法是将线分开

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

这种奇怪的行为符合localbash 手册页中的文档:“返回状态为 0,除非在函数外部使用 local、提供了无效名称或 name 是只读变量。”

虽然不是一个错误,但我觉得 bash 的行为是违反直觉的。不过,我知道执行的顺序,local不应该掩盖损坏的分配。

我最初的回答包含一些不准确之处。在与 mikeserv 进行了深入而深入的讨论(谢谢你)后,我开始修复它们。

答案3

实际解决方案:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

错误分组仅在bla返回错误状态时才会执行,并且exit不在子 shell 中,因此整个脚本都会停止。

答案4

括号开始一个子外壳并且退出仅退出该子 shell。

您可以使用以下命令读取退出代码$?并将其添加到脚本中,以便在退出子 shell 时退出脚本:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'

相关内容