2

I would like to temporarily change the value of a certain shopt option in a function.

For options which can be set with the builtin set this is possible with local - inside the function body. The following is an example code

#!/bin/bash

# Enable the options globally 
set -u
[[ "$SHELLOPTS" =~ nounset ]] && echo "1: nounset enabled"
shopt -s "extglob"
[[ "$BASHOPTS" =~ extglob ]] && echo "1: extglob enabled"

fun () {
    #Enable the options locally just in fun
    local -
#    local BASHOPTS
    set +u
    [[ "$SHELLOPTS" =~ nounset ]] || echo "2: nounset disabled"
    shopt -u "extglob"
    [[ "$BASHOPTS" =~ extglob ]] || echo "2: extglob disabled"
}

fun

if [[ "$SHELLOPTS" =~ nounset ]]; then
    echo "3: nounset enabled"
else
    echo "3: nounset disabled"
fi

if [[ "$BASHOPTS" =~ extglob ]]; then
    echo "3: extglob enabled"
else
    echo "3: extglob disabled"
fi

with the output

1: nounset enabled
1: extglob enabled
2: nounset disabled
2: extglob disabled
3: nounset enabled
3: extglob disabled

As can be seen the option nounset has been localized simply by adding local - to the function body. I want the same for shopt options. My current workaround is first checking if a certain option is enabled

if ! shopt -q extglob; then
    extglobchanged=1
    shopt -s extglob
fi

and then changing it back to what it was before at the end.

[[ "$extglobchanged" == 1 ]] && shopt -u extglob

This is not a good solution, because I have to take care of every case where the code in between could fail (I have to make sure the line above is executed before the function exits).

Question: Is it possible to localize shopt options?

--Edit 1--

Motivation

I have a function in my bash toolkit which queries the user for yes or no. I use case for that purpose and I use extended globs in order to link the patterns with an or. Here is my code

 yesnoquery () {
    local extglobchanged=0
    local returnvalue=2
    while true; do
    if read -p "$1" answer; then
        if ! shopt -q extglob; then
        extglobchanged=1
        shopt -s extglob #enable for pattern matching
        fi
        eval '
        case "$answer" in
            @([Yy]|[Yy][Ee][Ss])) returnvalue=0;; #true
            @([Nn]|[Nn][Oo])) returnvalue=1;; #false
            * ) echo "Please answer \"y\" or \"n\"";;   
        esac
        '
        [[ "$extglobchanged" == 1 ]] && shopt -u extglob #revert changes
        [[ "$returnvalue" != 2 ]] && return "$returnvalue"
    else
        return 2 #reading returned error
    fi
    done
}

It looks very long for such a trivial task. If I could localize the shopt option extglob, then I could directly return from within the case like @([Yy]|[Yy][Ee][Ss])) return 0;;. Furthermore, I would not need the [[ "$extglobchanged" == 1 ]]... line and the line after that.

main problem

At the moment I have to manually add code at every location where the function could exit to revert my changes to shopt.

My idea was that I need to localize the changes, just the way I can do it with local -. A different solution without localization would be perfectly fine.

If this is simply a language limitation then that also answers my question.

  • Explore using the RETURN trap. Take care of its interaction with set -T and declare -t, and of the fact that the RETURN trap will be run by all the callers of your function. – mosvy Feb 14 at 18:09
  • I think this is a good idea, thx. Like trap the RETURN within the function and change extglob back to what it was as the command of the trap, whenever the function exits at any place. That way I only need one command for all returns within a case statement. Furthermore, the task is basically doing clean up here, which seems to be the most common use case of traps. – Max1 Feb 15 at 11:11
0

Yes, its posible:

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


       :
       :

       set +vx; eval "$oldstate"
     }

There is an special case with errexit.

  • Thx, his is certainly more terse than my attempt, however I still need to make sure that eval "$oldstate" is seen by bash. This means for example that any return statement must have eval "$oldstate" prepended. I don't have that trouble with local -. The state of the shopt options is still changed globally in the example above. – Max1 Feb 14 at 11:31
  • @Max1 yes, but shopt is global, you can't get around that, it sets the options for the shell, there is no concept of scope there. So the best you can do is, as Isaac showed, save the current state and then restore it when exiting the function. To be honest, this feels like an XY problem. You might want to edit your question and explain why you want this. Why would you need to have shopt options localized? – terdon Feb 14 at 11:34
  • @terdon yes, I initially formulated an XY problem. I added more motivation and formulated the main problem in --Edit 1--. – Max1 Feb 14 at 17:58
0

I think I found a solution that suits my needs by executing the function body in a subshell. The advantages and disadvantages of executing the function body in a subshell, as well as the typical use cases, have been discussed here numerous times [1][2][3].

As stated in [4] taken from the bash man page the general syntax of a function is

[ function ] name () compound-command [redirection]

A Compound comamand can be a child process of the current shell, which is initiated with the round bracket syntax (list). man bash gives the following definition (own emphasis):

(list) list is executed in a subshell environment (see COMMAND EXECUTION ENVIRONMENT below). Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.

For my use case we want two things:

  • Localized effect of changes to the shopt option extglob.
  • Preservation of the return status

Due to the subshell changes to the shopt option extglob will not leak into the surrounding shell. Furthermore, we can return at any point from the function. A negative point is of course that starting a sub shell will cost additional resources.

Demonstration

The now much shorter functions looks like this:

yesnoquery () (
    while true; do
        if read -p "$1" answer; then
            shopt -s extglob
            eval '
            case "$answer" in
                @([Yy]|[Yy][Ee][Ss]) ) return 0;; #true
                @([Nn]|[Nn][Oo]) ) return 1;; #false
                * ) echo "Please answer \"yes\" or \"no\"";;   
            esac
            '
        else
            return 2 #reading returned error
        fi
    done
)

Within the case statement we can now directly return instead of saving to a temporary variable. Localized variables are not necessary anymore and two conditions have been saved.

That the shopt options are indeed local can quickly be seen with the example code from the code in the question

#!/bin/bash

# Enable the options globally 
set -u
[[ "$SHELLOPTS" =~ nounset ]] && echo "1: nounset enabled"
shopt -s "extglob"
[[ "$BASHOPTS" =~ extglob ]] && echo "1: extglob enabled"

fun () {
    set +u
    [[ "$SHELLOPTS" =~ nounset ]] || echo "2: nounset disabled"
    shopt -u "extglob"
    [[ "$BASHOPTS" =~ extglob ]] || echo "2: extglob disabled"
}

fun

if [[ "$SHELLOPTS" =~ nounset ]]; then
    echo "3: nounset enabled"
else
    echo "3: nounset disabled"
fi

if [[ "$BASHOPTS" =~ extglob ]]; then
    echo "3: extglob enabled"
else
    echo "3: extglob disabled"
fi

All that's been changed is the bracketing here ( instead of { (and superfluous local variables removed). The output

1: nounset enabled
1: extglob enabled
2: nounset disabled
2: extglob disabled
3: nounset enabled
3: extglob enabled

shows that the effects have been successfully localized.

Summary

Subshell execution environment as a function body has been proven useful to localize effects of bash shell options. case statements within the function body can profit from extglob, as changes to this variable will not affect code outside the scope of the function. If resources are scars the additional overhead of starting a subshell might unnecessarily deteriorate the performance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.