One thing that many macOS users don’t know is that they are using a completely outdated version of the Bash shell. However, it is highly recommended to use a newer version of Bash on macOS, because it enables you to use useful new features. This article explains how to do this.
To see how outdated the Bash version included in macOS is, execute the following command
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.As you can see, this is GNU Bash version 3.2, which dates from 2007! This version of Bash is included in all versions of macOS, even the newest one.
The reason that Apple includes such an old version of Bash in its operating system has to do with licensing. Since version 4.0 (successor of 3.2), Bash uses the GNU General Public License v3 (GPLv3), which Apple does not (want to) support. You can find some discussions about this here and here. Version 3.2 of GNU Bash is the last version with GPLv2, which Apple accepts, and so it sticks with it.
This means that the entire world (e.g. Linux) proceeds with new versions of Bash, whereas macOS users are stuck with an old version from a decade ago. At the time of this writing, the newest version of GNU Bash is 5.0 (see here), which has been released in January 2019. In this article, I give instructions to upgrade the default shell of your system to the newest version of Bash.
But why even bother with getting a newer version if Bash 3.2 works fine? The main reason, for me personally, is programmable completion. This is a feature that allows command-specific auto-completions in Bash. You probably use auto-completion for completing commands, filenames, and variables by starting to type and then hitting Tab to auto-complete the current word (or hitting Tab two times to get a list of all possible completions, if there is more than one). This is the default completion of Bash.
However, programmable completion goes much beyond that, because it allows command-specific completions that can depend on the context. Imagine, for example, typing cmd -[tab][tab], and then seeing a list of all options that are applicable to this command. Or typing cmd host rm [tab][tab] and then seeing a list of all the “hosts” that are specified in some configuration file. Programmable completion allows to do that.
Programmable completion logic is defined (by the creators of commands) in completion specifications, typically in the form of completion scripts. These completion scripts have to be sourced in your shell to enable the completion functionality for a command.
The problem is that the programmable completion features of Bash have been extended since version 3.2, and most completion scripts use these new features. This means that these completion scripts don’t work on Bash 3.2, which means that you miss out from the completion functionalities of many commands, if you keep using the default macOS shell.
By upgrading to a newer version of Bash, you become able to use these completion scripts, which can be extremely useful. I wrote an entire article called Programmable Completion for Bash on macOS, which explains all you need to know to take full advantage of programmable completion on macOS after upgrading to a newer Bash version.
To upgrade the default shell of your macOS system to the latest version of Bash, you have to do three things:
- Install the latest version of Bash
- “Whitelist” new Bash as a login shell
- Set new Bash as the default shell
Each step is extremely easy, as explained in the following.
Note: the following instructions don’t change the old version of Bash, but rather install a new version and set it as the default. The two versions will exist side by side on your system, but you can just ignore the old version from there on.
I recommend to use Homebrew to install the latest version of Bash:
brew install bashThat’s it already!
To verify the installation, you can check that you now have two versions of Bash on your system:
$ which -a bash
/usr/local/bin/bash
/bin/bashThe first one is the new version, and the second one is the old version:
$ /usr/local/bin/bash --version
GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.
Since the directory of the new version (/usr/local/bin) comes by default before the directory of the old version (/bin) in the PATH variable, the version used when you just type bash is the new one:
$ bash --version
GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)So far, so good. Now you have make this version the default.
UNIX includes a security feature that restricts the shells that can be used as login shells (i.e. the shell used after logging in to the system) to a list of “trusted” shells. These shells are listed in the /etc/shells file.
Since you want to use the newly installed Bash shell as the default shell, it must be able to act as a login shell. That means, you have to add it to the /etc/shells file. You can edit this file as the root user, and add the /usr/local/bin/bash shell to its content, so that the file looks something like this:
$ sudo vim /etc/shells
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/local/bin/bashThat’s it for this step!
At this point, if you opened a new terminal window, you would still be using Bash 3.2. This is because /bin/bash is still set as the default shell. To change this to your new shell, execute the following command:
$ chsh -s /usr/local/bin/bashThat’s it! The default shell for your current user is now set to the new version of Bash. If you close and reopen the terminal window, you should now be using the new version already. You can verify this as follows:
$ echo $BASH_VERSION
5.0.0(1)-release
Thechsh command changes the default shell only for the user who executes the command. If you want to change the default shell for other users too, you can repeat this command by assuming another user’s identity (e.g. with su). Most importantly perhaps you might want to change the default shell for the root user, which you can do as follows:
$ sudo chsh -s /usr/local/bin/bash
In this way, if you use sudo su to open a shell as the root user, it will also use the new Bash version.
As mentioned, you didn’t change the default version of Bash, but rather installed a new version and set it as the default. The two versions of Bash exist side by side on your system:
/bin/bash: old version
/usr/local/bin/bash: new version
In shell scripts, you often have a shebang line like in the following script:
#!/bin/bash
echo $BASH_VERSION
It is important to note that this shebang line explicitly refers to the old version of Bash (since it specifies /bin/bash). This means that if you run this script, it will be interpreted by the old version of Bash (you can see it in the output of the script, which will be something like 3.2.57(1)-release).
For most cases, this is probably not a problem. But if you wanted your script to be explicitly interpreted by the new version of Bash, you could change the shebang line like this:
#!/usr/local/bin/bash
echo $BASH_VERSION
Now the output will be something like 5.0.0(1)-release. However, note that this solution is non-portable, which means, it is likely to not work on other systems. This is because, other systems are likely to not have a shell located at /usr/local/bin/bash (whereas /bin/bash is almost a standard).
To combine the best of both worlds, you can use the following shebang line:
#!/usr/bin/env bash
echo $BASH_VERSION
This is a recommended format for a shebang line. It works by inspecting the PATH and using the first encountered bash executable as the interpreter for the script. If the directory of the new version is located before the directory of the old version in the PATH (which is the default), then the new version will be used, and the output of the script will be something like 5.0.0(1)-release.
Instead of dealing with both versions of Bash, couldn’t you just delete the old version and put the new version in its place? For example, by creating a symlink in /bin/bash that points to the new version, like the following?
$ sudo rm /bin/bash
$ sudo ln -s /usr/local/bin/bash /bin/bash
In this way, even scripts with a #!/bin/bash would be interpreted by the new version of Bash, so why not doing this?
You can do this, but you have to circumvent a macOS security feature called System Integrity Protection (SIP) (Wikipedia). This feature forbids write access to certain directories for even the root user (that’s why it’s also called “rootless”). These directories are listed here and include /bin. That means that even as the root user, you cannot execute the above commands, because you are not allowed to delete anything from or create any files in /bin.
The way around this is to disable SIP, make the changes in /bin, and then enable SIP again. Enabling and disabling SIP can be done according to the instructions here. It requires you to boot your machine into recovery mode and then use the csrutil disable and csrutil enable commands. It’s up to you if you want to go through this hassle to completely replace the old Bash version, or if you’re content with the two Bash versions living side by side.
References: