Claude-skill-registry bash-portability
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bash-portability" ~/.claude/skills/majiayu000-claude-skill-registry-bash-portability && rm -rf "$T"
skills/data/bash-portability/SKILL.mdBash 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#!/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#!/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
# POSIX - use [ ] with proper quoting if [ -f "$file" ]; then echo "File exists" fi # String comparison if [ "$var" = "value" ]; then echo "Match" fi # Numeric comparison if [ "$num" -gt 10 ]; then echo "Greater" fi # Compound conditions if [ -f "$file" ] && [ -r "$file" ]; then echo "Readable file" fi
Case Conversion (POSIX)
# Lowercase lower=$(echo "$string" | tr '[:upper:]' '[:lower:]') # Uppercase upper=$(echo "$string" | tr '[:lower:]' '[:upper:]')
Substring Operations (POSIX)
# Get substring - use expr or cut substr=$(expr "$string" : '.\{3\}\(.\{5\}\)') # chars 4-8 substr=$(echo "$string" | cut -c4-8) # String length length=$(expr length "$string") length=${#string} # This is actually POSIX
Reading Files (POSIX)
# Line by line while IFS= read -r line; do echo "$line" done < "$file" # Read entire file (without cat) content=$(cat "$file") # cat is POSIX
Command Substitution
# Modern syntax (preferred even in POSIX) result=$(command) # Legacy syntax (avoid) result=`command` # Nested (why modern is better) result=$(echo $(date)) # Clear result=`echo \`date\`` # Escape nightmare
Bash-Specific Features Worth Using
When portability isn't required, these bash features improve code quality:
Extended Test [[ ]]
[[ ]]# Pattern matching [[ "$file" == *.txt ]] # Regex matching [[ "$input" =~ ^[0-9]+$ ]] # No word splitting worries [[ -f $file ]] # Quotes optional (but still recommended) # Logical operators inside [[ -f "$file" && -r "$file" ]]
Arrays
# Indexed arrays declare -a files=() files+=("one.txt") files+=("two.txt") for f in "${files[@]}"; do process "$f" done # Associative arrays (Bash 4+) declare -A config config[host]="localhost" config[port]="8080"
Parameter Expansion
# Default value "${var:-default}" # Case conversion (Bash 4+) "${var,,}" # lowercase "${var^^}" # uppercase # Substring "${var:0:10}" # first 10 chars "${var: -5}" # last 5 chars # Search/replace "${var//old/new}"
Here Strings
# Bash read -r var <<< "input string" # POSIX equivalent var=$(echo "input string")
Process Substitution
# Bash - compare two command outputs diff <(sort file1) <(sort file2) # POSIX equivalent (with temp files) sort file1 > /tmp/sorted1 sort file2 > /tmp/sorted2 diff /tmp/sorted1 /tmp/sorted2
Detecting Shell Type
# Check if running in bash if [ -n "${BASH_VERSION:-}" ]; then echo "Running in Bash" fi # Check bash version for features if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then echo "Bash 4+ available" fi # Generic shell detection case "${SHELL##*/}" in bash) echo "bash" ;; zsh) echo "zsh" ;; *) echo "other" ;; esac
Portable Utility Functions
# Command existence check (POSIX) command_exists() { command -v "$1" >/dev/null 2>&1 } # Portable dirname get_dirname() { case "$1" in */*) echo "${1%/*}" ;; *) echo "." ;; esac } # Portable basename get_basename() { case "$1" in */*) echo "${1##*/}" ;; *) echo "$1" ;; esac } # Portable absolute path 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
# Problematic - behavior varies echo -n "no newline" echo -e "with\ttabs" # Portable printf '%s' "no newline" printf 'with\ttabs\n'
Variable Assignment
# Works everywhere var="value" # May fail on some shells var = "value" # Spaces around = are wrong
Export with Assignment
# POSIX - separate commands var="value" export var # Bash/modern - combined (works most places) export var="value"
Array-like Operations Without Arrays
# Use positional parameters set -- "item1" "item2" "item3" for item in "$@"; do echo "$item" done # Or IFS-based splitting items="item1:item2:item3" IFS=':' read -r item1 item2 item3 <<EOF $items EOF