Make shopt change local to function
Asked Answered
V

4

36

I'm trying to write a bash function that uses nocasematch without changing the callers setting of the option. The function definition is:

is_hello_world() {
  shopt -s nocasematch
  [[ "$1" =~ "hello world" ]] 
}

Before I call it:

$ shopt nocasematch
nocasematch     off

Call it:

$ is_hello_world 'hello world' && echo Yes
Yes
$ is_hello_world 'Hello World' && echo Yes
Yes

As expected, but now nocasematch of the caller has changed:

$ shopt nocasematch
nocasematch     on

Is there any easy way to make the option change local to the function?

I know I can check the return value of shopt -q but that still means the function should remember this and reset it before exit.

Vair answered 29/8, 2012 at 13:56 Comment(0)
P
54

The function body can be any compound command, not just a group command ( {} ). Use a sub-shell:

is_hello_world() (
  shopt -s nocasematch
  [[ "$1" =~ "hello world" ]] 
)
Praedial answered 29/8, 2012 at 14:0 Comment(6)
foo () {...} just looks so natural, you never think that the braces aren't part of the function syntax, rather than the mostly commonly used compound command.Praedial
Will this impact performance due to process' context switching spawning a new shell?Door
@Door There will be some overhead in spawning new processes. However, if it becomes noticeable or problematic in your shell script, that's a sign that you should be using a different language. A good rule of thumb is that the running time of any shell script should be dominated by the programs it runs, not the shell itself.Praedial
You may prefer to store the old value and then restore it. Subshelling is inefficient, but in the particular case of Bash, under some circumstances, it optimizes the process call reusing the same process rather than forking. $SHELLOPTS and $BASHOPTS are your friends.Aflutter
@chepner, indeed... what if foo () { ( # ...; ); }?Eternalize
That doesn't clear it up. What you have is legal; it's not clear if you are asking why it is legal, or why you might want to use both a subshell and a command group, or what? Perhaps you could be specific, rather than cryptic?Praedial
F
28

Use a RETURN trap

Commands specified with an RETURN trap are executed before execution resumes after a shell function ... returns...

The -p option [to shopt] causes output to be displayed in a form that may be reused as input.

https://www.gnu.org/software/bash/manual/bash.html

foobar() {
    trap "$(shopt -p extglob)" RETURN
    shopt -s extglob

    # ... your stuff here ...

}

For test

foobar() {
    trap "$(shopt -p extglob)" RETURN
    shopt -s extglob

    echo "inside foobar"
    shopt extglob # Display current setting for errexit option
}

main() {
    echo "inside main"
    shopt extglob # Display current setting for errexit option
    foobar
    echo "back inside main"
    shopt extglob # Display current setting for errexit option
}

Test

$ main
inside main
extglob         off
inside foobar
extglob         on
back inside main
extglob         off

Variation: To reset all shopt options, change the trap statement to:

trap "$(shopt -p)" RETURN

Variation: To reset all set options, change the trap statement to:

trap "$(set +o)" RETURN

Note: Starting with Bash 4.4, there's a better way: make $- local.

Variation: To reset all set and all shopt options, change the trap statement to:

trap "$(set +o); $(shopt -p)" RETURN

NOTE: set +o is equivalent to shopt -p -o:

+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.

Open Group Base Specifications Issue 7, 2018 edition > set

Floorboard answered 12/6, 2018 at 2:35 Comment(4)
This is a really excellent, useful answer. However, would "$(set +o; shopt -p;)" not work more correctly than "$(set +o)$(shopt -p)"? To see the difference, try echo "$(set +o; shopt -p;)" and carefully compare the result to that of echo "$(set +o)$(shopt -p)". Specifically, examine the last line returned by set and the first line returned by shopt.Tabular
Good catch @thb. I added a semicolon between the set +o and shopt -p outputs.Floorboard
More specific citations: shopt trapSideling
Making $- local doesn't work for all cases, as it only reverts those options that are set via set, but not all options that can be set via shopt can be set via set.Staphyloplasty
G
15

I know this post date from 2012, but you can also do the following (works in Git Bash 1.8.4 on Windows, so it should work on Linux) :

function foobar() {
    local old=$(shopt -p extglob)
    shopt -s extglob

    ... your stuff here ...
    eval "$old"
}

The -p option simply print shopt -s extglob if extglob is on, otherwise shopt -u extglob.

shopt -p print the whole list of options.

Germayne answered 27/11, 2013 at 23:27 Comment(3)
If there exists information that enhances the discussion it is always welcome. The solution you suggest, remembering the setting and changing it back, is what I was trying to avoid. I should have been more explicit about it. Anyway, it is worth keeping your answer as a good alternative of how to go about it. +1Vair
For my case, I did not care about it and I admit this might be as bad as tempering with IFS. On the other hand, I suppose it would be easy to make a pushshopt function like there is a pushd, and use bash arrays to remember previous option before setting them. Like pushshopt +extglob -nocasematch and popshopt.Germayne
Note that if you use set -e and you separate the statement into local old; old=$(shopt -p extglob), the second statement will error out because as man bash(1) states: "The return status when listing options is zero if all optnames are enabled, non-zero otherwise". In that case, you need to write something like local old; old=$(shopt -p extglob || true). The reason it works when using one statement is that local swallows the return type of the right hand side (e.g. local foo=$(false) actually returns the zero status code); that's one of bash's many pitfalls.Urbano
C
3

You can use an associative array to remember the previous setting and then use it for reverting to the earlier setting, like this:

shopt_set

declare -gA _shopt_restore
shopt_set() {
    local opt count
    for opt; do
        if ! shopt -q "$opt"; then
            echo "$opt not set, setting it"
            shopt -s "$opt"
            _shopt_restore[$opt]=1
            ((count++))
        else
            echo "$opt set already"
        fi
    done
}

shopt_unset

shopt_unset() {
    local opt restore_type
    for opt; do
        restore_type=${_shopt_restore[$opt]}
        if shopt -q "$opt"; then
            echo "$opt set, unsetting it"
            shopt -u "$opt"
            _shopt_restore[$opt]=2
        else
            echo "$opt unset already"
        fi
        if [[ $restore_type == 1 ]]; then
            unset _shopt_restore[$opt]
        fi
    done
}

shopt_restore

shopt_restore() {
    local opt opts restore_type
    if (($# > 0)); then
        opts=("$@")
    else
        opts=("${!_shopt_restore[@]}")
    fi
    for opt in "${opts[@]}"; do
        restore_type=${_shopt_restore[$opt]}
        case $restore_type in
        1)
            echo "unsetting $opt"
            shopt -u "$opt"
            unset _shopt_restore[$opt]
            ;;
        2)
            echo "setting $opt"
            shopt -s "$opt"
            unset _shopt_restore[$opt]
            ;;
        *)
            echo "$opt wasn't changed earlier"
            ;;
        esac
    done
}

Then use these functions as:

... some logic ...
shopt_set nullglob globstar      # set one or more shopt options
... logic that depends on the above shopt settings
shopt_restore nullglob globstar  # we are done, revert back to earlier setting

or

... some logic ...
shopt_set nullglob
... some more logic ...
shopt_set globstar
... some more logic involving shopt_set and shopt_unset ...
shopt_restore             # restore everything

Complete source code here: https://github.com/codeforester/base/blob/master/lib/shopt.sh

Charles answered 28/6, 2019 at 14:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.