How can I prevent unsupported 'shopt' options from causing errors in my .bashrc?

I don't see what's wrong with redirecting errors to /dev/null. If you want your code to be robust to set -e, use the common idiom … || true:

shopt -s direxpand 2>/dev/null || true

If you want to run some fallback code if the option does not exist, use the return status of shopt:

if shopt -s direxpand 2>/dev/null; then
  … # the direxpand option exists
else
  … # the direxpand option does not exist
fi

But if you really dislike redirecting the error away, you can use the completion mechanism to perform introspection. This assumes that you don't have antiquated machines with bash ≤ 2.03 that didn't have programmable completion.

shopt_exists () {
  compgen -A shopt -X \!"$1" "$1" >/dev/null
}
if shopt_exists direxpand; then
  shopt -s direxpand
fi

This method avoids forking, which is slow on some environments such as Cygwin. So does the straightforward 2>/dev/null, I don't think you can beat that on performance.


Check if direxpand is present in the output of shopt and enable it if it is:

shopt | grep -q '^direxpand\b' && shopt -s direxpand

When you know for sure that a specific shopt option is available at a certain major/minor/patch release of Bash, you can inspect the $BASH_VERSION variable or the elements of the $BASH_VERSINFO[] array in order to enable it conditionally.

Here's a test for Bash 4.2.29 or greater, the version where direxpand was first introduced to the 4.2 series:

if [[ $BASH_VERSION == 4.2.* && ${BASH_VERSINFO[2]} -ge 29 ]] ||
   [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -ge 3 ]] ||
   [[ ${BASH_VERSINFO[0]} -ge 5 ]]; then
    shopt -s direxpand
fi

Edit: To be clear, this is a ridiculously over-engineered solution for simply ignoring an error message coming from your login scripts, but I did want to document it regardless, for my own edification.

Note the braces around ${BASH_VERSINFO[index]}, which are required, and the use of -eq and -gt, which do integer rather than (locale-dependent) lexical comparisons. If unquoted, the RHS of the == operator is treated as "extglob" patterns within Bash [[/]] conditionals, as noted here, which makes a more aesthetic "starts with" comparison than a regex would, IMO.

The $BASH_VERSINFO array contains all the information you'd see in the output of bash --version:

bash --version | head -1
# result:
# GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)

declare -p BASH_VERSINFO
# result:
# declare -ar BASH_VERSINFO='([0]="4" [1]="3" [2]="48" [3]="1" [4]="release" [5]="x86_64-pc-linux-gnu")'

When it isn't clear from the documentation for shopt at which Bash version(s) became supported or changed their behavior, the method proposed by Luciano is fine:

# note the '-q' so that the matched pattern isn't actually printed
shopt | grep -q direxpand && shopt -s direxpand

...as is the solution proposed by Gilles of just ignoring the error (shopt -s direxpand 2>/dev/null), and perhaps checking $? if absolutely necessary.

References: 1, 2, 3
Related reading: Set and Shopt - Why Two?