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.
$BASH_VERSINFOreturns just the major release number; much better than trying to parse
export BASH_UPGRADE_ATTEMPTED=1Note that the variable is exported. Exported variables survive "exec".
export PATH=/opt/local/bin:/usr/local/bin:"$PATH":/binWe 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" """$@"""
execThis 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.
--noprofileWe 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.