🐚 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