C POSIX vs Bash Compatibility#

Understanding the differences between POSIX shell specification and Bash-specific features.

What is POSIX?#

POSIX (Portable Operating System Interface) defines a standard specification for Unix-like systems. The shell specification ensures scripts work across different Unix implementations: Bash, sh, dash, ksh, zsh, etc.

Key Principle: POSIX shell features work everywhere; Bash extensions only work in Bash.

When to Use Each#

Use POSIX Shell When:#

  • Script needs to run on multiple Unix systems

  • Running in containers with minimal /bin/sh

  • Writing system administration scripts

  • Distributing code to diverse environments

  • Guaranteed to be simple operations

Use Bash When:#

  • Target system definitely has Bash

  • Need advanced features for complex scripts

  • Building interactive user scripts

  • System is Linux-based

  • Need performance features Bash provides

POSIX vs Bash Features#

1. Shebang Line#

POSIX Compatible:

#!/bin/sh

Bash Specific:

#!/bin/bash

2. Variable Declaration#

POSIX:

# Simple variable assignment
VAR="value"

# Cannot use arrays (not in POSIX)
# Cannot use associative arrays

Bash Specific:

# Indexed arrays
arr=(a b c)
${arr[0]}
${arr[@]}

# Associative arrays
declare -A map
map[key]="value"

3. Arithmetic Expansion#

POSIX:

# Use expr command (slower)
result=$(expr 5 + 3)
result=$(expr $x \* 2)  # Note: * must be escaped

Bash (much faster):

result=$((5 + 3))
result=$((x * 2))
result=$((x**2))  # Exponentiation (Bash only)

4. String Operations#

POSIX:

# Limited to parameter expansion
${VAR#pattern}  # Remove shortest prefix
${VAR%pattern}  # Remove shortest suffix
${VAR:-default} # Use default if unset

# No length, substring, or case conversion

Bash Specific:

# Additional operations
${#VAR}              # Length
${VAR:0:5}          # Substring
${VAR/find/replace} # Replace
${VAR//find/replace} # Replace all (Bash 4+)
${VAR^^}             # Uppercase (Bash 4+)
${VAR,,}             # Lowercase (Bash 4+)
${!VAR@}             # Variable names matching prefix

5. Test Operators#

POSIX:

[ -f FILE ]    # File test
[ -d DIR ]     # Directory test
[ STRING = STRING ] # String comparison
[ -n STRING ]  # Non-empty string
[ -z STRING ]  # Empty string
[ NUM -eq NUM ] # Numeric equal
[ NUM -ne NUM ] # Numeric not equal
[ NUM -lt NUM ] # Numeric less than
[ -a COND ]    # AND operator
[ -o COND ]    # OR operator
[ ! COND ]     # NOT operator

Bash Specific (using [[ ]]):

[[ -f FILE ]]
[[ STRING = PATTERN ]]  # Glob pattern matching
[[ STRING =~ REGEX ]]   # Regular expression matching
[[ COND && COND ]]      # AND operator (cleaner)
[[ COND || COND ]]      # OR operator (cleaner)
[[ ! COND ]]            # NOT operator (cleaner)

6. Conditionals#

POSIX:

if CONDITION; then
  COMMANDS
elif CONDITION; then
  COMMANDS
else
  COMMANDS
fi

case $VAR in
  pattern) COMMANDS ;;
  *) DEFAULT ;;
esac

Bash (same syntax, but more robust [[]]):

if [[ CONDITION ]]; then
  COMMANDS
elif [[ CONDITION ]]; then
  COMMANDS
else
  COMMANDS
fi

# [[ ]] is safer (no word splitting, no glob expansion)

7. Loops#

POSIX (both work in Bash):

for VAR in LIST; do
  COMMANDS
done

while CONDITION; do
  COMMANDS
done

until CONDITION; do
  COMMANDS
done

Bash Extensions:

# C-style for loop
for ((i=0; i<10; i++)); do
  echo $i
done

# Brace expansion for loops
for i in {1..10}; do
  echo $i
done

8. Functions#

POSIX:

FNAME() {
  COMMANDS
}

# Call like
FNAME arg1 arg2

# Access arguments
$1 $2 $3 $@ $# $?

Bash:

# Both styles work
function fname() {
  COMMANDS
}

fname() {
  COMMANDS
}

# Bash-specific: local variables
function test() {
  local var="local"
  return 42
}

9. Command Substitution#

POSIX (both styles):

result=`command`
result=$(command)

# Prefer $() - more readable, nestable
result=$(echo $(date))

Note: Bash prefers $() syntax.

10. Redirection#

POSIX:

> FILE          # Redirect stdout
< FILE          # Redirect stdin
>> FILE         # Append stdout
2> FILE         # Redirect stderr (in POSIX)
2>&1            # Redirect stderr to stdout
| COMMAND       # Pipe

Bash Additions:

&> FILE         # Redirect both stdout and stderr (Bash only)
>>FILE 2>&1     # Append both (more portable)
|& COMMAND      # Pipe both (Bash 4+)

11. Process Substitution#

POSIX: Not available

Bash Only:

while read line; do
  process "$line"
done < <(command)  # Redirect process output

paste <(seq 1 5) <(seq 1 5)  # Side-by-side output

# Compare files
diff <(sort file1) <(sort file2)

12. Globbing and Expansion#

POSIX:

# Basic glob patterns
*.txt
?
[abc]
[a-z]

Bash Extensions:

# Brace expansion
{a,b,c}
{1..10}
{01..10}  # With padding

# Extended glob patterns (shopt -s extglob)
!(pattern)    # Anything except pattern
?(pattern)    # Zero or one
*(pattern)    # Zero or more
+(pattern)    # One or more

13. Named Parameters#

POSIX: Not available (positional only)

Bash 4+ Specific:

function greet() {
  # Still not true named params, but use patterns
  local name="${1:?Name required}"
  local greeting="${2:-Hello}"
  echo "$greeting, $name"
}

# Call like
greet "John" "Hi"

# Workaround for named params
function deploy() {
  while [[ $# -gt 0 ]]; do
    case $1 in
      --env=*) ENV="${1#*=}" ;;
      --tag=*) TAG="${1#*=}" ;;
      *) echo "Unknown: $1" ;;
    esac
    shift
  done
}

