keepcalculator

06 Numbers and Powers

Why Bash Scripts Behave Differently on Macs

Card No. 06·40601 · Updated · Standard formula, shown below

Bash scripts behave differently across Macs and other Unix-like systems primarily because macOS ships with an older version of Bash (3.2) and a different set of command-line utilities, so syntax and features that work on Linux may fail or produce different output on a Mac. A benchmark must prove that a script is portable by testing it on the actual target systems, verifying both the shell version and the behavior of every external command it uses.

Background: The macOS Bash Version Gap

macOS has included Bash since the early 2000s, but Apple has never updated it beyond version 3.2. The reason is licensing: Bash 4+ is released under the GPLv3, while Bash 3.2 is under GPLv2. Apple chose to keep the older license to avoid the obligations of GPLv3. As a result, every Mac that has not had a custom Bash installed runs Bash 3.2, which lacks many features that are standard in Bash 4 and later on Linux distributions.

This is not a trivial difference. Bash 4 introduced associative arrays, the ** globstar option, mapfile/readarray, and improved case and [[ ]] handling. A script that uses any of these will simply fail with a syntax error on a Mac’s default Bash.

How Bash Versions Change Script Behavior

Scripts can behave differently across Macs because of the shell version, but also because of the environment: the PATH variable, the availability of GNU vs. BSD utilities, and system-specific quirks. Here are the most common sources of divergence:

  • Associative arrays (Bash 4+): declare -A arr fails on Bash 3.2.
  • Globstar (**): not supported in Bash 3.2, so recursive globbing fails.
  • mapfile/readarray: not available in Bash 3.2.
  • [[ =~ ]] regex: works in both, but quoting behavior differs in some edge cases.
  • case with ;& and ;;&: not supported in Bash 3.2.

Even if your script avoids these features, the external commands it calls can behave differently. macOS uses BSD versions of sed, awk, grep, and find, which have different flags and behaviors than their GNU counterparts on Linux. For example, sed -i requires an extension on macOS (sed -i '') while GNU sed allows -i without it.

What a Benchmark Must Prove

A benchmark for cross-platform Bash scripts must prove that the script runs correctly on all target systems, not just that it produces the same output on one system. Specifically, it must verify:

  1. Shell version compatibility: The script must run without syntax errors on the lowest Bash version you claim to support (e.g., 3.2).
  2. External command behavior: Every sed, awk, grep, find, and other utility must be tested with the flags your script uses, on both GNU and BSD versions.
  3. Environment independence: The script should not rely on a specific PATH or on aliases that might not exist.
  4. Error handling: The script should fail gracefully when a feature is missing, rather than crashing with a cryptic error.

A benchmark should run the script on a clean macOS VM and a clean Linux VM, with the same inputs, and compare outputs and exit codes. It should also test with bash -n (syntax check) and bash -x (trace) to catch issues.

Common Problems and How to Solve Them

Here are the most frequent issues you’ll encounter when moving a Bash script from Linux to macOS, and how to handle them:

  • sed -i syntax: On macOS, use sed -i '' 's/foo/bar/' file; on Linux, sed -i 's/foo/bar/' file. To be portable, avoid -i or use a temporary file.
  • grep -P (Perl regex): Not available on macOS by default. Use grep -E or awk instead.
  • readlink -f: Does not work on macOS (BSD readlink lacks -f). Use realpath (if installed) or a portable function.
  • date -d: GNU date supports date -d 'yesterday', but macOS date uses date -v-1d. Use date -j -f for parsing.
  • find -mtime and -exec syntax: BSD find requires find . -mtime -7 -exec ... \; (with a space before \;), while GNU find allows find . -mtime -7 -exec ... \; as well, but some flags differ.

Portability Strategies

To write Bash scripts that work on both Macs and Linux, you can:

  • Target Bash 3.2: Write your script using only features available in Bash 3.2. This is the safest approach if you cannot control the environment.
  • Detect the OS: Use uname to branch on OS type and use the appropriate commands for each.
  • Use POSIX sh: If you don’t need Bash-specific features, write in POSIX sh, which is more portable (though macOS’s /bin/sh is actually Bash 3.2 in POSIX mode).
  • Install a modern Bash: On macOS, you can install Bash 5 via Homebrew, but that requires the user to do it, so it’s not a default solution.

For example, a portable way to get the absolute path of a script is:

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

This works on both BSD and GNU systems because it uses only POSIX commands.

Comparing Bash on macOS vs. Linux

Beyond the shell version, the userland tools differ significantly. Here’s a quick comparison of common commands:

  • sed: BSD sed doesn’t support -i without an argument, and its regex syntax is slightly different (e.g., \+ vs. + in BRE).
  • awk: Both use awk, but macOS’s awk is based on the original Bell Labs awk, not gawk. It lacks some gawk extensions like gensub.
  • grep: BSD grep doesn’t support -P (Perl regex) and has different behavior for -o with empty matches.
  • find: BSD find has different -exec syntax (requires a space before \;) and lacks some GNU extensions like -printf.
  • ls: BSD ls doesn’t support --color; use ls -G on macOS.

Costs and Alternatives

There is no monetary cost to making scripts portable, but there is a development cost: you must test on multiple platforms and possibly rewrite parts of your script. Alternatives to Bash scripting include:

  • Python: Python is preinstalled on macOS and most Linux distributions, and its standard library provides consistent behavior across platforms.
  • Perl: Also preinstalled on macOS, but its behavior can vary slightly.
  • Zsh: macOS uses Zsh as the default shell since Catalina, but Zsh has its own compatibility issues.
  • POSIX sh: If you stick to POSIX, you avoid most Bash-specific issues.

Frequently Asked Questions

Why does macOS still use Bash 3.2?

Apple chose to keep Bash 3.2 because it is licensed under GPLv2, while Bash 4+ is GPLv3. Apple avoids GPLv3 due to its stricter copyleft terms, particularly regarding patent grants and anti-tivoization clauses.

Can I update Bash on my Mac?

Yes, you can install a newer Bash via Homebrew (brew install bash) or MacPorts, but it will not be the default shell. You can change your default shell with chsh, but the system’s /bin/bash will remain 3.2 for scripts that use it.

What is the safest way to write a portable script?

Write your script in POSIX sh, or if you must use Bash, stick to Bash 3.2 features and avoid GNU-specific command flags. Test on both macOS and Linux using virtual machines or containers.

How do I check my Bash version?

Run bash --version or echo $BASH_VERSION. On macOS, this will typically show 3.2.57.

Are there any tools that automatically test Bash portability?

Yes, you can use shellcheck to lint your script for common issues, and you can run your script under both Bash 3.2 and Bash 5 using Docker or VMs to catch compatibility issues.

Does using #!/bin/bash vs #!/usr/bin/env bash matter?

Yes. #!/bin/bash uses the system’s default Bash (3.2 on macOS). #!/usr/bin/env bash uses the first bash in your PATH, which might be a newer version if you’ve installed one. For portability, decide which behavior you want and document it.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →