🌍 Portable Shell Scripts Patterns
Writing portable shell scripts ensures compatibility across different Unix-like systems, shells, and environments. This pattern focuses on maximizing compatibility while maintaining functionality.
🎯 Core Principles
POSIX Compliance
Adhere to POSIX standards for maximum portability.
1
2
3
4
5
6
7
8
9
10
11
12
13 | # ❌ Bash-specific syntax
array=(item1 item2 item3)
echo ${array[0]}
# ✅ POSIX-compliant equivalent
set -- item1 item2 item3
echo "$1"
# ❌ Bash parameter expansion
${variable//pattern/replacement}
# ✅ Portable alternative
echo "$variable" | sed 's/pattern/replacement/g'
|
Shell Declaration
Specify compatible shell interpreters.
1
2
3
4
5
6
7
8
9
10
11
12
13 | #!/bin/sh
# For maximum portability
#!/bin/bash
# When Bash-specific features are needed
#!/bin/dash
# For lightweight environments
# Always check shell capabilities
if [ -z "${BASH_VERSION}" ]; then
echo "Warning: Not running under Bash" >&2
fi
|
🔧 Syntax and Constructs
Variable Assignment and Expansion
Use portable variable handling.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | # ✅ Portable variable assignment
variable="value"
# ❌ Bash array syntax
items=(one two three)
# ✅ Portable list handling
set -- one two three
# Access as: $1, $2, $3
# Count as: $#
# ✅ Parameter expansion alternatives
# Instead of ${var:-default}
var_with_default() {
if [ -n "$1" ]; then
echo "$1"
else
echo "default"
fi
}
# Instead of ${var#prefix}
remove_prefix() {
echo "$1" | sed "s/^$2//"
}
# Instead of ${var%suffix}
remove_suffix() {
echo "$1" | sed "s/$2$//"
}
|
Conditional Statements
Use universally supported conditionals.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # ✅ Portable conditionals
if [ -n "$variable" ]; then
echo "Variable is set"
fi
if [ "$count" -gt 0 ] 2>/dev/null; then
echo "Count is positive"
elif [ "$count" -eq 0 ] 2>/dev/null; then
echo "Count is zero"
else
echo "Invalid count"
fi
# ❌ Bash-specific [[ ]]
if [[ "$string" =~ ^[0-9]+$ ]]; then
echo "String is numeric"
fi
# ✅ Portable regex check
if echo "$string" | grep -E '^[0-9]+$' >/dev/null 2>&1; then
echo "String is numeric"
fi
|
📋 String and Text Processing
Portable Text Manipulation
Avoid shell-specific string operations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | # ✅ Portable string operations
string_length() {
echo "${#1}" # This works in most shells
}
# Alternative for older shells
string_length_alt() {
echo "$1" | wc -c | awk '{print $1 - 1}'
}
# Substring extraction
substring() {
local string="$1"
local start="$2"
local length="$3"
echo "$string" | cut -c"${start}-$((start + length - 1))"
}
# Case conversion (portable)
to_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}
to_upper() {
echo "$1" | tr '[:lower:]' '[:upper:]'
}
|
File and Path Operations
Handle paths portably across systems.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | # ✅ Portable path operations
get_filename() {
# Works on most systems
basename "$1"
}
get_directory() {
# Works on most systems
dirname "$1"
}
# Absolute path resolution (without readlink -f)
resolve_absolute_path() {
local path="$1"
# Handle relative paths
if [ "${path#/}" = "$path" ]; then
# Relative path
path="$(pwd)/$path"
fi
# Normalize path separators
echo "$path" | sed 's|//|/|g'
}
# Cross-platform directory creation
safe_mkdir() {
mkdir -p "$1" 2>/dev/null || {
# Fallback for very old systems
if [ ! -d "$1" ]; then
mkdir "$1" 2>/dev/null
fi
}
}
|
🛠️ System Compatibility
Command Availability Checking
Verify command existence before use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | # ✅ Portable command checking
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Alternative for very old systems
command_exists_alt() {
which "$1" >/dev/null 2>&1
}
# Usage with fallbacks
get_system_info() {
if command_exists "lsb_release"; then
lsb_release -d
elif [ -f "/etc/os-release" ]; then
grep PRETTY_NAME /etc/os-release
elif [ -f "/etc/redhat-release" ]; then
cat /etc/redhat-release
else
uname -s
fi
}
# Tool selection with fallbacks
find_working_tool() {
for tool in "$@"; do
if command_exists "$tool"; then
echo "$tool"
return 0
fi
done
return 1
}
# Usage
ARCHIVER=$(find_working_tool "tar" "cpio")
if [ -z "$ARCHIVER" ]; then
echo "No archiving tool found" >&2
exit 1
fi
|
Identify system characteristics reliably.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | # ✅ Portable platform detection
detect_platform() {
# Operating system
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "darwin" ;;
SunOS*) echo "solaris" ;;
AIX*) echo "aix" ;;
HP-UX*) echo "hpux" ;;
FreeBSD*) echo "freebsd" ;;
OpenBSD*) echo "openbsd" ;;
NetBSD*) echo "netbsd" ;;
*) echo "unknown" ;;
esac
}
detect_architecture() {
# CPU architecture
case "$(uname -m)" in
x86_64|amd64) echo "x86_64" ;;
i386|i686) echo "i386" ;;
armv7l) echo "arm" ;;
aarch64) echo "arm64" ;;
ppc64) echo "ppc64" ;;
*) echo "unknown" ;;
esac
}
# Line ending detection
detect_line_endings() {
# Check if system uses CR/LF (Windows) or LF (Unix)
if [ "$(printf '\n')" != "$(printf '\r\n' | tr -d '\r')" ]; then
echo "windows"
else
echo "unix"
fi
}
|
🎨 Advanced Portability Techniques
Error Handling
Implement robust, portable error handling.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | # ✅ Portable error handling
set -e # Exit on error (use carefully)
set -u # Exit on undefined variables
# Better error handling approach
safe_execute() {
"$@" || {
echo "Command failed: $*" >&2
return 1
}
}
# Error trapping
error_handler() {
local line_no=$1
local error_code=$2
echo "Error at line $line_no: exit code $error_code" >&2
}
# Set up error handling
trap 'error_handler $LINENO $?' ERR
# Temporary file handling (portable)
create_temp_file() {
local suffix="${1:-}"
# Try mktemp first
if command_exists "mktemp"; then
mktemp "${TMPDIR:-/tmp}/XXXXXXXX$suffix" 2>/dev/null || {
# Fallback for systems without mktemp
local temp_file="${TMPDIR:-/tmp}/temp_$$_$(date +%s)$suffix"
touch "$temp_file"
echo "$temp_file"
}
else
# Very old systems fallback
local temp_file="${TMPDIR:-/tmp}/temp_$$_$(date +%s)$suffix"
touch "$temp_file"
echo "$temp_file"
fi
}
|
Function Libraries
Create portable function collections.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | # lib/portable.sh - Portable utilities
# Safe echo (some shells have issues with -n)
safe_echo() {
if [ "$#" -gt 0 ] && [ "$1" = "-n" ]; then
shift
printf '%s' "$*"
else
printf '%s\n' "$*"
fi
}
# Portable file testing
is_readable() {
[ -r "$1" ] 2>/dev/null
}
is_writable() {
[ -w "$1" ] 2>/dev/null
}
is_executable() {
[ -x "$1" ] 2>/dev/null
}
# Portable arithmetic
safe_arithmetic() {
expr "$1" "$2" "$3" 2>/dev/null || echo "0"
}
# Date formatting (portable)
portable_date() {
if date --help 2>&1 | grep -q GNU; then
# GNU date
date -u +"%Y-%m-%dT%H:%M:%SZ"
else
# BSD/POSIX date
date -u
fi
}
|
🧾 Summary Best Practices
Compatibility Guidelines
✅ Stick to POSIX shell syntax
✅ Test with multiple shells (dash, bash, ksh, zsh)
✅ Avoid bashisms unless specifically targeting Bash
✅ Check command availability before use
✅ Handle different path separators and conventions
Universal Do's and Don'ts
Do:
- Use [ ] instead of [[ ]]
- Use command -v to check command existence
- Quote variables consistently
- Use $(command) instead of backticks when possible
- Handle errors gracefully
Don't:
- Use arrays (unless targeting Bash)
- Rely on Bash-specific parameter expansion
- Assume specific command-line tools exist
- Hardcode path separators
- Ignore return codes
Testing Strategy
✅ Test on multiple Unix-like systems
✅ Use shellcheck with POSIX mode
✅ Validate with different shell interpreters
✅ Check in minimal container environments
✅ Verify with old system emulators
🧠 Portable Script Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 | #!/bin/sh
# portable-template.sh - Template for portable scripts
# Exit on error and undefined variables
set -e
# Note: set -u can cause issues in some shells, use carefully
# Configuration
DEFAULT_TIMEOUT=30
TEMP_DIR="${TMPDIR:-/tmp}"
# Portable utility functions
command_exists() {
command -v "$1" >/dev/null 2>&1
}
safe_temp_file() {
if command_exists "mktemp"; then
mktemp "${TEMP_DIR}/XXXXXXXX" 2>/dev/null || {
temp_file="${TEMP_DIR}/temp_$$_$(date +%s)"
touch "$temp_file"
echo "$temp_file"
}
else
temp_file="${TEMP_DIR}/temp_$$_$(date +%s)"
touch "$temp_file"
echo "$temp_file"
fi
}
# Main logic
main() {
# Parse arguments portably
while [ "$#" -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: $0 [options]"
exit 0
;;
-t|--timeout)
TIMEOUT="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
# Set defaults
TIMEOUT="${TIMEOUT:-$DEFAULT_TIMEOUT}"
# Your portable logic here
echo "Running with timeout: $TIMEOUT"
}
# Error handling
cleanup() {
# Cleanup code here
:
}
trap cleanup EXIT
# Run main function
main "$@"
|
🧾 See Also