Przejdź do treści

🌐 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

  1. Shell Grammar - Command syntax and structure
  2. Built-in Utilities - Required commands and their behavior
  3. Environment Variables - Standard variables and their meanings
  4. File System Semantics - Path handling, permissions, links
  5. Signal Handling - Standard signals and their default actions

POSIX Shell Features

1
2
3
4
5
6
7
8
9
# ✅ 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

1
2
3
4
5
6
7
8
# ❌ 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

Detection Tools

 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
}

🧪 Cross-Platform Testing

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
}

🧠 Handling Platform Differences

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

  1. Use #!/bin/sh shebang - Ensures POSIX shell execution
  2. Test with multiple shells - dash, posh, mksh for strict compliance
  3. Avoid bashisms - Even in bash scripts for maximum portability
  4. Use standard utilities - Prefer POSIX-defined commands
  5. Quote variables properly - Prevent word splitting issues
  6. Handle signals correctly - Use portable signal names
  7. Validate inputs rigorously - Assume hostile environment
  8. 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

Tools for POSIX Development

  • 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