59

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
Tilman Vogel
  • 9,337
  • 4
  • 33
  • 32
Daniel YC Lin
  • 15,050
  • 18
  • 63
  • 96
  • 5
    Don't set `errexit` if you want to handle errors. Handle errors if you want to handle errors. Setting `errexit` is abdicating from your responsibility to handle errors properly. I've never found it necessary to use `nounset`, but I am reasonably good at spelling consistently. – Jonathan Leffler Mar 09 '12 at 06:28
  • 34
    Always use errexit even if you want to handle errors. Any external command can fail and unless you plan on adding error handling for each|command|in|a&&work||flow then errexit will at least help protect you and the user. Same with nounset, not using it is like not using assert in other languages (because I never make errors, ever...) – Paul Hargreaves Sep 12 '12 at 20:54
  • FYI, none of these catch the full range of errexit codes. If your `bad_command` internally calls another command, e.g. `bad_com2`, then errexit would crash the entire program. However, the checks listed here would miss the exit code of `bad_com2` as long as it doesn't affect the exit code of `bad_command`. Given that you're asking this question, you're probably well aware of this limitation, but others might think that these methods are a replacement for errexit :-) – Hamy Jun 18 '14 at 06:54
  • My above comment shows a bad situation too - you can't have a 'test' for "will errexit causing a failure" - your only option is to enable it and see. – Hamy Jun 18 '14 at 06:56
  • 1
    @Daniel In your answer, you don't need backticks around `bad_command`. I.e., `if ! bad_command; then ... fi` is enough. – Linus Arver Sep 14 '16 at 20:10

14 Answers14

56

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
rrauenza
  • 6,285
  • 4
  • 32
  • 57
  • 2
    I like this method, it is lean. – Daniel YC Lin Apr 06 '13 at 23:17
  • why does this work? Does shell interpret this as `( command && rc=$? ) || rc=$?` ? – Michel Müller Apr 14 '16 at 11:21
  • 1
    See http://unix.stackexchange.com/questions/88850/precedence-of-the-shell-logical-operators – rrauenza Apr 14 '16 at 15:27
  • 2
    @MichelMüller it works because the whole statement works together and the final err code is 0, so errexit doesn't exit. Yes, your interpretation, with parentheses, appears correct. In fact, you can test this: Try this ```ls fakefile && x=1 || x=2 ; echo $x``` – JDS Apr 21 '16 at 13:41
  • 3
    I find having `&&` and `||` in the same line somehow unintuitive. Another way is to first define `rc=0` and only set `rc` to the actual exit code if the command failed: `rc=0; cat /tmp/doesnotexist || rc=$?`. This works because the piece of code after your `&&` will always set `rc` to zero so you could as well modify your own code to say `... && rc=0 || ...` – josch Oct 20 '18 at 18:41
  • ok, short, but it's a bit hacky… I don't believe there is no cleaner way :-/ – pihentagy Feb 04 '20 at 21:05
26

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.

ЯegDwight
  • 24,821
  • 10
  • 45
  • 52
george
  • 261
  • 3
  • 2
9

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
josch
  • 6,716
  • 3
  • 41
  • 49
7

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.

yarolig
  • 305
  • 3
  • 5
  • Can you expand on what to do so it works well in bash 4.2? Also, what about newer bash versions (4.3, 4.4, 5.0)? – Pedro A Jun 08 '22 at 17:15
  • Thank you for this solution, I've referenced it in: https://stackoverflow.com/a/76861665/130638 – balupton Aug 09 '23 at 15:45
6
set -o errexit

bad_command || {
  resp_code=$?
  echo Bad Thing $resp_code happened
}
user2870994
  • 61
  • 1
  • 1
4

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.

pjh
  • 6,388
  • 2
  • 16
  • 17
  • 1
    Note, `PIPESTATUS` is an array because it works also on multiple commands, for example given `! good_cmd_one | bad_cmd_two`, we would get `${PIPESTATUS[0]} -eq 0` and ${PIPESTATUS[1]} -eq 1` – lanoxx Mar 28 '22 at 06:42
  • Doesn't seem to work with functions, also it disables errexit for the bad_command. – balupton Aug 09 '23 at 15:44
  • @balupton, I haven't seen any problems using this with functions. For instance, `bash -ce 'function bad_command { return 1; }; ! bad_command; rc=${PIPESTATUS[0]}; echo "rc=$rc"'` outputs `rc=1`, indicating that the function return value has been successfully detected. – pjh Aug 09 '23 at 23:46
4

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.

jarno
  • 787
  • 10
  • 21
3

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.

shellter
  • 36,525
  • 7
  • 83
  • 90
1

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
  • 6
    This technique is ok if you only have to catch one command, but if you want to correctly handle lots of commands that may return codes (like grep, diff etc) then it'll prove painful. – Paul Hargreaves Sep 12 '12 at 20:51
  • @PaulHargreaves but this seems really the only way to get the error code of the command. Probably needed, but useful in some cases. – vadipp Feb 08 '13 at 03:43
1

A clean reliable way to error exit

command_that_error_exits || { echo "Line $LINENO: Failed with Error" 1>&2; exit 1;}
Joviano Dias
  • 1,043
  • 10
  • 13
1

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
Teodor
  • 114
  • 7
0

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

Community
  • 1
  • 1
demented hedgehog
  • 7,007
  • 4
  • 42
  • 49
0

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
jones77
  • 505
  • 1
  • 4
  • 16
-1

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=$?
Chris Dunder
  • 199
  • 2
  • 6
  • 1
    Note that using `$(some_command ...)` may create an extra subprocess (particularly in Bash) and cause performance problems. Also, there are rare circumstances where a command will behave significantly differently if run in a subprocess. – pjh Dec 13 '16 at 20:13