Portable shell scripts run wherever sh runs — Alpine containers with busybox ash, BSD machines, minimal rescue environments, and the CI runner that has no bash. The discipline is a subset of bash: POSIX sh plus the restraint to stay inside it.
The shebang decides the dialect #
#!/bin/sh # POSIX: runs on every Unix-like system
#!/usr/bin/env bash # bash: arrays, [[ ]], local niceties
#!/bin/sh on Debian-family systems is dash — noticeably stricter and faster than bash, and an honest test bench: if your script runs under dash, it is portable. The first debugging question for "works here, fails there" is which shell is actually running it — readlink /bin/sh answers.
What POSIX sh does not have #
| Bash feature | POSIX substitute |
|---|---|
[[ ... ]] | [ ... ] with careful quoting |
| Arrays | Positional params, or newline-separated strings |
local | Supported by most shells; POSIX-pedantic scripts avoid it |
${var,,} case ops | tr 'A-Z' 'a-z' |
${var//a/b} | sed or loop |
read -a, here-strings | while read -r, heredocs |
echo -e / \\u escapes | printf — the portable print |
The substitutions are all covered elsewhere in this series — tests, strings, printing. printf deserves special mention: printf '%s\n' "$x" behaves identically everywhere; echo's flags do not.
A worked conversion #
#!/usr/bin/env bash
files=(*.log)
for f in "${files[@]}"; do
[[ -e $f ]] || continue
gzip "${f,,}"
done
#!/bin/sh
for f in ./*.log; do
[ -e "$f" ] || continue
gzip "$(printf '%s' "$f" | tr '[:upper:]' '[:lower:]')"
done
Same structure, more typing — which is exactly the trade to make deliberately: write bash when you control the environment, POSIX when you do not.
The workflow that keeps it honest #
- Shebang
#!/bin/shfrom the start — retrofitting portability is harder. shellcheck -s sh script.sh— the-s shflag checks POSIX compliance specifically.- Test under dash:
dash script.sh— two minutes that find every bashism. - Avoid
echowith backslashes; avoidlocalin functions if you want full pedantry (most pragmatic scripts keep it).
The strictness habits carry over directly: quoting and exit codes matter more, not less, without bash's safety nets. The scripting series continues.