deploy --env=prod --tag=v1.0

Compatibility Checklist#

Writing Portable Scripts#

Use this checklist to ensure POSIX compatibility:

  • Use #!/bin/sh (not #!/bin/bash)

  • Avoid [[  ]] - use [ ] or test

  • Avoid ${var:0:5} substring - not POSIX

  • Avoid ${var//find/replace} - use sed

  • Avoid declare -a arrays - use separate variables or set

  • Use expr for arithmetic (or accept Bash-only)

  • Use `cmd` or $(cmd) not just variables

  • Avoid function-local variables - use local only if Bash-only

  • Don’t use += operator - not POSIX

  • Avoid process substitution <(cmd)

  • Test with sh not just bash

Conditional POSIX/Bash Code#

#!/bin/bash
# Works in both, but uses Bash when available

# POSIX way to get array length (compatible)
length=${#arr}

# Bash way (better in Bash)
if [[ -n "${BASH_VERSION}" ]]; then
  length=${#arr[@]}
else
  length=${#arr}
fi

# Check for Bash features
if command -v shopt &> /dev/null; then
  # This is Bash
  shopt -s extglob
fi

Common Incompatibilities#

Feature

POSIX

Bash

Issue

[[  ]]

No

Yes

Use [  ] for POSIX

Arrays

No

Yes

Use separate variables

Associative arrays

No

Yes

Bash 4+ only

$((expr))

No

Yes

Use expr for POSIX

${var:offset:len}

No

Yes

Not in POSIX

${var//find}

No

Yes

Use sed for POSIX

+= operator

No

Yes

Use var=$var... for POSIX

Regex in [[ =~ ]]

No

Yes

Use grep for POSIX

(( )) arithmetic

No

Yes

Use [ ] for POSIX

local in functions

Partial

Yes

Not guaranteed POSIX

Process substitution

No

Yes

Bash only feature

<<< heredoc string

No

Yes

Bash only

Associative array

No

Yes

Bash 4+

Testing Compatibility#

# Test in POSIX sh (dash is strict POSIX)
sh -n script.sh        # Syntax check
sh script.sh           # Run with sh

# Test with different shells
for shell in sh bash ksh zsh; do
  echo "Testing with $shell:"
  $shell -n script.sh
  $shell script.sh
done

# Use ShellCheck to find compatibility issues
shellcheck -S warning script.sh

Migration Path#

From Bash-only to POSIX:#

  1. Remove [[  ]] → use [  ]

  2. Remove arrays → use multiple variables or loops

  3. Replace expr → keep expr (not arithmetic)

  4. Remove substring syntax → use cut or sed

  5. Test with sh regularly

From POSIX to Bash:#

  1. Add #!/bin/bash

  2. Use [[  ]] for safety

  3. Use $((expr)) for arithmetic

  4. Use arrays for complex data

  5. Simplify with modern Bash features