10

I work in a relatively heterogeneous environment where I may be running different versions of Bash on different HPC nodes, VMs, or my personal workstation. Because I put my login scripts in a Git repo, I would like use the same(ish) .bashrc across the board, without a lot of "if this host, then..."-type messiness.

I like the default behavior of Bash ≤ 4.1 that expands cd $SOMEPATH into cd /the/actual/path when pressing the Tab key. In Bash 4.2 and above, you would need to shopt -s direxpand to re-enable this behavior, and that didn't become available until 4.2.29. This is just one example, though; another, possibly related shopt option, complete_fullquote (though I don't know exactly what it does) may have also changed default behavior at v4.2.

However, direxpand is not recognized by earlier versions of Bash, and if I try to shopt -s direxpand in my .bashrc, that results in an error message being printed to the console every time I log in to a node with an older Bash:

-bash: shopt: direxpand: invalid shell option name

What I'd like to do is wrap a conditional around shop -s direxpand to enable that option on Bash > 4.1 in a robust way, without chafing the older versions of Bash (i.e., not just redirecting the error output to /dev/null).

Kevin E
  • 468

3 Answers3

18

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.

  • That is not where my brain would've gone, but I like the compgen proposal. That's varsity level stuff right there! Avoiding redirection to /dev/null is just a personal preference. I like to ask for permission instead of forgiveness, if that makes sense? :) – Kevin E Feb 05 '19 at 18:09
  • +1 for a totally unanticipated schooling in Bash programmable completion, which forced me to go to the manual to decipher what compgen -A shopt -X ... even meant. – Kevin E Feb 05 '19 at 18:21
  • 4
    @TheDudeAbides I read about using compgen this way on [unix.se], I don't know who first proposed it. (I stopped using bash as my main shell before it had programmable completion.) In programming it's usually a bad idea to ask for permission because there's a risk that the permission check will not match what you're actually doing, either because of a coding error (where you aren't quite checking what you think you're checking) or because what you checked changed before you used it. – Gilles 'SO- stop being evil' Feb 05 '19 at 18:29
14

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

shopt | grep -q '^direxpand\b' && shopt -s direxpand
Kevin E
  • 468
  • 4
    Better make that grep -q '^direxpand\b' in case some future version or fork of bash has an option that contains this as a substring and removes direxpand. Unlikely in this specific case, but it doesn't cost much to be robust. – Gilles 'SO- stop being evil' Feb 05 '19 at 17:45
  • Thanks Luciano. I'd intended to answer my own question, but I"ll accept your answer after my edits go through peer review. Maybe you can approve them yourself? – Kevin E Feb 05 '19 at 18:05
  • 5
    Bash allows to query specific shell options, so one can use [ -z "$(shopt -po direxpand 2>&-)" ] || shopt -s direxpand. No more regex issues! :-) – David Foerster Feb 05 '19 at 21:47
  • @DavidFoerster I'd turn the logic around: [ -n "blah" ] && shopt blah The way you phrase it, you're saying "if direxpand is unsupported, then don't do this thing". – Rich Feb 06 '19 at 16:47
  • 1
    @Rich: Most of my shell scripts include set -e at the top, so I tend to use short-cut logic this way around. – David Foerster Feb 07 '19 at 18:07
  • @DavidFoerster Good eye. I had never scrutinized the output of help shopt that closely before, but I think you need to leave out the -o which restricts the output to options "defined for use with set -o"—which direxpand is not. A shame the return value of shopt is non-zero for both unset and invalid options, otherwise -q (suppress output) could've also come in handy. – Kevin E Aug 04 '20 at 15:19
6

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?

Kevin E
  • 468
  • You might also be able to use something like if [[ $BASH_VERSION > 4.3 ]]; (which matches 4.3.0, 5.0 etc., but also 4.3.0-alpha. I don't know if the later fact matters.) – ilkkachu Aug 23 '19 at 16:14
  • Hi @ilkkachu. Thanks for your edit to cover Bash v5.x. The direxpand option is indeed available for Bash 4.2, though; I've verified this with a Docker image at v4.2.53 by running docker run --rm bash:4.2 bash -c shopt | grep direxpand (and, for good measure, that it is indeed not available at v4.1.17 by running docker run --rm bash:4.1 bash -c shopt | grep direxpand). – Kevin E Aug 23 '19 at 16:29
  • ah ok, I tested 4.2.0 and stumbled upon the fact that it didn't work there. The changelog also mentions it added in bash-4.3-alpha. I suppose then that one would need to check ${BASH_VERSINFO[2]} to be exact about it, but I don't know which point release added it... – ilkkachu Aug 23 '19 at 17:24
  • I think we've basically proved the point Gilles made above; that it is, in fact, better to just try to enable the shell option, and then deal with the error (or suppress it) if it isn't supported. – Kevin E Aug 24 '19 at 16:08