POSIX Shell Compliance Standards
Audience: Contributors, portability reviewers
WHAT
POSIX.1-2017 compliance reference for shell scripting, documenting which Bash features are POSIX-compliant and which are Bash-only.
WHY
Understanding POSIX boundaries helps contributors make intentional decisions about portability vs Bash-specific convenience.
HOW
Table of Contents
- Overview
- Why POSIX Compliance Matters
- POSIX vs Bash Features
- POSIX Compliant Alternatives
- Testing for POSIX Compliance
- Common Pitfalls
- POSIX Shell Built-ins
- POSIX Utilities
- Best Practices
- DCK Implementation
- Shell Detection
- References
Overview
POSIX (Portable Operating System Interface) defines standards for shell scripting portability across Unix-like systems. This ensures scripts work on any POSIX-compliant shell (sh, dash, ksh, etc.), not just bash.
Why POSIX Compliance Matters
- Portability: Scripts run on any Unix-like system
- Container Compatibility: Alpine uses ash, Ubuntu uses dash
- Performance: POSIX shells are faster and smaller
- Embedded Systems: Limited environments may only have sh
- CI/CD: Build environments may not have bash
POSIX vs Bash Features
POSIX Compliant Features
Basic Syntax
#!/bin/sh
# POSIX shell shebang
# Variable assignment
VAR="value"
readonly VAR="constant"
# Command substitution
result=$(command)
# Arithmetic
result=$((2 + 2))
# Conditionals
if [ "$var" = "value" ]; then
echo "match"
elif [ "$var" != "other" ]; then
echo "no match"
else
echo "default"
fi
# Loops
for item in item1 item2 item3; do
echo "$item"
done
while [ "$count" -lt 10 ]; do
count=$((count + 1))
done
# Case statements
case "$var" in
pattern1) echo "match 1" ;;
pattern2|pattern3) echo "match 2 or 3" ;;
*) echo "default" ;;
esac
# Functions
my_function() {
echo "arg: $1"
return 0
}
# Parameter expansion
${var:-default} # Use default if unset
${var:=default} # Set and use default if unset
${var:?error} # Error if unset
${var:+alternate} # Use alternate if set
${#var} # String length
${var%pattern} # Remove shortest suffix
${var%%pattern} # Remove longest suffix
${var#pattern} # Remove shortest prefix
${var##pattern} # Remove longest prefix
Test Operators
# File tests
[ -e file ] # Exists
[ -f file ] # Regular file
[ -d dir ] # Directory
[ -r file ] # Readable
[ -w file ] # Writable
[ -x file ] # Executable
[ -s file ] # Size > 0
[ -L link ] # Symbolic link
# String tests
[ -z "$str" ] # Zero length
[ -n "$str" ] # Non-zero length
[ "$s1" = "$s2" ] # Equal
[ "$s1" != "$s2" ] # Not equal
# Numeric tests
[ "$n1" -eq "$n2" ] # Equal
[ "$n1" -ne "$n2" ] # Not equal
[ "$n1" -lt "$n2" ] # Less than
[ "$n1" -le "$n2" ] # Less or equal
[ "$n1" -gt "$n2" ] # Greater than
[ "$n1" -ge "$n2" ] # Greater or equal
# Logical operators
[ expr1 ] && [ expr2 ] # AND
[ expr1 ] || [ expr2 ] # OR
! [ expr ] # NOT
Bash-Only Features (Not POSIX)
Arrays
# Bash arrays - NOT POSIX
array=(one two three)
echo "${array[0]}"
echo "${array[@]}"
echo "${#array[@]}"
# POSIX alternative - use positional parameters
set -- one two three
echo "$1" # First element
echo "$@" # All elements
echo "$#" # Count
Advanced Test Syntax
# Bash [[ ]] - NOT POSIX
[[ "$str" =~ regex ]]
[[ "$str" == pattern* ]]
[[ "$a" < "$b" ]]
# POSIX alternatives
expr "$str" : "regex" >/dev/null
case "$str" in pattern*) ;; esac
[ "$(printf '%s\n' "$a" "$b" | sort | head -n1)" = "$a" ]
String Manipulation
# Bash string manipulation - NOT POSIX
${var^^} # Uppercase
${var,,} # Lowercase
${var/old/new} # Replace first
${var//old/new} # Replace all
# POSIX alternatives
echo "$var" | tr '[:lower:]' '[:upper:]' # Uppercase
echo "$var" | tr '[:upper:]' '[:lower:]' # Lowercase
echo "$var" | sed 's/old/new/' # Replace first
echo "$var" | sed 's/old/new/g' # Replace all
Process Substitution
# Bash process substitution - NOT POSIX
diff <(cmd1) <(cmd2)
# POSIX alternative using temp files
tmp1=$(mktemp)
tmp2=$(mktemp)
cmd1 > "$tmp1"
cmd2 > "$tmp2"
diff "$tmp1" "$tmp2"
rm -f "$tmp1" "$tmp2"
Arithmetic
# Bash arithmetic - NOT POSIX
((count++))
((result = a * b))
for ((i=0; i<10; i++)); do
# POSIX alternatives
count=$((count + 1))
result=$((a * b))
i=0; while [ "$i" -lt 10 ]; do
i=$((i + 1))
done
POSIX Compliance Checklist
Script Header
#!/bin/sh
# POSIX compliant shell script
set -e # Exit on error
set -u # Error on undefined variables
Variable Handling
- Always quote variables:
"$var" - Use
${var:-default}for defaults - Check if set:
[ -n "${var:-}" ] - No arrays, use positional parameters
- No declare/typeset/local (use subshells)
Command Syntax
- Use
[ ]not[[ ]] - Use
$(...)not`...` - Use
$((...))for arithmetic - No
<<<here-strings - No
<()process substitution
Functions
# POSIX function definition
my_func() {
# No 'local' keyword - use subshell for scope
(
var="local value"
echo "$var"
)
}
# No 'function' keyword
# No 'declare -f'
Portability Tips
1. Echo vs Printf
# echo behavior varies between shells
echo -n "text" # May not work
# printf is POSIX and consistent
printf '%s' "text"
printf '%s\n' "text with newline"
2. Command Availability
# Check command exists (POSIX)
command -v docker >/dev/null 2>&1 || {
echo "docker not found" >&2
exit 1
}
# NOT: which, type -P, hash
3. Signal Handling
# POSIX signal names (no SIG prefix)
trap 'cleanup' INT TERM EXIT
trap 'cleanup' 2 15 0 # Numeric also works
# NOT: SIGINT, SIGTERM
4. Redirection
# POSIX redirection
cmd > file 2>&1 # Stdout and stderr to file
cmd >> file 2>&1 # Append
# NOT: &>, &>>
Testing for POSIX Compliance
1. Use sh Instead of Bash
# Test with POSIX shell
sh -n script.sh # Syntax check
sh script.sh # Run with sh
2. Use Dash (Debian/Ubuntu)
# Install dash
apt-get install dash
# Test script
dash -n script.sh
dash script.sh
3. Use ShellCheck
# Check for POSIX compliance
shellcheck --shell=sh script.sh
# Add to script
# shellcheck shell=sh
4. Use checkbashisms
# Install
apt-get install devscripts
# Check for bashisms
checkbashisms script.sh
Common POSIX Violations and Fixes
1. Arrays
# Violation
files=(*.txt)
for file in "${files[@]}"; do
# POSIX Fix
for file in *.txt; do
2. Substring Expansion
# Violation
if [[ "$string" == *"substring"* ]]; then
# POSIX Fix
case "$string" in
*substring*) echo "found" ;;
esac
3. Regex Matching
# Violation
if [[ "$email" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then
# POSIX Fix
if expr "$email" : '^[a-z]\+@[a-z]\+\.[a-z]\+$' >/dev/null; then
4. Integer Comparison in [[
# Violation
if [[ $num -gt 10 ]]; then
# POSIX Fix
if [ "$num" -gt 10 ]; then
5. Here Strings
# Violation
cmd <<< "$variable"
# POSIX Fix
printf '%s\n' "$variable" | cmd
# or
cmd <<EOF
$variable
EOF
DCK POSIX Compliance Status
| Feature | POSIX | DCK Status | Notes |
|---|---|---|---|
| Shebang | #!/bin/sh | ⚠️ | Uses #!/usr/bin/env bash |
| Test syntax | [ ] | ✅ | Mostly compliant |
| Arrays | None | ⚠️ | Uses bash arrays |
| Arithmetic | $(()) | ✅ | POSIX compliant |
| Functions | name() | ✅ | POSIX style |
| Local vars | None | ⚠️ | Uses local keyword |
| String ops | Limited | ⚠️ | Uses bash features |
Making DCK POSIX Compliant
To make DCK fully POSIX compliant:
- Replace bash arrays with functions:
# Instead of: containers=() # Use: set -- add_container() { set -- "$@" "$1"; } - Replace string operations:
# Instead of: ${var,,} # Use: echo "$var" | tr '[:upper:]' '[:lower:]' - Replace [[ with [:
# Instead of: [[ "$var" == "value" ]] # Use: [ "$var" = "value" ] - Remove local keyword:
# Instead of: local var="value" # Use subshell: (var="value"; command)
Benefits of POSIX Compliance
- Universal Compatibility: Runs on any Unix-like system
- Container Ready: Works in minimal containers
- Faster Execution: POSIX shells are lighter
- Better Testing: Easier to validate behavior
- Industry Standard: Expected in enterprise environments