15

I have read through many questions on various stack exchange sites and unix help forums on how to modify shell options and then restore them - the most comprehensive one I found on here is at How to "undo" a `set -x`?

The received wisdom seems to be to either save off the result of set +o or shopt -po and then eval it later to restore the previous settings.

However, in my own testing with bash 3.x and 4.x, the errexit option does not get saved correctly when doing command substitution.

Here is an example script to show the issue:

set -o errexit
set -o nounset
echo "LOCAL SETTINGS:"
set +o
OLDOPTS=$(set +o)
echo
echo "SAVED SETTINGS:"
echo "$OLDOPTS"

And output (I trimmed out some of the irrelevant variables):

LOCAL SETTINGS:
set -o errexit
set -o nounset

SAVED SETTINGS:
set +o errexit
set -o nounset

This seems extremely dangerous. Most scripts I write depend on errexit to halt execution if any commands fail. I just located a bug in one of my scripts caused by this, where the function that was supposed to restore errexit at the end wound up overriding it, setting it back to the default of off for the duration of the script.

What I'd like to be able to do is write functions that can set options as needed and then restore all the options properly before exiting. But it seems as if in the subshell invoked by the command substitution, errexit is not inherited.

I'm at a loss for how to save off the result of set +o without using command substitution or jumping through FIFO hoops. I can read from $SHELLOPTS but it is not writable or in eval-able format.

I know one alternative is to use a subshell function, but that introduces a lot of headaches for being able to log output as well as pass back multiple variables.

Probably related: https://stackoverflow.com/questions/29532904/bash-subshell-errexit-semantics (seems there is a workaround for bash 4.4 and up but I'd rather have a portable solution)

1
  • This doesn't restore bash shopt set options (like nullglob). Commented Sep 23, 2018 at 19:03

5 Answers 5

11

What you're doing should work. But bash turns off the errexit option in command substitutions, so it preserves all the options except this one. This is specific to bash and specific to the errexit option. Bash does preserve errexit when running in POSIX mode. Since bash 4.4, bash also doesn't clear errexit in a command substitution if shopt -s inherit_errexit is in effect.

Since the option is turned off before any code runs inside the command substitution, you have to check it outside.

OLDOPTS=$(set +o)
case $- in
  *e*) OLDOPTS="$OLDOPTS; set -e";;
  *) OLDOPTS="$OLDOPTS; set +e";;
esac

If you don't like this complexity, use zsh instead.

setopt local_options
6
  • 5
    Note that bash4.4 now has local - (à la ash) as an equivalent to zsh's setopt localoptions (or what ksh88 does by default) Commented Oct 6, 2017 at 6:06
  • See also shopt -s lastpipe; set +o | IFS= read -rd '' OLDOPTS || : (the two sets of options is another of bash's idiosyncrasies). Commented Oct 6, 2017 at 6:15
  • 6
    Simpler: OLDOPTS="$(set +o); set -$-". Commented Oct 20, 2018 at 16:19
  • 2
    @StéphaneChazelas Where can I find docs on local -? Commented Nov 20, 2022 at 4:55
  • @user232326 Error /bin/sh: set: line 582: illegal option -s when running set -xse. Commented Dec 25, 2023 at 9:06
3

After trying old of the above on alpine 3.6, I've now taken the following much simpler approach:

OLDOPTS="$(set +o); set -${-//c}"
set -euf -o pipefail

... my stuff

# restore original options
set +vx; eval "${OLDOPTS}"

as per documentation, "$-" holds the list of currently active options. Seems to work great, am I missing anything?

4
  • @Isaac I use 'set +euf' because $- only contains the list of active options. i.e. when resetting I first deactivate everything and then reactivate only the list of previously active options (I choose 'euf' because those are the only options I usually modify - for general purpose use it probably should contain the full list of possible options). Good point about 'set +vx', I modified the example above accordingly Commented Oct 22, 2018 at 2:36
  • that may work for errexit but not e.g. for bounds checking or globbing (set -uf) Commented Oct 22, 2018 at 2:48
  • @Isaac I stand corrected. I don't know what I did wrong. Thanks a tons for your help! Also good to see that you ran into and solved the -c flag problem - I wish I had seen your comment earlier ;) Commented Nov 6, 2018 at 21:07
  • I updated above solution based on @Isaac 's feedback Commented Nov 7, 2018 at 10:51
1

errexit is propagated into process substitutions.

set -e

# Backup restore commands into an array
declare -a OPTS
readarray -t OPTS < <(shopt -po)

set +e

# Restore options
declare cmd
for cmd in "${OPTS[@]}"; do
    eval "$cmd"
done

Check:

$  shopt -po errexit
set -o errexit

Bash version:

$ bash --version
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
1

The simple solution is to append the errexit setting to OLDOPTS:

OLDOPTS="$(set +o)"
[ "${BASH_VERSION:+x}" ] && shopt -qo errexit && OLDOPTS+=";set -e" || true

Done.

3
  • See also [[ -o errexit ]] (ksh/bash/zsh) and [ -o errexit ] (bash/ksh/yash) Commented Nov 7, 2018 at 13:59
  • The shopt is a valid command for bash, there is no meaningful reason to avoid it (I is only a matter of your personal preference of how answers should be written). Not a meaningful change. @StéphaneChazelas Commented Nov 9, 2018 at 6:15
  • Solution is not working in /bin/sh. Commented Dec 25, 2023 at 9:21
1

Here's how Pacman's makepkg goes about restoring shell options and preserving errexit after executing contents of run_function()

run_function() {
    # ...
}

error_function() {
    # ...
}

run_function_safe() {
    local restoretrap restoreshopt

    # we don't set any special shopts of our own, but we don't want the user to
    # muck with our environment.
    restoreshopt=$(shopt -p)

    # localize 'set' shell options to this function - this does not work for shopt
    local -
    shopt -o -s errexit errtrace

    restoretrap=$(trap -p ERR)
    trap "error_function '$1'" ERR

    run_function "$1" "$2"

    trap - ERR
    eval "$restoretrap"
    eval "$restoreshopt"
}
1
  • Nice. You may want to add a note that local - requires Bash 4.4 or later, at least according to this comment. Commented Jan 7 at 18:41

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.