ð shell-portability
Use when writing shell scripts that need to run across different systems, shells, or environments. Covers POSIX compatibility and platform differences.
Overview
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 bashfor 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