A long time ago eutils.eclass was gifted with a set of terribly ugly functions to push/pop various variables and shell options. Those functions were written very badly, and committed without any review. As a result, a number of eclasses and ebuilds are now using that code without even understanding how bad it is.
In this post, I would like to shortly summarize how to properly and reliably save states of shell options. While the resulting code is a little bit longer than use of e*_push and e*_pop functions, it is much more readable, does not abuse eval, does not abuse global variables and is more reliable.
Preferable solution: subshell scope
Of course, the preferable way of altering shell options is to do that in a subshell. This is the only way that reliably isolates the alterations from parent ebuild environment. However, subshells are rarely desired — so this is something you’d rather reuse if it’s already there, rather than introducing just for the sake of shell option mangling.
Mangling shopt options
Most of the ‘new’ bash options are mangled using shopt builtin. In this case, the -s and -u switches are used to change the option state, while the -p option can be used to get the current value. The current value is output in the form of shopt command syntax that can be called directly to restore the previous value.
my_function() {
local prev_shopt=$(shopt -p nullglob)
# prev_shopt='shopt -u nullglob' now
shopt -s nullglob
# ...
${prev_shopt}
}
Mangling set options
The options set using the set builtin can be manipulated in a similar way. While the builtin support both short and long options, I strongly recommend using long options for readability. In fact, the long option names can be used through shopt with the additional -o parameter.
my_function() {
local prev_shopt=$(shopt -p -o noglob)
# prev_shopt='set +o noglob' now
set -o noglob # or shopt -s -o noglob
# ...
${prev_shopt}
}
Mangling umask
The umask builtin returns the current octal umask when called with no parameters. Furthermore, the -p parameter can be used to get full command for use alike shopt -p output.
my_function() {
local prev_umask=$(umask)
# prev_umask=0022 now
umask 077
# ...
umask "${prev_umask}"
}
alternative_function() {
local prev_umask=$(umask -p)
# prev_umask='umask 0022' now
umask 077
# ...
${prev_umask}
}
Mangling environment variables
The eutils hackery went as far as to reinvent local variables using… global stacks. Not that it makes any sense. Whenever you want to change variable’s value, attributes or just unset it temporarily, just use local variables. If the change needs to apply to part of a function, create a sub-function and put the local variable inside it.
While at it, please remember that bash does not support local functions. Therefore, you need to namespace your functions to avoid collisions and unset them after use.
my_function() {
# unset FOO in local scope (this also prevents it from being exported)
local FOO
# 'localize' bar for modifications, preserving value
local bar="${bar}"
#...
my_sub_func() {
# export LC_ALL=POSIX in function scope
local -x LC_ALL=POSIX
#...
}
my_sub_func
# unset the function after use
unset -f my_sub_func
}
Update: mangling shell options without a single subshell
(added on 2016-01-28)
izabera has brought it to my attention that the shopt builtin supports -q option to suppress output and uses exit statuses to return the original flag state. This makes it possible to set and unset the flags without using a single subshell or executing returned commands.
Since I do not expect most shell script writers to use such a long replacement, I present it merely as a curiosity.
my_setting_function() {
shopt -q nullglob
local prev_shopt=${?}
shopt -s nullglob
#...
[[ ${prev_shopt} -eq 0 ]] || shopt -u nullglob
}
my_unsetting_function() {
shopt -q extquote
local prev_shopt=${?}
shopt -u extquote
#...
[[ ${prev_shopt} -eq 0 ]] && shopt -s extquote
}
I usually use the RETURN trap to lock in the options to be reset at the time they’re initially toggled.
f() {
local -a shopts=(extglob dotglob nullglob lastpipe)
local opt
for opt in "${!shopts[@]}"; do
if shopt -q "${shopts[opt]}"; then
unset -v 'shopts[opt]'
else
shopt -s "${shopts[opt]}"
fi
done
(( ${#shopts[@]} )) && trap "trap RETURN; shopt -u ${shopts[*]}" RETURN
...
}
Technically a rare edge case in which `set -T` modifies the propagation of RETURN traps is possible but I’ve never had an issue with this method in practice.
In bash 4.4 you’ll be able to localize `set` options using `local -`. I’m a little disappointed it isn’t more general to also apply to `shopt` but it should still be useful.
f() {
local -
set -x # debug this function
# ...
}
“sub-functions” can be useful in some cases. Usually not purely internal uses like this though. A little rearranging yields a single function which doesn’t have the naming issues or necessity for unsetting the inner function.
my_function() {
if [[ ${FUNCNAME[0]} == ${FUNCNAME[1]} ]]; then
local -x LC_ALL=POSIX
# ...
else
local FOO bar=$bar
# ...
my_function
fi
}