bash: Restart bash if old version detected

I write a lot of small bash scripts. Many of them have to run on MacOS as well as FreeBSD and Linux. Sadly MacOS comes with a bash 3.x which doesn't have many of the cooler features of bash 4.x.

Recently I wanted to use read's "-i" option, which doesn't exist in bash 3.x.

My Mac does have bash 4.x but it is in /opt/local/bin because I install it using MacPorts.

I didn't want to list anything but "#!/bin/bash" on the first line because the script has to work on other platforms and on other people's machines. "#!/opt/local/bin/bash" would have worked for me on my Mac but not on my Linux boxes, FreeBSD boxes, or friend's machines.

I finally came up with this solution. If the script detects it is running under an old version of bash it looks for a newer one and exec's itself with the new bash, reconstructing the command line options correctly so the script doesn't know it was restarted.

#!/bin/bash
# If old bash is detected. Exec under a newer version if possible.
if [[ $BASH_VERSINFO < 4 ]]; then
  if [[ $BASH_UPGRADE_ATTEMPTED != 1 ]]; then
    echo '[Older version of BASH detected.  Finding newer one.]'
    export BASH_UPGRADE_ATTEMPTED=1
    export PATH=/opt/local/bin:/usr/local/bin:"$PATH":/bin
    exec "$(which bash)" --noprofile "$0" """$@"""
  else
    echo '[Nothing newer found.  Gracefully degrading.]'
    export OLD_BASH=1
  fi
else
  echo '[New version of bash now running.]'
fi

# The rest of the script goes below.
# You can use "if [[ $OLD_BASH == 1]]" to
# to write code that will work with old
# bash versions.

Some explanations:

  • $BASH_VERSINFO returns just the major release number; much better than trying to parse $BASH_VERSION.
  • export BASH_UPGRADE_ATTEMPTED=1 Note that the variable is exported. Exported variables survive "exec".
  • export PATH=/opt/local/bin:/usr/local/bin:"$PATH":/bin We prepend a few places that the newer version of bash might be. We postpend /bin because if it isn't found anywhere else, we want the current bash to run. We know bash exists in /bin because of the first line of the script.
  • exec $(which bash) --noprofile "$0" """$@"""
    • exec This means "replace the running process with this command".
    • $(which bash) finds the first command called "bash" in the $PATH.
    • "$(which bash)" By the way... this is in quotes because $PATH might include spaces. In fact, any time we use a variable that may contain spaces we put quotes around it so the script can't be hijacked.
    • --noprofile We don't want bash to source .bashrc and other files.
    • "$0" The name of the script being run.
    • """$@""" The command line arguments will be inserted here with proper quoting so that if they include spaces or other special chars it will all still work.
  • You can comment out the "echo" commands if you don't want it to announce what it is doing. You'll also need to remove the last "else" since else clauses can't be empty.

Enjoy!

Posted by Tom Limoncelli in Technical Tips

No TrackBacks

TrackBack URL: http://everythingsysadmin.com/cgi-bin/mt-tb.cgi/1703

5 Comments | Leave a comment

Why not just use the hashbang #!/usr/bin/env bash and let the PATH sort it out? Then you can still add old version handling if desired but essentially provide one script for all environments set up optimally.

Presumably the bash in /opt/local/bin is already the default bash in your PATH, right? In that case, the following shebang will probably work, and remain generally portable:

#!/usr/bin/env bash

Sorry to double-up on Oliver. I forgot to click 'submit' when I first wrote my comment. Now the page has refreshed and I see I'm a few hours late! :)

This sort of tip is in the "works for me, only" arena. It's disgusting and if I ever saw this in production code then I'd shoot the author (well, it'd fail my code review, anyway :-))

This would not work on freebsd because bash is default installed in /usr/local/bin.

Leave a comment