Stage 2 · Tools
Scripting Patterns
Portable Scripting (POSIX)
Write scripts that run on bash, dash, zsh, and busybox sh without modification.
POSIX Overview
POSIX (Portable Operating System Interface) defines a standard for Unix shells. Scripts written to POSIX standard run on any POSIX-compliant shell — bash, dash, zsh, busybox sh, ksh, and more.
Not every environment has bash. Docker containers often use Alpine (busybox sh). Embedded systems may use ash or dash. Writing portable scripts ensures they work everywhere.
Bash vs sh Differences
| Feature | POSIX sh | Bash |
|---|---|---|
| Arrays | Not supported | Supported |
| [[ ]] tests | Not supported | Supported |
| (( )) arithmetic | Not supported | Supported |
| Process substitution | Not supported | <() and >() |
| function keyword | Not supported | Supported |
| Local variables | Not in functions | local keyword |
| Here strings | Not supported | <<< |
# NOT POSIX — avoid these
declare -A HASH # Associative arrays
[[ "$var" =~ regex ]] # Regex matching
(( count++ )) # Arithmetic evaluation
echo "{array[@]}" # Array expansion
<(command) # Process substitution
function foo { } # function keyword
read -p "prompt: " var # -p flag
local var=value # local in functions (debatable)
# POSIX alternatives
# Use case for pattern matching instead of [[ =~ ]]
case "$var" in
[0-9]*) echo "starts with number" ;;
esac
# Use expr or $(( )) for arithmetic (limited)
count=$(($count + 1))
# Use while read for line-by-line processing
while IFS= read -r line; do
echo "$line"
done < file.txtPOSIX sh lacks arrays, [[ ]], (( )), and other Bash features. Use case for pattern matching, $(( )) for arithmetic (which is POSIX), and while read for line processing.
Portable Alternatives
#!/bin/sh
# Use #!/bin/sh for POSIX scripts
# String contains (POSIX)
contains() {
case "$1" in
*"$2"*) return 0 ;;
*) return 1 ;;
esac
}
# Array simulation with space-separated strings
ITEMS="one two three"
for item in $ITEMS; do
echo "$item"
done
# Find executable (portable)
find_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "$1 not found" >&2
return 1
}
}
# Temporary file (POSIX)
TMPFILE=$(mktemp 2>/dev/null || echo "/tmp/tmpfile.$$")
trap 'rm -f "$TMPFILE"' EXIT
# Portable date parsing (limited)
date '+%Y-%m-%d'
# Check if string is numeric (POSIX)
is_numeric() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}These patterns work on any POSIX shell. The contains() function replaces [[ $var == *pattern* ]]. Space-separated strings simulate arrays. command -v replaces type -p.
Cross-Platform Concerns
#!/usr/bin/env bash
# Date command differs between macOS and Linux
get_date() {
if date -v 1d >/dev/null 2>&1; then
# macOS
date -v-30d '+%Y-%m-%d'
else
# Linux
date -d '30 days ago' '+%Y-%m-%d'
fi
}
# sed -i differs between macOS and Linux
sed_inplace() {
if sed --version >/dev/null 2>&1; then
# GNU sed (Linux)
sed -i "$@"
else
# BSD sed (macOS)
sed -i '' "$@"
fi
}
# Readlink differs between macOS and Linux
realpath() {
if command -v grealpath >/dev/null 2>&1; then
grealpath "$@"
elif readlink -f "$1" >/dev/null 2>&1; then
readlink -f "$@"
else
# Fallback: no realpath
cd "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")"
fi
}
# mktemp differs between macOS and Linux
safe_mktemp() {
mktemp 2>/dev/null || mktemp -t tmp
}macOS uses BSD tools; Linux uses GNU tools. The key differences: date, sed -i, readlink, and mktemp. Use command detection to handle both platforms.
Testing Portability
#!/usr/bin/env bash
# Check with checkbashisms (Debian tool)
# install: sudo apt install devscripts
checkbashisms myscript.sh
# Run with dash (POSIX sh)
dash myscript.sh
# Run with busybox sh (in Docker)
docker run --rm -v "$PWD:/scripts" alpine sh /scripts/myscript.sh
# ShellCheck for portability
shellcheck -s sh myscript.sh # Check as POSIX sh
shellcheck -s bash myscript.sh # Check as bash
# Test multiple shells
for shell in bash dash zsh sh; do
echo "Testing with $shell..."
$shell myscript.sh || echo "FAILED with $shell"
donecheckbashisms identifies bash-isms in scripts marked with #!/bin/sh. ShellCheck -s sh checks for POSIX compliance. Testing in Docker with Alpine catches busybox issues.
If you control the environment and know bash is available, use bash features. If your script runs on diverse systems (containers, embedded, macOS), write POSIX sh. The shebang declares your intent.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.