Przejdź do treści

🐚 Non-Portable Bashisms Anti-Patterns

Using Bash-specific features in scripts intended to be portable creates compatibility issues across different shells and systems. This anti-pattern identifies common Bashisms and their portable alternatives.


🎯 Core Problems

Shell Compatibility Issues

Bash-specific syntax fails on other POSIX-compliant shells.

 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
# ❌ Anti-pattern: Bash array syntax
process_items() {
    local items=(item1 item2 item3)  # Bash-only syntax

    for item in "${items[@]}"; do    # Bash-only syntax
        echo "Processing: $item"
    done
}

# Problems:
# - Fails on dash, ash, ksh, zsh (in POSIX mode)
# - Not compatible with minimal environments
# - Breaks in containerized environments
# - Fails on embedded systems

# ✅ Portable alternative: Positional parameters
process_items_portable() {
    # Use positional parameters instead of arrays
    for item in "$@"; do
        echo "Processing: $item"
    done
}

# Usage
process_items_portable item1 item2 item3

Parameter Expansion Extensions

Extended parameter expansion not available in all shells.

 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
# ❌ Anti-pattern: Bash parameter expansion
handle_filename() {
    local filepath="$1"

    # Bash-specific extensions
    local basename="${filepath##*/}"        # Remove path
    local extension="${filepath##*.}"       # Get extension
    local no_extension="${filepath%.*}"     # Remove extension
    local lowercase="${filepath,,}"         # Lowercase (Bash 4+)

    echo "Base: $basename, Ext: $extension"
}

# Problems:
# - Not available in POSIX sh
# - Fails on older Bash versions
# - Incompatible with dash/zsh in POSIX mode

# ✅ Portable alternatives
handle_filename_portable() {
    local filepath="$1"

    # Portable basename
    local basename
    basename=$(basename "$filepath")

    # Portable extension extraction
    local extension
    case "$filepath" in
        *.*)
            extension="${filepath##*.}"
            ;;
        *)
            extension=""
            ;;
    esac

    # Portable case conversion
    local lowercase
    lowercase=$(echo "$filepath" | tr '[:upper:]' '[:lower:]')

    echo "Base: $basename, Ext: $extension"
}

🔧 Common Bashism Abuses

Array Operations

Using Bash arrays in portable scripts.

 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
# ❌ Anti-pattern: Bash array operations
manage_user_list() {
    local users=(alice bob charlie)
    users+=("david")  # Append to array

    echo "Users: ${users[*]}"           # All elements
    echo "Count: ${#users[@]}"          # Array length
    echo "First: ${users[0]}"           # First element
}

# Problems:
# - Arrays not available in POSIX sh
# - += operator Bash-specific
# - [*] and [@] expansions Bash-specific

# ✅ Portable alternative: Delimited strings
manage_user_list_portable() {
    # Use delimited string instead of array
    local users="alice,bob,charlie"
    users="${users},david"  # Append

    # Process each user
    IFS=',' read -ra user_array <<< "$users"
    for user in "${user_array[@]}"; do
        echo "User: $user"
    done

    # Count users
    local count=0
    IFS=',' read -ra user_array <<< "$users"
    count=${#user_array[@]}
    echo "Count: $count"
}

Advanced Test Conditions

Using Bash-specific test syntax.

 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
# ❌ Anti-pattern: Extended test conditions
validate_input() {
    local input="$1"

    # Bash-specific regex matching
    if [[ "$input" =~ ^[0-9]+$ ]]; then
        echo "Valid number"
    fi

    # Bash-specific string operations
    if [[ "$input" == foo* ]]; then
        echo "Starts with foo"
    fi

    # Bash-specific logical operators
    if [[ -n "$input" && "$input" != "test" ]]; then
        echo "Valid input"
    fi
}

# Problems:
# - [[ ]] syntax not POSIX compliant
# - =~ regex operator Bash-specific
# - == pattern matching Bash-specific

# ✅ Portable alternatives
validate_input_portable() {
    local input="$1"

    # Portable regex matching
    if echo "$input" | grep -E '^[0-9]+$' >/dev/null 2>&1; then
        echo "Valid number"
    fi

    # Portable prefix matching
    case "$input" in
        foo*)
            echo "Starts with foo"
            ;;
    esac

    # Portable logical conditions
    if [ -n "$input" ] && [ "$input" != "test" ]; then
        echo "Valid input"
    fi
}

🎨 Advanced Bashism Issues

Process Substitution

Using Bash process substitution in portable scripts.

 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
# ❌ Anti-pattern: Process substitution
compare_files() {
    local file1="$1"
    local file2="$2"

    # Bash process substitution - not POSIX
    diff <(sort "$file1") <(sort "$file2")
}

# Problems:
# - Process substitution not available in POSIX sh
# - Fails on dash and other minimal shells
# - Not supported in all Bash environments

# ✅ Portable alternative: Temporary files
compare_files_portable() {
    local file1="$1"
    local file2="$2"
    local temp1 temp2

    # Create temporary files
    temp1=$(mktemp) || { echo "Failed to create temp file" >&2; return 1; }
    temp2=$(mktemp) || { echo "Failed to create temp file" >&2; return 1; }

    # Clean up on exit
    trap 'rm -f "$temp1" "$temp2"' EXIT

    # Sort files to temporary locations
    sort "$file1" > "$temp1"
    sort "$file2" > "$temp2"

    # Compare sorted files
    diff "$temp1" "$temp2"
}

