8

Consider the following function

function current_dir {
  set -e
  git foobar
  echo "I will not print this, because git foobar returned a non-zero exit code"
}

Now, when I source the function and try to call it in my shell, it exits not only the function, but also the shell itself.

How can this be avoided?

Niels B.
  • 5,912
  • 3
  • 24
  • 44
  • Don't use `set -e`? It's intended for scripts, not interactive use. – chepner May 22 '14 at 12:34
  • I see, but then the question is how to get the equivalent functionality in semi-interactive scripts (functions) – Niels B. May 22 '14 at 12:36
  • Like I said, remove `set -e` from the function. Add it to the top of a script which you want to exit on any error, but don't add it to specific functions which might be used in both interactive and non-interactive shells. – chepner May 22 '14 at 12:53
  • Well, the function is sourced and called directly from the shell, so it's not an option. I am looking for a way to write a bash function that immediately returns if any commands inside the function fails. – Niels B. May 22 '14 at 13:20

3 Answers3

5

If you don't need the function to execute in the current shell (e.g., it isn't setting any parameter values that need to be visible to the caller), you can make the body of the function a subshell, not a command group:

current_dir () (
    set -e
    git foobar
    echo "I will not print this, because git foobar returned a non-zero exit code"
)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • This does not seem to work in GNU bash, version 4.3.11(1)-release (x86_64-pc-linux-gnu): syntax error near unexpected token `newline' – Marcin Owsiany Nov 07 '17 at 13:13
  • Huh, that doesn't appear to work in any version of `bash`; the problem is the `function` keyword, so all the more reason to avoid it and use a POSIX-style function definition: `current_dir () ( ... )`. – chepner Nov 07 '17 at 13:59
4

As far as I know, set -e will do exactly what you see: exiting the shell completely as soon as a command exits with a non-zero status.

You can try to reformulate your function with trap or using && between the commands:

function current_dir {
  git foobar && echo "I will not print this, because git foobar returned a non-zero exit code"
}

or (better readability):

function current_dir {
  trap 'trap - ERR; return' ERR
  git foobar
  ...other commands...
  echo "I will not print this, because a command returned a non-zero exit code"
}

If you really need set -e for some reason, you can temporary disable it with set +e and reenable it again after your critical section.

Eric Fournie
  • 1,362
  • 8
  • 10
  • 1
    I see the idea here, but the readability suffers as soon as the lines pile up. – Niels B. May 22 '14 at 12:37
  • 1
    You can start a new line immediately after the `&&`, or you can use an `if` statement instead of an `&&` list. – chepner May 22 '14 at 12:54
  • In this case, use `trap ... ERR` as in the example I added. – Eric Fournie May 22 '14 at 13:25
  • 1
    I think `trap … ERR` is correct, but it's not necessary to kill the current shell entirely. Why not `current_dir() { trap 'trap - ERR; return' ERR; git foobar; …; }`? – kojiro May 22 '14 at 13:50
  • I tried it. Didn't notice I made a typo and thought it was not possible to `return` in a `trap`, there are so many ways to be wrong in bash :). Of course it is much better this way! I will correct my answer. Why the `trap` inside the `trap` though? – Eric Fournie May 22 '14 at 13:54
  • @EricFournie if you don't clear the trap then every time `ERR` happens in the shell outside of a function you'll get an annoying message. – kojiro May 22 '14 at 14:01
  • [Followup questions](http://stackoverflow.com/questions/23809608/where-does-the-exit-status-go-after-trap-return) – kojiro May 22 '14 at 14:24
3

"set -e" is used to exit immediatly when a command (a pipeline or a sub-shell command, etc.) exits with a non-zero status.

By default, BASH ignores errors and continues to interpret your script. This can cause a very bad surprise, an example:

    if ! lss myLock
    then touch myLock; echo "No lock, I can do the job"; rm -f myLock
    fi

The result of this script is:

bash: lss : commande introuvable
No lock, I can do the job

As you can see, BASH makes no difference between a command not found and a command which failed.

"-e" is frequently used to make sure the Shell interpretor will stop immediatly after an error (so we have to think about all errors...) This help to prevent a very bad issue when we performed a mistake in the script (think about a rm -rf "$v"/* when you forgot to set v ;-)). For this purpose we pass "-e" option in the shebang. Obviously, it's not designed for interactive use, and I don't imagine a good usage of "set -e", nor of "set +e" (but to test).

To answer to your initial question. You can avoid the termination of your shell by applying one of the following solution:

Use if statement

function myF1() {
    if git foobar; then
        echo "I will not print this, because git foobar returned a non-zero exit code"
    fi
}

Use the control operator &&

function myF2() {
    git foobar && \
    echo "I will not print this, because git foobar returned a non-zero exit code"
}

Open a subshell

function myF4() {(
    set -e
    git foobar
    echo "I will not print this, because git foobar returned a non-zero exit code"
)}

In this case, "set -e" will exit immediatly of the sub-shell, but it will not terminate the caller :-)

Use a trap is not a good idea here as it would cause a side-effect on the caller (the trap is defined globally). Obviously we can combine trap and sub-shell, but it's not useful in your case.

mcoolive
  • 3,805
  • 1
  • 27
  • 32
  • 1
    I like the subshell idea here. The side-effect is though that you won't have access to the state of your calling shell - eg. custom variables and such. – Tim Malone Oct 22 '18 at 22:31