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


47

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

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

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

我假设看到输出:

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

但我实际上看到了:

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

()命令链是否以某种方式创建作用域?什么是exit退出了,如果不是脚本?


5
这需要一个单词的答案:subshel​​l
Joshua

Answers:


87

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

从bash手册:

(列表)列表在子shell环境中执行。在命令完成后,影响外壳环境的变量分配和内置命令将保持无效。返回状态是列表的退出状态。

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

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

echo $(exit)
cat <(exit)

当将命令显式地放置在内部时(),很可能会涉及到子外壳,但鲜为人知的事实是,它们也在以下其他结构中生成:

  • 命令在后台启动

    exit &

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

    如果命令由控制操作符&终止,则外壳程序将在子外壳程序的后台执行该命令。Shell不等待命令完成,返回状态为0。

  • 管道

    exit | echo foo

    仍然仅从子shell退出。

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

    exit | exit | exit

    在bash中基本上不执行任何操作,但是由于last 退出zsh exit

  • coproc exit也可以exit在子外壳中运行。


5
啊。现在查找所有地方,我的前任用错了括号。感谢您的见解。
Minix 2014年

10
请务必注意手册页的间距:{}是不是语法,它们是保留字,必须由空间包围,且列表必须以命令终止(分号,换行符号)结束
格伦·杰克曼

出于兴趣,这是否实际上实际上是另一个过程或内部堆栈中的单独环境?我经常使用()隔离chdir,如果使用$$等,一定很幸运。
Dan Sheppard 2014年

5
@DanSheppard这是另一个过程,但是会(echo $$)打印父Shell ID,因为$$即使在创建子Shell之前它也会被扩展。事实上印刷子shell进程ID可能会非常棘手,看到stackoverflow.com/questions/9119885/...
jimmij

@jimmij,$$在创建子shell之前如何对其进行扩展,但仍$BASHPID显示子shell的正确值?
通配符

13

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

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

脚本打印42,从子外壳退出并返回代码1,然后继续执行脚本。即使用替换呼叫echo $(CALC) || exit 1也无济于事,因为echo无论返回码是,返回码都是0 calc。并且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中检测到错误的输入数据。但是该行exit 1末尾的local …永远不会触发,因为该local指令总是返回0。这是因为在之后local执行并因此覆盖了其返回代码。因此,脚本将继续并使用空参数进行调用。这个例子很简单,但是在实际的应用程序中,bash的行为可能会令人困惑。我知道,真正的程序员不使用本地语言。☺ $(fname)touch

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

解决办法是像

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

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

虽然不是bug,但我认为bash的行为违反直觉。我知道执行的顺序local,但是不应掩盖已损坏的任务。

我的最初答案包含一些错误。在与mikeserv进行了深入和深入的讨论之后(谢谢您),我去修复它们。


@mikeserv:我添加了一个示例来显示相关性。
hermannk 2014年

@mikeserv:是的,您是对的。甚至更短。但是陷阱仍然存在。
hermannk 2014年

@mikeserv:对不起,我的例子坏了。我忘记了考试doit()
2014年


1

方括号开始一个子shell,而出口仅退出该子shell。

您可以读取$?退出代码,并在退出脚本中添加此代码以退出脚本:

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'

1

实际解决方案:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.