1

While the Bash man page states that:

The ERR trap is not executed if the failed command is ... part of a command executed in a && or || list ...

I hoped that code in a subshell would be in a different context and would not be subject to the above restriction. The code below shows that even subshells are not immune from this restriction:

#!/bin/bash

main()
{
   local Arg="$1"
   (
      set -e
      echo "In main: $Arg"
      trap MyTrap ERR
      $(exit 1)
      echo "Should not get here"
   )

   return 1
}

MyTrap()
{
   echo "In MyTrap"
}

main 1
[[ $? -eq 0 ]] || echo "failed"
echo
main 2 || echo "failed"

The above code has the following output:

In main: 1
In MyTrap
failed

In main: 2
Should not get here
failed

My present workaround is to use file persistence to save error states in MyTrap and then inspect return codes back in the caller. For example:

MyTrap()
{
   echo "_ERROR_=$?" > $HOME/.persist
   echo "In MyTrap"
}

main 1
[[ -f $HOME/.persist ]] && . $HOME/.persist || _ERROR_=0
[[ $_ERROR_ -eq 0 ]] || echo "failed"

The output of the above is now:

In main: 1
In MyTrap
failed

So, the question is: Can an approach be found to do set -e/ERR subshell trapping that is immune to && and || restrictions that is simpler than the workaround above?

Notes: This question applies to:

  • Bash 4.2 and higher
  • Red Hat 7, CentOS 7, and related distros (that is, not Debian, etc.)
  • No third-party software should be used. Packages must be available, for example, via OS-provider packages in the [CentOS-7-x86_64-DVD-1611.iso][2] repository iso (and similarly for RHEL 7, Fedora 7, etc).
Jolta
  • 2,620
  • 1
  • 29
  • 42
Steve Amerige
  • 1,309
  • 1
  • 12
  • 28
  • 3
    How about... don't use `set -e`? – gniourf_gniourf Jan 22 '17 at 22:46
  • You may want to take a look at this post: http://stackoverflow.com/questions/41774696/bash-scripting-graceful-function-death-on-error/41774978?noredirect=1#comment70744209_41774978 – codeforester Jan 22 '17 at 22:55
  • @gniourf_gniourf Please let me know what you think will work instead? – Steve Amerige Jan 22 '17 at 23:16
  • 1
    @codeforester I checked your link. It looks like they are just adding more inline tests. Also, in my case, please note that operations are happening in a subshell. I've already got it very nicely working except for the workaround that I'm trying to avoid. Also, I don't want to test every command that can fail. That's why I'm using `ERR` trap with `set -e`. It is saving me a huge amount of work and is making the code more maintainable. Thanks for the comment. – Steve Amerige Jan 22 '17 at 23:19
  • "More maintainable" is questionable. `set -e` behavior varies wildly between different releases of the shell and different runtime configurations; your code may *look* cleaner, but that doesn't mean it's behaving in a consistent and predictable manner, which I'd argue is more important to maintainability than terseness. If you haven't reviewed it hitherto, see [BashFAQ #105](http://mywiki.wooledge.org/BashFAQ/105). – Charles Duffy Feb 02 '17 at 17:00
  • @SteveAmerige, ...so, in short: (1) `set -e` is a highly contentious feature within the bash community, with a substantial set of greyhairs considering it more trouble than it's worth; and (2) for folks within that set, "more inline tests" is in fact the proposed alternative. Frankly, that's an opinion that's shared by even some modern languages' designers -- see Golang's approach to explicit error handling, for example. – Charles Duffy Feb 02 '17 at 17:06
  • That said, http://fvue.nl/wiki/Bash:_Error_handling provides a review of necessary workarounds that you may find useful -- though it *doesn't* go through the version history of `set -e`, and thus caveats you may find in various historical shell releases. If you need compatibility across platforms, here there be dragons still. – Charles Duffy Feb 02 '17 at 17:07

1 Answers1

1

This code is NOT a solution. It is a modified version that you can test to give you some ideas.

#!/bin/bash

main()
{
   local Arg="$1"
   (
      echo "In main: $Arg"
      $(exit 1)
      echo "Should not get here"
   )

   return 1
}

MyTrap()
{
   echo "In MyTrap"
   exit 1
}

set -o errtrace
set -o functrace
trap MyTrap ERR

main 1
[[ $? -eq 0 ]] || echo "failed"
echo
main 2 || echo "failed"

It will not do what you would like it to do (I assume), because there is something important missing that is too involved to explain in a post, but there are a few key things.

If you want to handle errors uniformly in all your script, you will have to do the following.

  • Set the trap at the top level, and do NOT use set -e
  • Use the shell options that cause subshells and functions to inherit traps (shown in the example)
  • Create a framework where you differentiate between "expected" errors (explicitely handled as some kind of exception, not as return codes), and "unexpected" exceptions (bugs), which will be trapped.
  • Never use logical operators directly (except on test constructs or other simple statements you decide are safe enough)
  • Perform logical tests after collecting exceptions, not as tests on the return codes.

Building that framework is very tricky, but doable (I have it working flawlessly on dozens of very complex scripts). Once you have done it, if you chose to blindly test a command (without explicitly handling exceptions), if it fails you have the option to crash your script in the trap (exit) instead of continuing execution with your script in an unstable state.

This probably incurs a slight performance penalty (at least the way I did it, using a special try function preceding every statement for which exception handling is used), and sure requires a LOT of discipline in coding, but in my case it makes me way more productive building complex scripts with the reasonable confidence the location of any bug (not that I have any of those...) will be much easier to pinpoint.

Fred
  • 6,590
  • 9
  • 20
  • 1
    Yes, you're right in that there is a lot too involved for a post. Actually, I have implemented this in the context of a Bash-scripting framework that considers all of the things you've mentioned. In particular, I've implemented a try/catch mechanism entirely in Bash that even allows for nested try/catch with persistence of variables. But, all of that is too much for here, I agree! In trying to come up with a minimal test case, a lot is lost... and it is easy to omit things I've dealt with already. If you like, we can take this into a chat session. – Steve Amerige Jan 23 '17 at 00:50
  • Ok for a chat.. But I am too new here, I don't even know how to chat, so I will need a bit of help. – Fred Jan 23 '17 at 00:58
  • At the bottom of each page is a chat link (in the dark-gray area). You can click there and then filter using `bash` and we can agree on a particular chat room. This is a good approach if you are interested in letting others easily chime in on the conversation. Alternatively, you can click on my profile link at the bottom of the original posting, then modify the URL to begin with `chat.` and then click the `start a new room with this user` button. The discussion is still public, but would get less traffic than if you use a common chat room. – Steve Amerige Jan 23 '17 at 09:39
  • I have created a public chat room called "Bash error handling". Is there a way to invite a user, or does that have to be done in comments? – Fred Jan 23 '17 at 19:02
  • It does require some coordination. You can set sound notifications in the chat window. I'm in the US, North Carolina (UTC-05:00) and work from 04:00 to 15:00 most days. I'll keep the chat session open for a day or two to the room you created. – Steve Amerige Jan 24 '17 at 10:45