Documentation/Jutsu/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.



Overview

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

Shebang Selection

Bash Scripts

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

POSIX Shell Scripts

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

Bash vs POSIX Differences

Arrays (Bash only)

# 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 - extended test
if [[ "$var" == "value" ]]; then
    echo "match"
fi

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

String Operations

# 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 - 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

# 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

# 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

# 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

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

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

Shell Detection

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

Portable Patterns

Reading Files

# 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

# 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

# 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

# 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

# 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

#!/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