Daniel YC Lin
Daniel YC Lin

Reputation: 16012

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

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

The following code will stop after bad_command.

#!/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_handling
good_command

Is there any better or cleaner method?

My Answer:

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

Upvotes: 65

Views: 35455

Answers (15)

demented hedgehog
demented hedgehog

Reputation: 7548

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

Edit.. comment below informs me that this is not portable so take care.

Upvotes: 1

ForDummies
ForDummies

Reputation: 151

I tried the early answers and they did not work when calling a program that segfaults. My use case is trying to catch a rare segfault and reporting which of a few thousand inputs caused it to occur. Instead, I based it on

   if [ $(./segfault >realstdout || echo error) == "error" ]
   then
      echo this died # and report details
   else
      echo strange success
   fi

This seems convoluted but I have not found a cleaner way.

Upvotes: 0

yarolig
yarolig

Reputation: 313

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.

Update: No problem with exit codes in bash 5.0.

Upvotes: 8

jarno
jarno

Reputation: 856

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.

Upvotes: 4

Teodor
Teodor

Reputation: 124

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

Upvotes: 1

Joviano Dias
Joviano Dias

Reputation: 1110

A clean reliable way to error exit

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

Upvotes: 1

josch
josch

Reputation: 7164

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

Upvotes: 9

jones77
jones77

Reputation: 511

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

Upvotes: 0

pjh
pjh

Reputation: 8134

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.

Upvotes: 4

Chris Dunder
Chris Dunder

Reputation: 309

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=$?

Upvotes: -1

rrauenza
rrauenza

Reputation: 6993

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

Upvotes: 58

user2870994
user2870994

Reputation: 71

set -o errexit

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

Upvotes: 7

Andrey Kartashov
Andrey Kartashov

Reputation: 21

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

Upvotes: 1

george
george

Reputation: 271

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.

Upvotes: 27

shellter
shellter

Reputation: 37288

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.

Upvotes: 3

Related Questions