7

I have a function and would like to use the pipefail option inside. But I don't want to simply set -o pipefail, because I fear that another part of the script may not expect pipefail to be set. Of course I could do set +o pipefail afterwards, but in case pipefail was set outside of my function, this may introduce unexpected behavior as well.

Of course I could use the exit code of false | true as a measure, if pipefail is set, but this seems a little bit dirty.

Is there a more general (and maybe canonical?) way to check the set bash options?

1

5 Answers 5

12

In bash-4.4 and above, you can use:

myfunction() {
  local -
  set -o someoption
  ...
}

A few notes:

  • that's copied from ash. In ash, the idea is to make $- (which stores the set of options) local. It works in ash because in ash, local doesn't affect the value nor attributes of the variable (contrary to bash where it initially makes it unset). Still it also works with bash the same way as in ash as a special case, that is it doesn't cause $- to be unset or revert to a default behaviour
  • it only covers the options set with set -o option, not the ones set by shopt -s option (bash being the only shell with two sets of options!)
  • like for local variables, it is dynamic scoping, not static. The value of $- will be restored upon returning from the function, but the new setting will affect the other functions or sourced scripts called by your function. If you want static scoping, you'll need to switch to ksh93 and use:

    function myfunction {
      set -o myoption
      ...
      other_function
    }
    

    With the other_function not affected as long as it's declared with the function other_function { syntax as well (and not the Bourne other_function()... syntax).

  • As it doesn't reset the options, your function may still be affected by options set by other code. To avoid that, you'd want to use zsh instead. zsh's equivalent of local - is set -o localoptions, but you can also get to a well known emulation mode locally. For instance:

    myfunction() {
      emulate -L zsh
      set -o someoption
      ...
    }
    

    would start with a sane zsh-default set of options (with some exceptions, see doc) on top of which you add your option. That's also dynamic scoping like in ash/bash, but you can also call other functions with:

    emulate zsh -c 'other_function...'
    

    for that other_function to be called with a vanilla zsh option set.

    zsh also has a number of operators that can make you avoid changing options globally in the first place (like (N)/(D) glob qualifiers for nullglob/dotglob, (#i) glob operator for case insensitive globbing, ${(s:x:)var} to avoid mingling with $IFS, ${~var} to request globbing upon parameter expansion (you need noglob in other Bourne-like shells to avoid (!) it)...

10

The $SHELLOPTS variable holds all set options separated by colons. As an example, it may look like:

braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

To check for a given option one may do the following:

# check if pipefail is set, if not set it and remember this
if [[ ! "$SHELLOPTS" =~ "pipefail" ]]; then
    set -o pipefail;
    PIPEFAIL=0;
else
    PIPEFAIL=1;
fi

# do something here

# reset pipefail, if it was not set before
if [ "$PIPEFAIL" -eq 0 ]; then
    set +o pipefail;
fi
3
  • Note that bash has 2 sets of options (for no good reason, no other Bourne-like shell does that), one you set with set -o and seen in $SHELLOPTS and one you set with shopt -s (and seen in $BASHOPTS). Nov 22, 2017 at 15:49
  • 4
    See also [[ -o pipefail ]] to test if the ($SHELLOPTS) pipefail option is set. Also works in ksh and zsh Nov 22, 2017 at 15:50
  • 3
    shopt -q globstar to query the state of a shopt/$BASHOPTS option Nov 22, 2017 at 15:57
3

If you want to set a shell option inside a function, but do not want to affect the caller, you do it two different ways:

Option 1: subshell

Put the function inside a subshell (notice the parenthesis surrounding the function's statements instead of curly brackets):

function foo() (
  set -o pipefail
  echo inside foo
  shopt -o pipefail
  # ...
)

Then, the caller can have pipefail unset and is not affected:

$ shopt -o pipefail
pipefail        off
$ foo
inside foo
pipefail        on
$ shopt -o pipefail
pipefail        off

or

Option 2: test-and-reset

You could test-and-reset the option as needed (notice the curly braces around the function statements -- no subshell):

function foo() {
  shopt -q -o pipefail
  local resetpipefail=$?
  set -o pipefail
  echo inside foo
  shopt -o pipefail
  # ...
  # only reset pipefail if needed
  [[ $resetpipefail -eq 1 ]] && set +o pipefail  
}

$ shopt -o pipefail
pipefail        off
$ foo
inside foo
pipefail        on
$ shopt -o pipefail
pipefail        off

Hat tip to cuonglm for their shopt -q answer.

2

The following uses bash's -o test to test whether the pipefail shell option is set or not (this works in [[ ... ]] and with the test built-in command, but not with [ ... ]). If the option isn't set, it sets it and installs a trap that will unset it when the function returns:

foo () {
    if [[ ! -o pipefail ]]; then
        set -o pipefail
        trap 'set +o pipefail' RETURN
    fi

    # rest of function body
}

Code with some debugging included:

foo () {
    if [[ ! -o pipefail ]]; then
        set -o pipefail
        trap 'set +o pipefail' RETURN
    fi

    # rest of function body
    if [[ -o pipefail ]]; then
        echo 'pipefail is set (in function)'
    else
        echo 'pipefail is not set (in function)'
    fi
}

if [[ -o pipefail ]]; then
    echo 'pipefail is set'
else
    echo 'pipefail is not set'
fi

foo

if [[ -o pipefail ]]; then
    echo 'pipefail is set'
else
    echo 'pipefail is not set'
fi

Running it:

$ bash script.sh
pipefail is not set
pipefail is set (in function)
pipefail is not set

$ bash -o pipefail script.sh
pipefail is set
pipefail is set (in function)
pipefail is set
2

Use a RETURN trap

Commands specified with an RETURN trap are executed before execution resumes after a shell function or a shell script executed with . or source 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

foo() {
  trap "$(shopt -p -o pipefail)" RETURN
  set -o pipefail
  echo inside foo
  shopt -o pipefail
  # ...
}

Test

$ shopt -o pipefail
pipefail        off
$ foo
inside foo
pipefail        on
$ shopt -o pipefail
pipefail        off

The following function template handles the general case of ensuring that both set and shopt options are restored to their original values when a function returns:

foo() {
  trap "$(shopt -p -o; shopt -p)" RETURN # ①
  # ...
}

shopt -p -o can be simplified to the standard equivalent set +o.

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .