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

Min*_*nix 60 shell-script exit subshell

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

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

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'
Run Code Online (Sandbox Code Playgroud)

我假设会看到输出:

:~$ ./test.sh
1
:~$
Run Code Online (Sandbox Code Playgroud)

但我实际上看到:

:~$ ./test.sh
1
2
:~$
Run Code Online (Sandbox Code Playgroud)

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

jim*_*mij 91

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

从 bash 手册:

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

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

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

echo $(exit)
cat <(exit)
Run Code Online (Sandbox Code Playgroud)

当命令显式地放置在 中时,子shell 可能很明显涉及(),但不太明显的事实是它们也在这些其他结构中产生:

  • 命令在后台启动

    exit &
    
    Run Code Online (Sandbox Code Playgroud)

    不退出当前外壳,因为(之后man bash

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

  • 管道

    exit | echo foo
    
    Run Code Online (Sandbox Code Playgroud)

    仍然只从子外壳退出。

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

    exit | exit | exit
    
    Run Code Online (Sandbox Code Playgroud)

    在 bash 中基本上什么都不做,但由于最后一个 exit.

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

  • 注意手册页中的空格:`{` 和 `}` 不是语法,它们是保留字,必须用空格包围,列表必须以命令终止符(分号、换行符、与号)结尾 (12认同)
  • 啊。现在找到我的前任使用错误大括号的所有地方。感谢您的洞察力。 (5认同)
  • @DanSheppard 这是另一个进程,但是 `(echo $$)` 会打印父 shell id,因为 `$$` 甚至在创建子 shell 之前就被扩展了。事实上,打印子外壳进程 ID 可能很棘手,请参阅 http://stackoverflow.com/questions/9119885/how-to-get-the-process-id-of-a-bash-subprocess-on-command-line (5认同)

her*_*nnk 17

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

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)
Run Code Online (Sandbox Code Playgroud)

脚本打印 42,从子shell退出并返回代码1,然后继续执行脚本。即使将调用替换为 byecho $(CALC) || exit 1也无济于事,因为echo无论calc. 并且calc在 之前执行echo

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

我想创建一个名为“年月日.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"
Run Code Online (Sandbox Code Playgroud)

看起来挺好的。让脚本命名为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
Run Code Online (Sandbox Code Playgroud)

该行fname…表示在子外壳中检测到错误的输入数据。但是永远不会触发行exit 1尾的 ,local …因为local指令总是 return 0。这是因为在之后local执行并因此覆盖了其返回码。正因为如此,脚本会继续使用空参数进行调用。这个例子很简单,但 bash 的行为在实际应用中可能会很混乱。我知道,真正的程序员不使用本地人。? $(fname)touch

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

解决方法是像这样分割线

local FNAME
FNAME=$(fname "$1") || exit 1
Run Code Online (Sandbox Code Playgroud)

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

虽然不是一个错误,但我觉得 bash 的行为是违反直觉的。local尽管如此,我知道执行的顺序,不应该掩盖中断的任务。

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


Wal*_*alf 6

实际解决方案:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'
Run Code Online (Sandbox Code Playgroud)

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