How to restore the value of shell options like `set -x`?

Abstract

To reverse a set -x just execute a set +x. Most of the time, the reverse of an string set -str is the same string with a +: set +str.

In general, to restore all (read below about bash errexit) shell options (changed with set command) you could do (also read below about bash shopt options):

oldstate="$(set +o)"                # POSIXly store all set options.
.
.
set -vx; eval "$oldstate"         # restore all options stored.

Should be enough, but bash has two groups of options accessed via set (or shopt -po) and some others accessed via shopt -p. Also, bash doesn't preserve set -e (errexit) on entering subshells. Note that the list of options that results from expanding $- might not be valid to re-enter in a shell.

To capture the whole present state (in bash) use:

oldstate="$(shopt -po; shopt -p)"; [[ -o errexit ]] && oldstate="$oldstate; set -e"

Or, if you don't mind setting the inherit_errexit flag (and your bash is ≥4.4):

shopt -s inherit_errexit;    oldstate="$(shopt -po; shopt -p)"

Longer Description

bash

This command:

shopt -po xtrace

is used to generate an executable string that reflects the state of the option(s). The p flag means print, and the o flag specifies that we are asking about option(s) set by the set command (as opposed to option(s) set only by the shopt command). You can assign this string to a variable, and execute the variable at the end of your script to restore the initial state.

# store state of xtrace option.
tracestate="$(shopt -po xtrace)"

# change xtrace as needed
echo "some commands with xtrace as externally selected"
set -x
echo "some commands with xtrace set"

# restore the value of xtrace to its original value.
eval "$tracestate"

This solution also works for multiple options simultaneously:

oldstate="$(shopt -po xtrace noglob errexit)"

# change options as needed
set -x
set +x
set -f
set -e
set -x

# restore to recorded state:
set +vx; eval "$oldstate"

Adding set +vx avoids the printing of a long list of options.


If you don’t list any option names,

oldstate="$(shopt -po)"

it gives you the values of all (set) options. And, if you leave out the o flag, you can do the same things with shopt options:

# store state of dotglob option.
dglobstate="$(shopt -p dotglob)"

# store state of all options.
oldstate="$(shopt -p)"

If you need to test whether a set option is set, the most idiomatic (Bash) way to do it is:

[[ -o xtrace ]]

which is better than the other two similar tests:

  1. [[ $- =~ x ]]
  2. [[ $- == *x* ]]

With any of the tests, this works:

# record the state of the xtrace option in ts (tracestate):
[ -o xtrace ] && ts='set -x' || ts='set +x'

# change xtrace as needed
echo "some commands with xtrace as externally selected"
set -x
echo "some commands with xtrace set"

# set the xtrace option back to what it was.
eval "$ts"

Here’s how to test the state of a shopt option:

if shopt -q dotglob
then
        # dotglob is set, so “echo .* *” would list the dot files twice.
        echo *
else
        # dotglob is not set.  Warning: the below will list “.” and “..”.
        echo .* *
fi

POSIX

A simple, POSIX-compliant solution to store all set options is:

set +o

which is described in the POSIX standard as:

+o

    Write the current option settings to standard output in a format that is suitable for reinput to the shell as commands that achieve the same options settings.

So, simply:

oldstate=$(set +o)

will preserve values for all options set using the set command (in some shells).

Again, restoring the options to their original values is a matter of executing the variable:

set +vx; eval "$oldstate"

This is exactly equivalent to using Bash's shopt -po. Note that it will not cover all possible Bash options, as some of those are set (only) by shopt.

bash special case

There are many other shell options listed with shopt in bash:

$ shopt
autocd          off
cdable_vars     off
cdspell         off
checkhash       off
checkjobs       off
checkwinsize    on
cmdhist         on
compat31        off
compat32        off
compat40        off
compat41        off
compat42        off
compat43        off
complete_fullquote  on
direxpand       off
dirspell        off
dotglob         off
execfail        off
expand_aliases  on
extdebug        off
extglob         off
extquote        on
failglob        off
force_fignore   on
globasciiranges off
globstar        on
gnu_errfmt      off
histappend      on
histreedit      off
histverify      on
hostcomplete    on
huponexit       off
inherit_errexit off
interactive_comments    on
lastpipe        on
lithist         off
login_shell     off
mailwarn        off
no_empty_cmd_completion off
nocaseglob      off
nocasematch     off
nullglob        off
progcomp        on
promptvars      on
restricted_shell    off
shift_verbose   off
sourcepath      on
xpg_echo        off

Those could be appended to the variable set above and restored in the same way:

$ oldstate="$oldstate;$(shopt -p)"
.
.                                   # change options as needed.
.
$ eval "$oldstate" 

bash's set -e special case

In bash, the value of set -e (errexit) is reset inside sub-shells, that makes it difficult to capture its value with set +o inside a $(…) sub-shell.

As a workaround, use:

oldstate="$(set +o)"; [[ -o errexit ]] && oldstate="$oldstate; set -e"

Or (if it doesn't contradict your goals and your bash supports it) you can use the inherit_errexit option.


Note: each shell has a slightly different way to build the list of options that are set or unset (not to mention different options that are defined), so the strings are not portable between shells, but are valid for the same shell.

zsh special case

zsh also works correctly (following POSIX) since version 5.3. In previous versions it followed POSIX only partially with set +o in that it printed options in a format that was suitable for reinput to the shell as commands, but only for set options (it didn't print un-set options).

mksh special case

The mksh (and by consequence lksh) is not yet (MIRBSD KSH R54 2016/11/11) able to do this. The mksh manual contains this:

In a future version, set +o will behave POSIX compliant and print commands to restore the current options instead.


With the Almquist shell and derivatives (dash, NetBSD/FreeBSD sh at least) and bash 4.4 or above, you can make options local to a function with local - (make the $- variable local if you like):

$ bash-4.4 -c 'f() { local -; set -x; echo test; }; f; echo no trace'
+ echo test
test
no trace

That doesn't apply to sourced files, but you can redefine source as source() { . "$@"; } to work around that.

With ksh88, option changes are local to the function by default. With ksh93, that's only the case for functions defined with the function f { ...; } syntax (and the scoping is static compared to the dynamic scoping used in other shells including ksh88):

$ ksh93 -c 'function f { set -x; echo test; }; f; echo no trace'
+ echo test
test
no trace

In zsh, that's done with the localoptions option:

$ zsh -c 'f() { set -o localoptions; set -x; echo test; }; f; echo no trace'
+f:0> echo test
test
no trace

POSIXly, you can do:

case $- in
  (*x*) restore=;;
  (*) restore='set +x'; set -x
esac
echo test
{ eval "$restore";} 2> /dev/null
echo no trace

However some shells will output a + 2> /dev/null upon the restore (and you'll see the trace of that case construct of course if set -x was already enabled). That approach is also not re-entrant (like if you do that in a function that calls itself or another function that uses the same trick).

See https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh (local scope for variables and options for POSIX shells) for how to implement a stack that works around that.

With any shell, you can use subshells to limit the scope of options

$ sh -c 'f() (set -x; echo test); f; echo no trace'
+ echo test
test
no trace

However, that limits the scope of everything (variables, functions, aliases, redirections, current working directory...), not just options.


You can read the $- variable at the beginning to see whether -x is set or not and then save it to a variable e.g.

if [[ $- == *x* ]]; then
  was_x_set=1
else
  was_x_set=0
fi

From the Bash manual:

($-, a hyphen.) Expands to the current option flags as specified upon invocation, by the set builtin command, or those set by the shell itself (such as the -i option).

Tags:

Shell

Bash