POSIX sh, bashisms and when to stop

Bash · Expert · 12 min

Scripts run in Alpine containers, on macOS and under dash; knowing the boundaries prevents 'works on my machine'.

/bin/sh is not bash everywhere: it is dash on Debian and Ubuntu, BusyBox ash in Alpine images and bash in POSIX mode on RHEL. macOS ships an old bash 3.2 (zsh is its default shell), which lacks associative arrays, mapfile, ${var,,} and wait -n. A script with #!/bin/sh must stick to POSIX shell.

Common bashisms and their POSIX replacements:

  • [[ ]] becomes [ ] with careful quoting, or case for patterns,
  • arrays become the single list "$@" (via set --) or newline-separated strings,
  • (( )) becomes $(( )) inside [ ]; function f becomes f(),
  • &> becomes > f 2>&1; source becomes .; $'\t' becomes printf,
  • <( ) has no equivalent; use temporary files or FIFOs.

Check portability with shellcheck --shell=sh, checkbashisms, or simply run the script with dash. If you need bash, say so with #!/usr/bin/env bash and, if necessary, guard on BASH_VERSINFO.

Know when to stop. Shell is excellent for gluing commands together, but once a script passes a couple of hundred lines, manipulates structured data (JSON, CSV with quoting), needs real error handling, floats or unit tests, a language like Python or Go will be shorter and safer. Keep the shell part a thin wrapper.

Example

# Bash only                          # POSIX sh equivalent
# [[ $x == a* ]] && echo yes        # case $x in a*) echo yes ;; esac
# (( n++ ))                          # n=$((n + 1))
# lower=${s,,}                       # lower=$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]')
# cmd &> out.log                     # cmd > out.log 2>&1
# source ./lib.sh                    # . ./lib.sh
# arr=(a b c); "${arr[@]}"           # set -- a b c; "$@"
if (( BASH_VERSINFO[0] < 4 )); then  # macOS /bin/bash is 3.2
  echo "this script needs bash >= 4" >&2
  exit 1
fi
printf 'x=1; [[ $x == 1 ]] && echo ok\n' > /tmp/t.sh
bash /tmp/t.sh                       # ok
dash /tmp/t.sh 2>&1 || true          # /tmp/t.sh: 1: [[: not found

Remember

  • /bin/sh may be dash, ash or bash in POSIX mode; macOS bash is 3.2
  • Test portability with shellcheck --shell=sh or by running dash
  • Move to Python or Go when logic, data or size outgrow the shell

Watch out for

  • Writing bash features in a script whose shebang is #!/bin/sh: it works on RHEL and fails on Ubuntu.

Check yourself

  1. A #!/bin/sh script using [[ ]] works on Fedora but fails on Ubuntu. Why?
  2. Which feature is NOT available in macOS's default /bin/bash 3.2?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.gnu.org/software/bash/manual/bash.html#Bash-POSIX-Mode