| name | bash-portability |
| description | This skill should be used when the user asks about "POSIX compatibility", "portable shell scripts", "cross-shell compatibility", "bashisms", "shebang selection", or mentions writing scripts that work on different shells (bash, sh, dash, zsh) or different systems. |
Bash Portability
Guidance for writing portable POSIX-compatible scripts and understanding when to leverage bash-specific features.
Shebang Selection
Use #!/usr/bin/env bash for Bash Scripts
#!/usr/bin/env bash
Why: Searches PATH for bash, works across systems where bash may be in different locations.
Use #!/bin/sh for POSIX Scripts
#!/bin/sh
Why: Maximum portability when bash features aren't needed. On many systems, /bin/sh is dash or another POSIX shell.
Direct Path When Required
#!/bin/bash
Use only when: System requirements guarantee bash location, or security policy requires absolute paths.
POSIX vs Bash Feature Matrix
| Feature | POSIX | Bash | Recommendation |
| -------------- | ------- | ---- | ------------------------------ | ---- |
| [[ ]] | No | Yes | Use [ ] for POSIX |
| (( )) | No | Yes | Use [ ] with -eq etc. |
| Arrays | No | Yes | Use positional params or files |
| local | Partial | Yes | Generally safe |
| ${var,,} | No | 4+ | Use tr for POSIX |
| <<< | No | Yes | Use echo | cmd |
| =~ regex | No | Yes | Use grep or expr |
| source | No | Yes | Use . (dot) command |
| function f() | No | Yes | Use f() only |
| $'...' | No | Yes | Use printf |
| {1..10} | No | Yes | Use seq or while loop |
POSIX-Compatible Patterns
Conditionals
if [ -f "$file" ]; then
echo "File exists"
fi
if [ "$var" = "value" ]; then
echo "Match"
fi
if [ "$num" -gt 10 ]; then
echo "Greater"
fi
if [ -f "$file" ] && [ -r "$file" ]; then
echo "Readable file"
fi
Case Conversion (POSIX)
lower=$(echo "$string" | tr '[:upper:]' '[:lower:]')
upper=$(echo "$string" | tr '[:lower:]' '[:upper:]')
Substring Operations (POSIX)
substr=$(expr "$string" : '.\{3\}\(.\{5\}\)')
substr=$(echo "$string" | cut -c4-8)
length=$(expr length "$string")
length=${#string}
Reading Files (POSIX)
while IFS= read -r line; do
echo "$line"
done < "$file"
content=$(cat "$file")
Command Substitution
result=$(command)
result=`command`
result=$(echo $(date))
result=`echo \`date\``
Bash-Specific Features Worth Using
When portability isn't required, these bash features improve code quality:
Extended Test [[ ]]
[[ "$file" == *.txt ]]
[[ "$input" =~ ^[0-9]+$ ]]
[[ -f $file ]]
[[ -f "$file" && -r "$file" ]]
Arrays
declare -a files=()
files+=("one.txt")
files+=("two.txt")
for f in "${files[@]}"; do
process "$f"
done
declare -A config
config[host]="localhost"
config[port]="8080"
Parameter Expansion
"${var:-default}"
"${var,,}"
"${var^^}"
"${var:0:10}"
"${var: -5}"
"${var//old/new}"
Here Strings
read -r var <<< "input string"
var=$(echo "input string")
Process Substitution
diff <(sort file1) <(sort file2)
sort file1 > /tmp/sorted1
sort file2 > /tmp/sorted2
diff /tmp/sorted1 /tmp/sorted2
Detecting Shell Type
if [ -n "${BASH_VERSION:-}" ]; then
echo "Running in Bash"
fi
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
echo "Bash 4+ available"
fi
case "${SHELL##*/}" in
bash) echo "bash" ;;
zsh) echo "zsh" ;;
*) echo "other" ;;
esac
Portable Utility Functions
command_exists() {
command -v "$1" >/dev/null 2>&1
}
get_dirname() {
case "$1" in
*/*) echo "${1%/*}" ;;
*) echo "." ;;
esac
}
get_basename() {
case "$1" in
*/*) echo "${1##*/}" ;;
*) echo "$1" ;;
esac
}
get_abs_path() {
(cd "$(dirname "$1")" && printf '%s/%s' "$(pwd)" "$(basename "$1")")
}
Portability Decision Guide
Use POSIX when:
- Script runs on minimal systems (containers, embedded)
- Target includes dash, ash, or busybox sh
- Maximum compatibility is required
- Script is part of system initialization
Use Bash when:
- Target systems guaranteed to have bash
- Need arrays, associative arrays, or regex
- Complex string manipulation required
- Code clarity significantly improved
- Interactive features needed
Common Portability Pitfalls
echo vs printf
echo -n "no newline"
echo -e "with\ttabs"
printf '%s' "no newline"
printf 'with\ttabs\n'
Variable Assignment
var="value"
var = "value"
Export with Assignment
var="value"
export var
export var="value"
Array-like Operations Without Arrays
set -- "item1" "item2" "item3"
for item in "$@"; do
echo "$item"
done
items="item1:item2:item3"
IFS=':' read -r item1 item2 item3 <<EOF
$items
EOF