Przejdź do treści

🌍 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

Platform Detection

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