What is the proper way to detect shell exit code when errexit option is set?

BashShell

Bash Problem Overview


I prefer to write solid shell code, so the errexit & nounset is always set.

The following code will stop at bad_command line

#!/bin/bash
set -o errexit
set -o nounset
bad_command # stop here
good_command

I want to capture it, here is my method

#!/bin/bash
set -o errexit
set -o nounset
rc=1
bad_command && rc=0 # stop here
[ $rc -ne 0 ] && do_err_handle
good_command

Is there any better or cleaner method

My Answer:

#!/bin/bash
set -o errexit
set -o nounset
if ! bad_command ; then
  # error handle here
fi
good_command

Bash Solutions


Solution 1 - Bash

How about this? If you want the actual exit code ...

#!/bin/sh                                                                       
set -e
                                                                      
cat /tmp/doesnotexist && rc=$? || rc=$?                                         
echo exitcode: $rc        
                                                       
cat /dev/null && rc=$? || rc=$?                                                 
echo exitcode: $rc   

Output:

cat: /tmp/doesnotexist: No such file or directory
exitcode: 1
exitcode: 0

Solution 2 - Bash

Keep with errexit. It can help find bugs that otherwise might have unpredictable (and hard to detect) results.

#!/bin/bash
set -o errexit ; set -o nounset

bad_command || do_err_handle
good_command

The above will work fine. errexit only requires that the line pass, as you do with the bad_command && rc=0. Therefore, the above with the 'or' will only run do_err_handle if bad_command fails and, as long as do_err_handle doesn't also 'fail', then the script will continue.

Solution 3 - Bash

A slight variation of the answer given by @rrauenza. Since the && rc=$? part is their answer will always be equal to && rc=0 one can as well set rc to 0 before running the command. The result ends up more readable in my opinion because the variable is defined upfront in its own line of code and is only changed if the command exits with a non-zero exit status. If nounset is also given, then it's now clear that rc was indeed never undefined. This also avoids mixing && and || in the same line which might be confusing because one might not always know the operator precedence by heart.

#!/bin/sh                                                                       
set -eu

rc=0
cat /tmp/doesnotexist || rc=$?                                         
echo exitcode: $rc        

rc=0
cat /dev/null || rc=$?                                                 
echo exitcode: $rc   

Output:

cat: /tmp/doesnotexist: No such file or directory
exitcode: 1
exitcode: 0

Solution 4 - Bash

Continue to write solid shell code.

You do it right if bad_command is really a command. But be careful with function calls in if, while, ||, && or !, because errexit will not work there. It may be dangerous.

If your bad_command is actually bad_function you should write this:

set -eu 

get_exit_code() {
    set +e
    ( set -e;
      "$@"
    )
    exit_code=$?
    set -e
}

...

get_exit_code bad_function

if [ "$exit_code" != 0 ]; then
    do_err_handle
fi

This works well in bash 4.0. In bash 4.2 you only get exit codes 0 or 1.

Solution 5 - Bash

set -o errexit

bad_command || { resp_code=$? echo Bad Thing $resp_code happened }

Solution 6 - Bash

Agree with comments, so if you can give up errexit then you can easily shorten your code to

 bad_command || do_err_handle
 good_command

I hope this helps.

Solution 7 - Bash

A common way to avoid exiting a Bash program when errexit is set and a command that may fail is run is to precede the command with ! .

After the command has run $? does not contain the exit status of the command. (It contains 0 if the command failed and 1 otherwise.) However, the PIPESTATUS array does contain the exit status of the command. A safe way to capture the exit status of a command that may fail, whether or not errexit is set, is:

! bad_command
rc=${PIPESTATUS[0]}

The second line can be simplified to rc=$PIPESTATUS, but Shellcheck will complain about it.

If (as is often the case) you don't need to know the exit status of the command, just if it succeeded or failed, then the solution by @george is good if the error handler is a one-liner. For multi-line error handlers a good option is:

if ! bad_command ; then
    # Handle errors here
fi

Note that (except in very unusual circumstances) it would not be correct to use `bad_command` (as suggested in the question) instead of plain bad_command. You can use ${PIPESTATUS[0]} to get the exit status of the command if it is needed in the error handling code, since $? doesn't contain it in this case either.

Solution 8 - Bash

