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, orcasefor patterns,- arrays become the single list
"$@"(viaset --) or newline-separated strings, (( ))becomes$(( ))inside[ ];function fbecomesf(),&>becomes> f 2>&1;sourcebecomes.;$'\t'becomesprintf,<( )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 foundRemember
- /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.