Advanced Parameter Expansion

Using Bash 4+ parameter expansion features.

 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
# ❌ Anti-pattern: Advanced parameter expansion
process_variables() {
    local input="$1"

    # Bash 4+ features - not widely available
    local upper="${input^^}"        # Uppercase all
    local substr="${input:2:5}"     # Substring
    local replace="${input/foo/bar}" # Replace first
    local strip="${input#prefix}"   # Remove prefix

    echo "Processed: $upper, $substr, $replace, $strip"
}

# Problems:
# - Not available in older Bash versions
# - Not POSIX compliant
# - Fails on minimal shells

# ✅ Portable alternatives
process_variables_portable() {
    local input="$1"

    # Portable uppercase conversion
    local upper
    upper=$(echo "$input" | tr '[:lower:]' '[:upper:]')

    # Portable substring (characters 3-7)
    local substr
    substr=$(echo "$input" | cut -c3-7)

    # Portable string replacement
    local replace
    replace=$(echo "$input" | sed 's/foo/bar/')

    # Portable prefix removal
    local strip
    strip=$(echo "$input" | sed 's/^prefix//')

    echo "Processed: $upper, $substr, $replace, $strip"
}

🧾 Summary of Issues

Common Non-Portable Bashisms

Bashism Issue Portable Alternative
Arrays Not POSIX Delimited strings
[[ ]] Bash-specific [ ] with proper quoting
=~ Regex matching grep -E
Process substitution Bash-only Temporary files
Parameter expansion Version-dependent External tools
(( )) Arithmetic evaluation expr or awk

Red Flags to Avoid

🚩 ${array[@]} syntax 🚩 [[ condition ]] double brackets 🚩 <(command) process substitution 🚩 ${var/pattern/replacement} extended expansion 🚩 ${var,,} case conversion 🚩 function name() Bash function syntax


🧠 Prevention Strategies

Shell Compatibility 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Test script compatibility across shells
test_shell_compatibility() {
    local script="$1"

    local shells=(
        "/bin/sh"
        "/bin/bash"
        "/bin/dash"
        "/bin/ksh"
        "/bin/zsh"
    )

    echo "Testing compatibility for: $script"
    echo "====================================="

    for shell in "${shells[@]}"; do
        if [ -x "$shell" ]; then
            echo -n "Testing with $shell: "
            if "$shell" -n "$script" 2>/dev/null; then
                echo "PASS"
            else
                echo "FAIL"
            fi
        else
            echo "Shell not available: $shell"
        fi
    done
}

# Use shellcheck for Bashism detection
check_for_bashisms() {
    local script="$1"

    if command -v shellcheck >/dev/null 2>&1; then
        echo "Checking for Bashisms..."
        shellcheck -s sh "$script"  # Check as POSIX shell
    else
        echo "shellcheck not available for Bashism checking"
    fi
}

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
#!/bin/sh
# portable-script.sh - Template avoiding Bashisms

# Use POSIX-compliant function declaration
main() {
    # Use portable variable assignment
    local_var="value"

    # Use portable conditionals
    if [ -n "$local_var" ]; then
        echo "Variable set: $local_var"
    fi

    # Use portable loops
    set -- item1 item2 item3  # Set positional parameters
    for item in "$@"; do
        echo "Processing: $item"
    done

    # Use portable parameter expansion alternatives
    if [ "${#local_var}" -gt 0 ]; then
        echo "Non-empty string"
    fi

    # Use portable arithmetic
    count=5
    count=$(expr "$count" + 1)
    echo "Count: $count"

    # Use portable command substitution
    current_date=$(date)
    echo "Date: $current_date"
}

# Portable error handling
set -e  # Exit on error (careful with this in some shells)

# Run main function
main "$@"

Compatibility Functions Library

 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
# lib/portable-compat.sh - Portable alternatives to Bashisms

# Portable array-like operations using delimited strings
string_array_append() {
    local array_var="$1"
    local new_element="$2"
    local delimiter="${3:-,}"

    # Get current value
    local current_value
    eval "current_value=\$$array_var"

    # Append new element
    if [ -n "$current_value" ]; then
        eval "$array_var=\"\${current_value}${delimiter}${new_element}\""
    else
        eval "$array_var=\"$new_element\""
    fi
}

# Portable parameter expansion alternatives
string_remove_prefix() {
    local string="$1"
    local prefix="$2"

    case "$string" in
        "$prefix"*)
            echo "${string#$prefix}"
            ;;
        *)
            echo "$string"
            ;;
    esac
}

# Portable case conversion
string_to_lowercase() {
    echo "$1" | tr '[:upper:]' '[:lower:]'
}

string_to_uppercase() {
    echo "$1" | tr '[:lower:]' '[:upper:]'
}

# Portable substring
string_substring() {
    local string="$1"
    local start="$2"
    local length="$3"

    echo "$string" | cut -c"${start}-$((start + length - 1))"
}

🧾 See Also