Bash compatibility#
Short version: bash 3.2 is the floor, not the target. Everything here runs on
bash 4 and 5, and ./dev prefers them.
The library is written so that stock macOS can use it without installing anything. That is a minimum, not a ceiling - nothing is held back to 3.2-era behaviour on a machine that has something newer.
Which bash actually runs#
| Situation | What runs |
|---|---|
| Linux, most containers | bash 5.x - whatever /usr/bin/env bash finds |
| macOS with Homebrew bash | bash 5.x from /opt/homebrew/bin or /usr/local/bin |
| macOS with nothing installed | bash 3.2, /bin/bash |
| Anything older than 3.2 | refused at load, with a message |
./dev resolves this deliberately. It walks these candidates:
and takes the first one that is bash 4 or newer; only if none of them is
does it fall back to the first that is at least 3.2. So a Mac whose $BASH is
the stock 3.2 but which has Homebrew bash installed gets the Homebrew one; a
flat first-match order would not do that. /bin/bash is last. That ordering exists so a Mac with a modern bash
installed actually uses it: a bare exec bash would resolve from PATH and
discard the script's own shebang, which on macOS lands back on 3.2.
helpers.sh refuses anything below 3.2 outright, and exports
SHLIB_BASH_LEGACY=1 on bash 3.x, 0 on 4.0 and newer. On 3.x it prints a
one-time note on stderr, and only to a terminal, naming the fix for that
host. Silence it with SHLIB_NO_BASH_ADVISORY=1.
Why 3.2 is the floor#
macOS ships bash 3.2 as /bin/bash and always will: bash 4 moved to GPLv3,
which Apple does not ship. That is not going to change, so a shell library that
requires bash 4 is a shell library that requires every Mac user to install
something before anything works.
This was not true for the first twenty-three releases, which is why there are now two gates rather than a convention:
make test-bash32 # the whole suite under a real bash 3.2, in Docker
bash tests/portability_test.sh # static scan for bash-4-only and GNU-only constructs
CI runs the suite on macOS under /bin/bash for every change touching shell
code. make test-bash32 needs no Mac - it runs the same suite in the
bash:3.2 image.
What that costs, in practice#
These are bash 4+ features, and they are not used in lib/:
| Feature | Use instead |
|---|---|
mapfile / readarray |
while IFS= read -r line; do arr+=("$line"); done < <(...) |
Associative arrays (declare -A) |
a newline-delimited string, or parallel arrays |
Namerefs (declare -n) |
print a value and capture it, or pass a name and eval carefully |
${var^^} / ${var,,} |
tr '[:lower:]' '[:upper:]' |
shopt -s globstar (**) |
find |
&>> |
>>file 2>&1 (not currently banned by the scanner - see below) |
The same discipline rules out GNU-only tool flags, because macOS ships BSD
versions: no sed -i/sed -r, no readlink -f, no realpath, no grep -P,
no date -d, no find -printf, no xargs -r, no base64 -w, no
md5sum/sha256sum, and no \s or \b in a grep or sed pattern.
tests/portability_test.sh enforces the GNU-only tool flags above, and every
bash-4 construct in the previous table except &>>, statically, over tracked
and untracked files - so a new script is checked before it is committed.
&>> is unused today and has no ban rule; do not rely on the scanner for it.
When a function genuinely needs bash 4#
Some things cannot be done on 3.2 at all. The clearest case is a function that takes an associative array from the caller: there is no way to receive one on 3.2, so there is nothing to degrade to.
Three functions are in that position - select_distro,
select_multiple_distros and download_iso. They do not silently misbehave;
they call require_bash4 and fail with a message naming the feature and, on
macOS, the remedy:
For anything conditional rather than absolute, use bash_at_least:
Both live in os, along with bash_major.
Adding a new module#
Write it 3.2-safe. It then runs everywhere, 4+ included, and both gates pass without you thinking about them again.
Reach for require_bash4 only when the feature is impossible on 3.2 rather
than merely more convenient with bash 4 - and when you do, add the file to the
allow-list in tests/portability_test.sh with a comment saying why, so the
exception is visible rather than assumed.
If you find yourself wanting an associative array, a newline-delimited string
usually does the job - scripts/lint_docs.sh uses one exactly where a map would
be the obvious bash 4 answer, and says so in a comment.