Documentation/Validation/Shellcheck/ skills /shell-portability

📖 shell-portability

Use when writing shell scripts that need to run across different systems, shells, or environments. Covers POSIX compatibility and platform differences.

Allowed Tools: Read, Write, Edit, Bash, Grep, Glob


Overview

Techniques for writing shell scripts that work across different platforms and environments.

Shebang Selection

Bash Scripts

bash
#!/usr/bin/env bash
# Most portable for bash scripts
# Works on Linux, macOS, BSD

POSIX Shell Scripts

sh
#!/bin/sh
# For maximum portability
# Use only POSIX features

Bash vs POSIX Differences

Arrays (Bash only)

bash
# Bash - arrays available
declare -a items=("one" "two" "three")
for item in "${items[@]}"; do
    echo "$item"
done

# POSIX - use positional parameters or space-separated strings
set -- one two three
for item in "$@"; do
    echo "$item"
done

Test Syntax

bash
# Bash - extended test
if [[ "$var" == "value" ]]; then
    echo "match"
fi

# POSIX - basic test
if [ "$var" = "value" ]; then
    echo "match"
fi

String Operations

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

# POSIX - use case or external tools
case "$input" in
    *[!0-9]*|'') echo "not numeric" ;;
    *) echo "numeric" ;;
esac

Arithmetic

bash
# Bash - arithmetic expansion
(( count++ ))
if (( count > 10 )); then
    echo "greater"
fi

# POSIX - expr or arithmetic expansion
count=$((count + 1))
if [ "$count" -gt 10 ]; then
    echo "greater"
fi

Platform Differences

macOS vs Linux

bash
# Date command differences
# GNU (Linux)
date -d "yesterday" +%Y-%m-%d

# BSD (macOS)
date -v-1d +%Y-%m-%d

# Portable approach
if date --version >/dev/null 2>&1; then
    # GNU date
    yesterday=$(date -d "yesterday" +%Y-%m-%d)
else
    # BSD date
    yesterday=$(date -v-1d +%Y-%m-%d)
fi

sed Differences

bash
# GNU sed - in-place edit
sed -i 's/old/new/g' file.txt

# BSD sed - requires backup extension
sed -i '' 's/old/new/g' file.txt

# Portable approach
sed 's/old/new/g' file.txt > file.txt.tmp && mv file.txt.tmp file.txt

# Or use a function
sed_inplace() {
    if sed --version >/dev/null 2>&1; then
        sed -i "$@"
    else
        sed -i '' "$@"
    fi
}

readlink Differences

bash
# GNU readlink
readlink -f /path/to/link

# BSD/macOS - no -f option by default
# Use greadlink from coreutils or:
resolve_path() {
    local path="$1"
    if command -v greadlink >/dev/null 2>&1; then
        greadlink -f "$path"
    elif command -v realpath >/dev/null 2>&1; then
        realpath "$path"
    else
        # Fallback
        cd "$(dirname "$path")" && pwd -P
    fi
}

Detecting Environment

Operating System

bash
detect_os() {
    case "$(uname -s)" in
        Linux*)  echo "linux" ;;
        Darwin*) echo "macos" ;;
        MINGW*|CYGWIN*|MSYS*) echo "windows" ;;
        FreeBSD*) echo "freebsd" ;;
        *) echo "unknown" ;;
    esac
}

OS=$(detect_os)
case "$OS" in
    linux)  INSTALL_CMD="apt-get install" ;;
    macos)  INSTALL_CMD="brew install" ;;
esac

Architecture

bash
detect_arch() {
    case "$(uname -m)" in
        x86_64|amd64) echo "amd64" ;;
        aarch64|arm64) echo "arm64" ;;
        armv7l) echo "arm" ;;
        *) echo "unknown" ;;
    esac
}

Shell Detection

bash
detect_shell() {
    if [ -n "$BASH_VERSION" ]; then
        echo "bash"
    elif [ -n "$ZSH_VERSION" ]; then
        echo "zsh"
    else
        echo "sh"
    fi
}

Portable Patterns

Reading Files

bash
# Portable line reading
while IFS= read -r line || [ -n "$line" ]; do
    echo "$line"
done < "$file"
# The || [ -n "$line" ] handles files without trailing newline

Temporary Files

bash
# POSIX-compatible temp file
make_temp() {
    if command -v mktemp >/dev/null 2>&1; then
        mktemp
    else
        # Fallback
        local tmp="/tmp/tmp.$$.$RANDOM"
        touch "$tmp" && echo "$tmp"
    fi
}

Command Existence Check

bash
# POSIX-compatible command check
has_command() {
    command -v "$1" >/dev/null 2>&1
}

# Usage
if has_command curl; then
    curl "$url"
elif has_command wget; then
    wget -O- "$url"
else
    echo "No HTTP client available" >&2
    exit 1
fi

String Contains

bash
# POSIX-compatible string contains
contains() {
    case "$1" in
        *"$2"*) return 0 ;;
        *) return 1 ;;
    esac
}

# Usage
if contains "$PATH" "/usr/local/bin"; then
    echo "Found in PATH"
fi

ShellCheck Compatibility

Disabling Warnings for Portability

bash
# When intentionally using non-portable features
# shellcheck disable=SC2039  # Bash-specific feature
if [[ "$var" =~ regex ]]; then
    :
fi

# Document why
# shellcheck disable=SC2016  # Intentionally not expanding
echo 'Use $HOME for home directory'

Testing Multiple Shells

bash
#!/usr/bin/env bash
# shellcheck shell=bash

# Or for POSIX:
#!/bin/sh
# shellcheck shell=sh

Best Practices

  1. Choose the right shebang for your needs
  2. Document shell requirements in README
  3. Use #!/usr/bin/env bash for bash scripts
  4. Test on multiple platforms when possible
  5. Prefer POSIX features when portability matters
  6. Abstract platform differences into functions
  7. Use ShellCheck with appropriate shell directive
  8. Provide fallbacks for platform-specific commands