🌐 Advanced POSIX Compatibility
Master writing portable shell scripts that work across diverse Unix-like systems by adhering to POSIX standards and avoiding platform-specific extensions.
🧭 Understanding POSIX Standards
POSIX (Portable Operating System Interface) defines a family of standards for maintaining compatibility between operating systems. For shell scripting, POSIX.1-2017 defines:
Core Requirements
- Shell Grammar - Command syntax and structure
- Built-in Utilities - Required commands and their behavior
- Environment Variables - Standard variables and their meanings
- File System Semantics - Path handling, permissions, links
- Signal Handling - Standard signals and their default actions
POSIX Shell Features
| # ✅ POSIX-compliant features
var=value # Variable assignment
[ "$var" = "value" ] # Test command
for i in 1 2 3; do ... done # For loop
while [ condition ]; do ... # While loop
case $var in pattern) ... ;; # Case statement
$(command) # Command substitution
${var:-default} # Parameter expansion
$((arithmetic)) # Arithmetic expansion
|
🧪 Identifying Non-POSIX Features
Common Bashisms to Avoid
| # ❌ Bash-specific features (not POSIX)
[[ condition ]] # Double bracket test
${array[@]} # Array expansion
function name { ... } # Function declaration
source file # Source command
<(command) # Process substitution
{1..10} # Brace expansion
((arithmetic)) # Arithmetic evaluation
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | # ShellCheck for POSIX compliance
shellcheck --shell=sh script.sh
shellcheck --shell=posix script.sh
# checkbashisms tool
checkbashisms script.sh
# Manual testing with different shells
test_posix_compliance() {
local script="$1"
for shell in sh dash posh; do
echo "Testing with $shell..."
if ! $shell "$script"; then
echo "❌ Failed in $shell"
else
echo "✅ Passed in $shell"
fi
done
}
|
🧠 Writing Portable Shell Scripts
Standard Header 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 | #!/bin/sh
# vim: set ft=sh:
# POSIX-compliant shell script template
# Exit on error and undefined variables
set -eu
# Set consistent locale for predictable behavior
LC_ALL=C
export LC_ALL
# Portable directory detection
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)"
# Error handling function
error_exit() {
echo "${0##*/}: error: $*" >&2
exit 1
}
# Usage function
usage() {
cat <<EOF
Usage: ${0##*/} [OPTIONS] ARGUMENTS
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
EXAMPLES:
${0##*/} --verbose input.txt
EOF
}
# Main function
main() {
# Parse arguments
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
;;
-*)
error_exit "Unknown option: $1"
;;
*)
break
;;
esac
shift
done
# Main logic here
}
# Run main function
main "$@"
|
🧪 Portable Parameter Expansion
Safe Parameter Substitution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # ✅ POSIX-compliant parameter expansions
portable_expansions() {
# Default values
editor="${EDITOR:-vi}"
config_file="${CONFIG_FILE:-$HOME/.config/app.conf}"
# Remove prefix/suffix
filename="${filepath##*/}" # basename equivalent
dirname="${filepath%/*}" # dirname equivalent
no_ext="${filename%.*}" # remove extension
# Replace patterns
new_path="${old_path/$HOME/\/home\/user}" # replace first occurrence
all_paths="${old_path//$HOME/\/home\/user}" # replace all occurrences
# Length
length="${#string}"
# Substring
substr="${string#?}" # remove first character
substr="${string%?}" # remove last character
}
|
Avoiding Bash-Specific Expansions
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 | # ❌ Bash-specific (avoid in portable scripts)
bash_specific() {
# Arrays
arr=(item1 item2 item3)
echo "${arr[@]}"
# Associative arrays
declare -A assoc
assoc[key]=value
# Advanced parameter expansion
echo "${var^^}" # uppercase
echo "${var,,}" # lowercase
echo "${!var}" # indirect expansion
}
# ✅ POSIX alternatives
posix_alternatives() {
# Use positional parameters instead of arrays
set -- item1 item2 item3
for item; do
echo "$item"
done
# Use delimited strings for associative-like behavior
config="key1:value1;key2:value2"
echo "$config" | tr ';' '\n' | while IFS=':' read -r key value; do
echo "Key: $key, Value: $value"
done
# Manual case conversion
to_uppercase() {
echo "$1" | tr '[:lower:]' '[:upper:]'
}
# Indirect variable access
get_indirect() {
eval printf '%s' \"\$$1\"
}
}
|
🧠 Portable Command Usage
Standard Utility Alternatives
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 | # Portable alternatives to GNU-specific features
# ❌ GNU-specific
gnu_specific() {
# seq with floating point
seq 0.1 0.1 1.0
# sort with version sorting
sort -V versions.txt
# grep with Perl regex
grep -P '\d+' numbers.txt
}
# ✅ POSIX alternatives
posix_alternatives() {
# Generate sequence manually
i=0.1
while [ "$(printf '%.1f' "$i")" != "1.1" ]; do
printf '%.1f\n' "$i"
i=$(echo "$i + 0.1" | bc)
done
# Version sorting workaround
sort_versions() {
# Simple numeric version sort
sort -t. -k1,1n -k2,2n -k3,3n versions.txt
}
# Basic regex with standard grep
grep '[0-9]\+' numbers.txt # Extended regex pattern
}
|
File and Directory 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 | # Portable file operations
# ✅ Safe directory creation
safe_mkdir() {
# POSIX mkdir -p equivalent
for dir in "$@"; do
if [ ! -d "$dir" ]; then
# Create parent directories
parent_dir=$(dirname "$dir")
if [ "$parent_dir" != "/" ] && [ "$parent_dir" != "." ]; then
safe_mkdir "$parent_dir"
fi
# Create directory
if ! mkdir "$dir" 2>/dev/null; then
# Handle race condition
if [ ! -d "$dir" ]; then
echo "Failed to create directory: $dir" >&2
return 1
fi
fi
fi
done
}
# ✅ Portable temporary file creation
create_temp() {
# Try mktemp first
if command -v mktemp >/dev/null 2>&1; then
mktemp "${TMPDIR:-/tmp}/XXXXXXXX"
else
# Fallback method
echo "${TMPDIR:-/tmp}/tmp.$$.$(date +%s)"
fi
}
# ✅ Portable file testing
test_file() {
# Use -e instead of -f for broader compatibility
[ -e "$1" ] && echo "File exists"
# Test readability/writability properly
[ -r "$1" ] && echo "Readable"
[ -w "$1" ] && echo "Writable"
# Test executability for regular files
if [ -f "$1" ] && [ -x "$1" ]; then
echo "Executable regular file"
fi
}
|
Testing Matrix
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 | # Comprehensive cross-platform testing script
test_compatibility() {
local script="$1"
local shells="
sh
dash
bash --posix
zsh --emulate sh
ksh93
mksh
posh
"
local failed_shells=""
echo "Testing POSIX compatibility for: $script"
echo "=========================================="
for shell_cmd in $shells; do
set -- $shell_cmd
local shell="$1"
shift
local args="$*"
if command -v "$shell" >/dev/null 2>&1; then
echo -n "Testing with $shell $args... "
if "$shell" $args "$script" >/dev/null 2>&1; then
echo "✅ PASS"
else
echo "❌ FAIL"
failed_shells="$failed_shells $shell"
fi
else
echo "⚠️ SKIP ($shell not found)"
fi
done
if [ -n "$failed_shells" ]; then
echo "❌ Failed shells:$failed_shells"
return 1
else
echo "✅ All tests passed!"
return 0
fi
}
|
Docker-Based Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | # Docker-based cross-platform testing
docker_test() {
local script="$1"
local test_images="
alpine:latest
debian:stable-slim
ubuntu:latest
centos:7
rockylinux:8
"
for image in $test_images; do
echo "Testing in $image..."
docker run --rm -v "$(pwd):/test" "$image" sh -c "
cd /test
if sh $script; then
echo '✅ PASS in $image'
else
echo '❌ FAIL in $image'
fi
"
done
}
|
OS-Specific Code Sections
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 OS detection
detect_os() {
case "$(uname)" in
Linux*) echo "linux" ;;
Darwin*) echo "darwin" ;;
CYGWIN*|MINGW*) echo "windows" ;;
FreeBSD*) echo "freebsd" ;;
OpenBSD*) echo "openbsd" ;;
SunOS*) echo "solaris" ;;
*) echo "unknown" ;;
esac
}
# Conditional code execution
platform_specific() {
case "$(detect_os)" in
linux)
# Linux-specific code
if command -v systemctl >/dev/null 2>&1; then
systemctl status service-name
fi
;;
darwin)
# macOS-specific code
if command -v launchctl >/dev/null 2>&1; then
launchctl list | grep service-name
fi
;;
*)
# Fallback for other systems
echo "OS not specifically supported"
;;
esac
}
|
Path and Filesystem 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 | # Portable path manipulation
portable_path() {
# Normalize path separators
normalize_path() {
echo "$1" | sed 's#//#/#g'
}
# Join paths portably
join_path() {
local base="$1"
local append="$2"
# Remove trailing slash from base
base="${base%/}"
# Remove leading slash from append
append="${append#/}"
echo "$base/$append"
}
# Portable directory separator
DIR_SEP="/"
case "$(detect_os)" in
windows) DIR_SEP="\\" ;;
esac
}
|
🧪 Advanced POSIX Patterns
Portable Function Libraries
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 | # lib/portable.sh - Portable utility functions
# Portable realpath implementation
portable_realpath() {
local target="$1"
# Try system realpath first
if command -v realpath >/dev/null 2>&1; then
realpath "$target"
return
fi
# Fallback implementation
if [ -d "$target" ]; then
(CDPATH= cd -P -- "$target" && pwd -P)
elif [ -f "$target" ]; then
local dir
local file
dir=$(dirname "$target")
file=$(basename "$target")
(CDPATH= cd -P -- "$dir" && printf '%s/%s\n' "$(pwd -P)" "$file")
else
echo "$target"
fi
}
# Portable basename/dirname
portable_basename() {
# Remove all trailing slashes
local path="${1%/}"
# Remove everything up to last slash
echo "${path##*/}"
}
portable_dirname() {
# Remove all trailing slashes
local path="${1%/}"
# Remove everything after last slash
case "$path" in
*/*) echo "${path%/*}" ;;
*) echo "." ;;
esac
}
|
Error Handling Patterns
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 | # Portable error handling framework
portable_error_handling() {
# Standard error codes
readonly E_SUCCESS=0
readonly E_GENERAL=1
readonly E_MISSARG=2
readonly E_NOTFOUND=3
readonly E_PERMDENIED=4
readonly E_SYNTAX=5
# Portable error reporting
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
fatal() {
error "$@"
exit "${2:-$E_GENERAL}"
}
# Portable usage function
usage() {
[ "$#" -gt 0 ] && error "$@"
cat <<EOF
Usage: ${0##*/} [OPTIONS] ARGUMENTS
Standard options:
-h, --help Display this help and exit
-V, --version Display version information and exit
EOF
exit "${1:-$E_SUCCESS}"
}
}
|
🧠 Continuous Compatibility Testing
Integration with CI/CD
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 | # .github/workflows/posix-test.yml
name: POSIX Compatibility Test
on: [push, pull_request]
jobs:
posix-test:
runs-on: ubuntu-latest
strategy:
matrix:
shell: [sh, dash, bash, zsh]
include:
- shell: bash
args: --posix
- shell: zsh
args: --emulate sh
steps:
- uses: actions/checkout@v2
- name: Install test shells
run: |
sudo apt-get update
sudo apt-get install -y dash zsh
- name: Test with ${{ matrix.shell }}
run: |
${{ matrix.shell }} ${{ matrix.args }} ./your-script.sh
|
Automated Compliance Checking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | # Continuous POSIX compliance checker
check_posix_compliance() {
local target_dir="${1:-.}"
find "$target_dir" -name "*.sh" -type f | while read -r script; do
echo "Checking $script..."
# ShellCheck for POSIX
if command -v shellcheck >/dev/null 2>&1; then
shellcheck --shell=posix "$script"
fi
# checkbashisms
if command -v checkbashisms >/dev/null 2>&1; then
checkbashisms "$script" || echo "Potential bashisms found"
fi
# Test with multiple shells
test_compatibility "$script"
done
}
|
🧾 POSIX Compatibility Best Practices
Key Guidelines
- Use
#!/bin/sh shebang - Ensures POSIX shell execution
- Test with multiple shells - dash, posh, mksh for strict compliance
- Avoid bashisms - Even in bash scripts for maximum portability
- Use standard utilities - Prefer POSIX-defined commands
- Quote variables properly - Prevent word splitting issues
- Handle signals correctly - Use portable signal names
- Validate inputs rigorously - Assume hostile environment
- Document dependencies - Make requirements explicit
Common POSIX Compliance Checklist
- [ ] Uses
#!/bin/sh shebang
- [ ] Avoids
[[ ]] (use [ ] instead)
- [ ] Avoids
${array[@]} (use $@ instead)
- [ ] Avoids
function name (use name() instead)
- [ ] Avoids
source (use . instead)
- [ ] Avoids
<() process substitution
- [ ] Avoids
{1..10} brace expansion
- [ ] Uses quoted variables:
"$var"
- [ ] Uses portable parameter expansions
- [ ] Tests with multiple POSIX shells
🧾 Summary
Essential POSIX Concepts
- Standards Compliance - Follow POSIX.1-2017 specifications
- Shell Grammar - Use standard command syntax
- Built-in Utilities - Rely on POSIX-defined commands
- Parameter Expansion - Use portable substitution patterns
- Error Handling - Implement standard error reporting
- Signal Handling - Use portable signal names and behaviors
Advanced POSIX Techniques
- Cross-platform testing - Validate with multiple shells
- Conditional compilation - Adapt to platform differences
- Portable libraries - Create reusable POSIX functions
- Continuous integration - Automate compliance checking
- Documentation standards - Clearly specify requirements
- ShellCheck - Static analysis for POSIX compliance
- checkbashisms - Detect bash-specific features
- Multiple shells - dash, posh, mksh for testing
- Docker containers - Test across distributions
- CI/CD integration - Automated compatibility verification
👉 Continue to: Security and Sandboxing