Skip to content

More efficient way to save/restore shell options #691

Open
@akinomyoga

Description

@akinomyoga

Describe the feature/solution

When we need to change the shell options for certain operations, currently we save the original state using local reset=$(shopt -p nullglob), local reset=$(shopt -po noglob), etc. and then restore the state by $reset. However, this requires the extra cost of creating a subshell. Can we think about switching to a more efficient way to save/restore shell options? Maybe there is no simple way to achieve it by only using the builtin features directly, but we might consider preparing special helper functions for this purpose.

This discussion derived from #687. Here the existing suggestions there are quoted:

#687 (comment) @sudonym1

store_options() {
    declare -n state=$1
    shift
    for opt in $@
    do
        shopt -q $opt && state[$opt]="-s" || state[$opt]="-u"
    done
}

restore_options() {
    declare -n state=$1
    shift
    for opt in ${!state[@]}
    do
        shopt ${state[$opt]} $opt
    done
}

some_function() {
    declare -A opts
    set -x
    store_options opts localvar_unset

    restore_options opts
    set +x
}

What about this?

#687 (comment) @akinomyoga

Maybe we can just save the value of $BASHOPTS, and then later test it using [[ :$saved_bashopts: == *:"$opt":* ]]. This is actually what I do in my personal scripts.

_comp_restore_shopt() {
    local bashopts=$1; shift
    local opt
    for opt; do
        if [[ :$bashopts: == *:"$opt":* ]]; then
            shopt -s "$opt"
        else
            shopt -u "$opt"
        fi
    done
}
some_function() {
    local bashopts=$BASHOPTS
    set -x
    shopt -u localvar_unset

    _comp_restore_shopt "$bashopts" localvar_unset
    set +x
}

#687 (comment) @sudonym1

I like this approach overall, and it is straightforward. The only concern I have is that it is easy to forget an argument when calling _comp_restore_shopt. That could be avoided by looping over all options like this:

_comp_restore_shopt() {
    local bashopts=$1; shift
    local IFS=:
    for opt in $BASHOPTS $bashopts; do
        if [[ :$bashopts: == *:"$opt":* ]]; then
            shopt -s "$opt"
        else
            shopt -u "$opt"
        fi
    done
}

I think this approach is likely not worth the loss of efficiency compared with your approach.

We can use the same approach for set options using the special shell variable $- or $SHELLOPTS.

Note that Bash 4.4 supports local - where set options will be automatically restored when the control returns the function.

Edit: This discussion #352 (review) can also be related.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions