As you may know, macOS ships with an ancient version of the Bash shell interpreter under /bin/bash. On an up-to-date Catalina installation, we get:

$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
Copyright (C) 2007 Free Software Foundation, Inc.

which is… old. Bash 3.2.57 was released on November 7th, 2014—five years ago. The most recent version is 5.0 from the beginning of this year—so it’s not like nothing has changed since then.

But why? Why does macOS stick to such an ancient version? Well, the reason is simple: 3.2.57 is the last Bash release that was published under the GPL v2. All subsequent versions are GPL v3-licensed, which as you can imagine, companies do not like.

You may also have noticed that Zsh is the recommended default shell starting with Catalina. And you guessed right: Zsh is not GPL-licensed. Zsh is released under an MIT-like license, which is much more amenable to Apple. Which means Apple can ship a more modern shell interpreter and keep it up-to-date. And if you look further, you will also discover that Catalina ships with Dash, a standards-compliant, fast, and BSD-licensed shell interpreter first introduced by Debian to replace /bin/bash as the system-wide scripts interpreter.

At this point we can only conjecture, but it seems clear that Apple wants to remove the ancient /bin/bash from the system and will do so in two ways: first, making Zsh the interpreter for interactive sessions; and second, using Dash to run any system scripts. Which makes sense, because Bash 3.2.57 is a big liability.

Other than being old, Bash 3.2.57 lacks new features and contains bugs that cannot be dealt with. My pet peeve is set -u being unusable: if you try to reference ${@} or ${*} in a function, and the function hasn’t been given any arguments, the program will fail. Which is unfortunate because a lot of scripts enable the shell’s strict mode without realizing that they’ll break unexpectedly for perfectly-valid code.

Consider this benign-looking script that prints its arguments one per line:

set -u

main() {
    for arg in "${@}"; do
        echo "Argument: ${arg}"
    done
}

main "${@}"

If we try to run it with Dash, we get:

$ /bin/dash print-args.sh 1 2 3
Argument: 1
Argument: 2
Argument: 3
$ /bin/dash print-args.sh

And with the ancient Bash:

$ /bin/bash print-args.sh 1 2 3
Argument: 1
Argument: 2
Argument: 3
$ /bin/bash print-args.sh
print-args.sh: line 9: @: unbound variable

Oops. Note how Bash failed miserably when no arguments were given… and there is nothing we can do to protect against that—not even checking ${#} upfront. See, if we modify the program to be like this:

set -u

main() {
    if [ "${#}" -gt 0 ]; then
        for arg in "${@}"; do
            echo "Argument: ${arg}"
        done
    fi
}

main "${@}"

we will still get the same error:

$ /bin/bash print-args.sh
print-args.sh: line 11: @: unbound variable

We lose. So either we have to avoid using set -u to avoid this potential issue throughout our scripts, or we need to be very aware of it and surround the offending code with set +u and set -u pairs to temporarily disable this behavior… which is very fragile. And ugly.

That’s just one of the many examples of subtle issues you may encounter. (I’ve hit others in the past but I cannot remember them right now.) You can skim through the CHANGES list for more details though.

At this point, I’m starting to feel that /bin/bash on macOS is the equivalent of what /bin/sh from Solaris was, primarily because /bin/sh is /bin/bash on this platform. Sure, Bash 3.2.57 is not that broken, but it’s still broken enough to be problematic when writing shell scripts that are, in theory, perfectly valid. And unless Apple decides to yank Bash from the default installation completely—something that seems very hard to do—we’ll be stuck with having to support this version for a long, long time.