In case you want to detect the exit code of a compound list (or a function), and have errexit and nounset applied there, you can use this kind of code:

#!/bin/sh
set -eu
unset -v unbound_variable

f() {
	echo "before false"
	false
	echo "after false"
}

g() {
	echo "before unbound"
	var=$unbound_variable
	echo "after unbound"
}

set +e
(set -e; f)
echo error code of f = $?
set -e

echo still in main program

set +e
(set -e; g)
echo error code of g = $?
set -e

echo still in main program

The above should print non-zero error codes for both functions f and g, though you might want that the script exits immediately after unbound variable error. I suppose this works by any POSIX shell. You can also detect the error code in EXIT trap, but the shell exits thereafter. The problem with other proposed methods is that the errexit setting is ignored when an exit status of such a compound list is tested. Here is a quote from the POSIX standard:

> The -e setting shall be ignored when executing the compound list > following the while, until, if, or elif reserved word, a pipeline > beginning with the ! reserved word, or any command of an AND-OR list > other than the last.

Note that if you have defined your function like

f() (
    set -e
    ...
)

it is enough to do

set +e
f
echo exit code of f = $?
set -e

to get the exit code.

Solution 9 - Bash

Just trying to complete the following answer: https://stackoverflow.com/a/32201766/2609399 , which is a very good catch, BTW.

I've faced this limitation a time ago and applied a similar fix for it. Please consider the below code sample.

invokeBashFunction() {
  local functionExitCode="0"
  /bin/bash -c "
    set -o errexit

    ${*}
  " || functionExitCode="${?}"

  # add some additional processing logic/code

  return "${functionExitCode}"
}
export -f invokeBashFunction

And there are few example of how to use it:

invokeBashFunction bad_function param1 "param 2" param3 && echo "It passed." || echo "It failed!"
if invokeBashFunction bad_function param1 "param 2" param3
then
  echo "It passed."
fi
if ! invokeBashFunction bad_function param1 "param 2" param3
then
  echo "It failed!"
fi

Solution 10 - Bash

what if you want to know exit status of bad_command?

I think the simplest way is to disable errexit:

#!/bin/sh
set -o errexit

some_code_here

set +o errexit
bad_command
status=$?
set -o errexit
process $status

Solution 11 - Bash

In bash you can use the trap builtin which is quite nice. See https://unix.stackexchange.com/questions/79648/how-to-trigger-error-using-trap-command

Not sure how portable it is with other shells.. so YMMV

Solution 12 - Bash

I cobbled together a (hopefully) textbook example from all the answers:

#!/usr/bin/env bash

# exit immediately on error
set -o errexit

file='dfkjlfdlkj'
# Turn off 'exit immediately on error' during the command substitution
blah=$(set +o errexit && ls $file) && rc=$? || rc=$?
echo $blah
# Do something special if $rc
(( $rc )) && echo failure && exit 1
echo success

Solution 13 - Bash

A clean reliable way to error exit

command_that_error_exits || { echo "Line $LINENO: Failed with Error" 1>&2; exit 1;}

Solution 14 - Bash

The accepted answer is good, but I think it could be refactored to be even better; more generic, easier to refactor and read:

some_command_status=$(some_command && echo $? || echo $?)

vs.

some_command && some_command_status=$? || some_command_status=$?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDaniel YC LinView Question on Stackoverflow
Solution 1 - BashrrauenzaView Answer on Stackoverflow
Solution 2 - BashgeorgeView Answer on Stackoverflow
Solution 3 - BashjoschView Answer on Stackoverflow
Solution 4 - BashyaroligView Answer on Stackoverflow
Solution 5 - Bashuser2870994View Answer on Stackoverflow
Solution 6 - BashshellterView Answer on Stackoverflow
Solution 7 - BashpjhView Answer on Stackoverflow
Solution 8 - BashjarnoView Answer on Stackoverflow
Solution 9 - BashTeodorView Answer on Stackoverflow
Solution 10 - BashAndrey KartashovView Answer on Stackoverflow
Solution 11 - Bashdemented hedgehogView Answer on Stackoverflow
Solution 12 - Bashjones77View Answer on Stackoverflow
Solution 13 - BashJoviano DiasView Answer on Stackoverflow
Solution 14 - BashChris DunderView Answer on Stackoverflow