Shell Script Portability
Techniques for writing shell scripts that work across different platforms and environments.
Shebang Selection
Bash Scripts
#!/usr/bin/env bash
Most portable for bash scripts
Works on Linux, macOS, BSD
POSIX Shell Scripts
#!/bin/sh
For maximum portability
Use only POSIX features
Bash vs POSIX Differences
Arrays (Bash only)
Bash - arrays available
declare -a items=("one" "two" "three") for item in "${items[@]}"; do echo "$item" done
POSIX - use positional parameters or space-separated strings
set -- one two three for item in "$@"; do echo "$item" done
Test Syntax
Bash - extended test
if [[ "$var" == "value" ]]; then echo "match" fi
POSIX - basic test
if [ "$var" = "value" ]; then echo "match" fi
String Operations
Bash - regex matching
if [[ "$input" =~ ^[0-9]+$ ]]; then echo "numeric" fi
POSIX - use case or external tools
case "$input" in [!0-9]|'') echo "not numeric" ;; *) echo "numeric" ;; esac
Arithmetic
Bash - arithmetic expansion
(( count++ )) if (( count > 10 )); then echo "greater" fi
POSIX - expr or arithmetic expansion
count=$((count + 1)) if [ "$count" -gt 10 ]; then echo "greater" fi
Platform Differences
macOS vs Linux
Date command differences
GNU (Linux)
date -d "yesterday" +%Y-%m-%d
BSD (macOS)
date -v-1d +%Y-%m-%d
Portable approach
if date --version >/dev/null 2>&1; then # GNU date yesterday=$(date -d "yesterday" +%Y-%m-%d) else # BSD date yesterday=$(date -v-1d +%Y-%m-%d) fi
sed Differences
GNU sed - in-place edit
sed -i 's/old/new/g' file.txt
BSD sed - requires backup extension
sed -i '' 's/old/new/g' file.txt
Portable approach
sed 's/old/new/g' file.txt > file.txt.tmp && mv file.txt.tmp file.txt
Or use a function
sed_inplace() { if sed --version >/dev/null 2>&1; then sed -i "$@" else sed -i '' "$@" fi }
readlink Differences
GNU readlink
readlink -f /path/to/link
BSD/macOS - no -f option by default
Use greadlink from coreutils or:
resolve_path() { local path="$1" if command -v greadlink >/dev/null 2>&1; then greadlink -f "$path" elif command -v realpath >/dev/null 2>&1; then realpath "$path" else # Fallback cd "$(dirname "$path")" && pwd -P fi }
Detecting Environment
Operating System
detect_os() { case "$(uname -s)" in Linux*) echo "linux" ;; Darwin*) echo "macos" ;; MINGW*|CYGWIN*|MSYS*) echo "windows" ;; FreeBSD*) echo "freebsd" ;; *) echo "unknown" ;; esac }
OS=$(detect_os) case "$OS" in linux) INSTALL_CMD="apt-get install" ;; macos) INSTALL_CMD="brew install" ;; esac
Architecture
detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7l) echo "arm" ;; *) echo "unknown" ;; esac }
Shell Detection
detect_shell() { if [ -n "$BASH_VERSION" ]; then echo "bash" elif [ -n "$ZSH_VERSION" ]; then echo "zsh" else echo "sh" fi }
Portable Patterns
Reading Files
Portable line reading
while IFS= read -r line || [ -n "$line" ]; do echo "$line" done < "$file"
The || [ -n "$line" ] handles files without trailing newline
Temporary Files
POSIX-compatible temp file
make_temp() { if command -v mktemp >/dev/null 2>&1; then mktemp else # Fallback local tmp="/tmp/tmp.$$.$RANDOM" touch "$tmp" && echo "$tmp" fi }
Command Existence Check
POSIX-compatible command check
has_command() { command -v "$1" >/dev/null 2>&1 }
Usage
if has_command curl; then curl "$url" elif has_command wget; then wget -O- "$url" else echo "No HTTP client available" >&2 exit 1 fi
String Contains
POSIX-compatible string contains
contains() { case "$1" in "$2") return 0 ;; *) return 1 ;; esac }
Usage
if contains "$PATH" "/usr/local/bin"; then echo "Found in PATH" fi
ShellCheck Compatibility
Disabling Warnings for Portability
When intentionally using non-portable features
shellcheck disable=SC2039 # Bash-specific feature
if [[ "$var" =~ regex ]]; then : fi
Document why
shellcheck disable=SC2016 # Intentionally not expanding
echo 'Use $HOME for home directory'
Testing Multiple Shells
#!/usr/bin/env bash
shellcheck shell=bash
Or for POSIX:
#!/bin/sh
shellcheck shell=sh
Best Practices
-
Choose the right shebang for your needs
-
Document shell requirements in README
-
Use #!/usr/bin/env bash for bash scripts
-
Test on multiple platforms when possible
-
Prefer POSIX features when portability matters
-
Abstract platform differences into functions
-
Use ShellCheck with appropriate shell directive
-
Provide fallbacks for platform-specific commands