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