76

I generally have -e set in my Bash scripts, but occasionally I would like to run a command and get the return value.

Without doing the set +e; some-command; res=$?; set -e dance, how can I do that?

David Wolever
  • 148,955
  • 89
  • 346
  • 502
  • Because of problems just like this one, I would recommend NOT using `set -e` Here are some more examples: http://mywiki.wooledge.org/BashFAQ/105 – idfah Sep 04 '13 at 20:02
  • 7
    Gotta disagree. When using bash for configuring systems or other, actual work, `set -e` is essential for showing correctness. – phs Jul 18 '14 at 23:20

6 Answers6

102

From the bash manual:

The shell does not exit if the command that fails is [...] part of any command executed in a && or || list [...].

So, just do:

#!/bin/bash

set -eu

foo() {
  # exit code will be 0, 1, or 2
  return $(( RANDOM % 3 ))
}

ret=0
foo || ret=$?
echo "foo() exited with: $ret"

Example runs:

$ ./foo.sh
foo() exited with: 1
$ ./foo.sh
foo() exited with: 0
$ ./foo.sh
foo() exited with: 2

This is the canonical way of doing it.

Adrian Frühwirth
  • 42,970
  • 10
  • 60
  • 71
  • 11
    You probably want to initialize ret=0 before you do that, in case the command succeeds. Some of them do :) – rici Sep 04 '13 at 19:58
  • @AdrianFrühwirth -- I had a `ret=0` before `foo || ret=$?` so it would show the case where it succeeded. I actually tested it multiple times (`for i in $(seq 30); do echo '$ ./foo.sh'; /tmp/stack.sh ; done`) and copy-pasted some results. – docwhat Jul 21 '14 at 00:41
  • @TheDoctorWhat Sorry, I overlooked that. I have rolled back to your edit. Thanks :-) – Adrian Frühwirth Jul 22 '14 at 14:23
  • No problem. I've made mistakes that needed catching plenty of times. – docwhat Jul 24 '14 at 23:44
  • 2
    Alternatively, one could do: `foo || ret=$? ; echo "foo() exited with: ${ret:-0}"` – Petski Nov 10 '20 at 15:44
15

as an alternative

ans=0
some-command || ans=$?
KitsuneYMG
  • 12,753
  • 4
  • 37
  • 58
8

Maybe try running the commands in question in a subshell, like this?

res=$(some-command > /dev/null; echo $?)
chepner
  • 497,756
  • 71
  • 530
  • 681
Phil Miller
  • 36,389
  • 13
  • 67
  • 90
0

Given behavior of shell described at this question it's possible to use following construct:

#!/bin/sh

set -e

{ custom_command; rc=$?; } || :

echo $rc
reddot
  • 764
  • 7
  • 15
0

Another option is to use simple if. It is a bit longer, but fully supported by bash, i.e. that the command can return non-zero value, but the script doesn't exit even with set -e. See it in this simple script:

#! /bin/bash -eu

f () {
  return 2
}

if f;then
  echo Command succeeded
else
  echo Command failed, returned: $?
fi
echo Script still continues.

When we run it, we can see that script still continues after non-zero return code:

$ ./test.sh 
Command failed, returned: 2
Script still continues.
Lada00
  • 1
  • 1
-3

Use a wrapper function to execute your commands:

function __e {
    set +e
    "$@"
    __r=$?
    set -e
}

__e yourcommand arg1 arg2

And use $__r instead of $?:

if [[ __r -eq 0 ]]; then
    echo "success"
else
    echo "failed"
fi

Another method to call commands in a pipe, only that you have to quote the pipe. This does a safe eval.

function __p {
    set +e
    local __A=() __I
    for (( __I = 1; __I <= $#; ++__I )); do
        if [[ "${!__I}" == '|' ]]; then
            __A+=('|')
        else
            __A+=("\"\$$__I\"")
        fi
    done
    eval "${__A[@]}"
    __r=$?
    set -e
}

Example:

__p echo abc '|' grep abc

And I actually prefer this syntax:

__p echo abc :: grep abc

Which I could do with

...
        if [[ ${!__I} == '::' ]]; then
...
konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • @DavidWolever Please tell me what you think about this solution. – konsolebox Sep 04 '13 at 19:53
  • This will e.g. break on I/O redirection. – Adrian Frühwirth Sep 04 '13 at 19:59
  • @AdrianFrühwirth I doubt that. Example? – konsolebox Sep 04 '13 at 20:02
  • @AdrianFrühwirth Where is it then? Can you give me an example command that would fail instead? – konsolebox Sep 04 '13 at 20:05
  • `cat foo | grep bar >/dev/null 2>&1`. You cannot store complex commands in variables, thus `"$@"` will not work for all use cases. Disclaimer: I do use this myself sometimes, but then I know how those commands look like and that they don't work for arbitrary complex invocations. This is an example of where it's a bad idea. For further info see the link. – Adrian Frühwirth Sep 04 '13 at 20:10
  • @AdrianFrühwirth I guess it does have trouble with piped commands as the function was only intended to run a single one. But only with that. It would save you from running two-line commands everytime and it's simpler. – konsolebox Sep 04 '13 at 20:25
  • 2
    How is it simpler than a one-line `r=0; command || r=$?` which works as expected? Oh well, not trying to start argument here, I don't even use `set -e` myself since its implementation is really stupid. Whatever works ;-) – Adrian Frühwirth Sep 04 '13 at 20:31
  • @AdrianFrühwirth Well I do don't like the idea of using `-e` as well :) Let's just let the OP decide on that I guess. And no worries. – konsolebox Sep 04 '13 at 20:59
  • Why people call functions and variables like `__function` `__variable` this is so hard to read. Is that python-ish way to say it's private? If so it's strange as `__r` is a global variable. ¯\_(ツ)_/¯ – tamerlaha Oct 01 '20 at 17:07
  • @tamerlaha Yes this was an old answer. I don't like the idea of having a lowercase variable like __r set as a global variable as well. Should have been __R at least as I use all-caps for globals. I don't remember why I used a prefix for the function. Perhaps I just couldn't think of a proper name or was lazy. Prefixed local variables on the other hand are useful for avoiding collision when they are the functions accept reference variable arguments. Bash 4.4's `declare -n` feature was not safe on collisions at least. – konsolebox Oct 04 '20 at 03:51