44

I have a script, that does not exit when I want it to.

An example script with the same error is:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

I would assume to see the output:

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

But I actually see:

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

Does the () command chaining somehow create a scope? What is exit exiting out of, if not the script?

81

() runs commands in the subshell, so by exit you are exiting from subshell and returning to the parent shell. Use braces {} if you want to run commands in the current shell.

From bash manual:

(list) list is executed in a subshell environment. Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.

{ list; } list is simply executed in the current shell environment. list must be terminated with a newline or semicolon. This is known as a group command. The return status is the exit status of list. Note that unlike the metacharacters ( and ), { and } are reserved words and must occur where a reserved word is permitted to be recognized. Since they do not cause a word break, they must be separated from list by whitespace or another shell metacharacter.

It's worth mentioning that the shell syntax is quite consistent and the subshell participates also in the other () constructs like command substitution (also with the old-style `..` syntax) or process substitution, so the following won't exit from the current shell either:

echo $(exit)
cat <(exit)

While it may be obvious that subshells are involved when commands are placed explicitly inside (), the less visible fact is that they are also spawned in these other structures:

  • command started in the background

    exit &
    

    doesn't exit the current shell because (after man bash)

    If a command is terminated by the control operator &, the shell executes the command in the background in a subshell. The shell does not wait for the command to finish, and the return status is 0.

  • the pipeline

    exit | echo foo
    

    still exits only from the subshell.

    However different shells behave differently in this regard. For example bash puts all components of the pipeline into separate subshells (unless you use the lastpipe option in invocations where job control is not enabled), but AT&T ksh and zsh run the last part inside the current shell (both behaviours are allowed by POSIX). Thus

    exit | exit | exit
    

    does basically nothing in bash, but exits from the zsh because of the last exit.

  • coproc exit also runs exit in a subshell.

  • 5
    Ah. Now to find all the places, where my predecessor used the wrong braces. Thanks for the insight. – Minix Dec 10 '14 at 13:43
  • 10
    Take careful note of the spacing in the man page: { and } are not syntax, they are reserved words and must be surrounded by spaces, and the list must end with a command terminator (semicolon, newline, ampersand) – glenn jackman Dec 10 '14 at 16:18
  • Out of interest, does this tend to actually be another process or just a separate environment in an internal stack? I use () a lot for isolating chdirs, and must have been lucky with my use of $$ etc if the former. – Dan Sheppard Dec 11 '14 at 0:14
  • 5
    @DanSheppard It is another process, but (echo $$) prints parent shell id because $$ is expanded even before subshell is created. In fact printing subshell process id could be tricky, see stackoverflow.com/questions/9119885/… – jimmij Dec 11 '14 at 0:25
  • @jimmij, How can it be that $$ is expanded before the subshell is created, and yet $BASHPID shows the correct value for a subshell? – Wildcard Sep 10 '16 at 2:17
13

Executing the exit in a subshell is one pitfall:

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

The script prints 42, exits from the subshell with return code 1, and continues with the script. Even replacing the call by echo $(CALC) || exit 1 does not help because the return code of echo is 0 regardless of the return code of calc. And calc is executed prior to echo.

Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script. I stumbled over the problem when I wrote a function to verify an input value. Example:

I want to create a file named "year month day.log", i.e., 20141211.log for today. The date is input by a user who may fail to provide a reasonable value. Therefore, in my function fname I check the return value of date to verify the validity of the user input:

#!/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"

Looks good. Let the script be named s.sh. If the user calls the script with ./s.sh "Thu Dec 11 20:45:49 CET 2014", the file 20141211.log is created. If, however, the user types ./s.sh "Thu hec 11 20:45:49 CET 2014", then the script outputs:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

The line fname… says that the bad input data has been detected in the subshell. But the exit 1 at the end of the local … line is never triggered because the local directive always return 0. This is because local is executed after $(fname) and thus overwrites its return code. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

To make it clear: Without the local, the script aborts as expected when an invalid date is entered.

The fix is to split the line like

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

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

Though not being a bug I feel that the behaviour of bash is counterintuitive. I am aware of the sequence of execution, local should not mask a broken assignment, nevertheless.

My initial answer contained some inaccurancies. After a revealing and in-depth discussion with mikeserv (thank you for that) I went for fixing them.

  • @mikeserv: I added an example to show the relevance. – hermannk Dec 11 '14 at 20:59
  • @mikeserv: Yes, you're right. Even terser. But the pitfall is still there. – hermannk Dec 11 '14 at 21:11
  • @mikeserv: Sorry, my example was broken. I'd forgotten the test in doit(). – hermannk Dec 11 '14 at 22:18
  • Let us continue this discussion in chat. – hermannk Dec 12 '14 at 6:45
1

The actual solution:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

The error grouping will only execute if bla returns an error status, and exit is not in a subshell so the whole script stops.

0

The brackets start a subshell and the exit only exits that subshell.

You can read the exitcode with $? and add this in your script to exit the script if the subshell was exited:

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.