shell-scripting-standards

Essential patterns for reliable shell scripts - portable shebangs, error handling, quoting rules, functions, testing with Bats, and ShellCheck integration.

$ Installieren

git clone https://github.com/williamzujkowski/standards /tmp/standards && cp -r /tmp/standards/skills/coding-standards/shell ~/.claude/skills/standards

// tip: Run this command in your terminal to install the skill


name: shell-scripting-standards description: Essential patterns for reliable shell scripts - portable shebangs, error handling, quoting rules, functions, testing with Bats, and ShellCheck integration.

Shell Scripting Standards

Industry-standard shell scripting practices for writing reliable, maintainable, and secure bash scripts.

Level 1: Quick Reference

Portable Shebang

#!/usr/bin/env bash
# Finds bash in PATH - works across systems
# Never use #!/bin/bash (non-portable)

Essential Error Handling

#!/usr/bin/env bash
set -euo pipefail
# -e: Exit on error | -u: Exit on undefined | -o pipefail: Exit on pipe failure

trap cleanup EXIT ERR
cleanup() {
  local exit_code=$?
  rm -f "$temp_file"
  exit "$exit_code"
}

Variable Quoting Rules

# ALWAYS quote variables to prevent word splitting
echo "$variable"           # Correct
echo $variable             # Wrong - word splitting

# Arrays need different quoting
files=("file 1.txt" "file 2.txt")
for file in "${files[@]}"; do
  echo "$file"
done

# Command substitution
current_dir="$(pwd)"       # Modern (correct)
current_dir=`pwd`          # Deprecated (avoid)

Function Pattern

function_name() {
  local param1="$1"
  local param2="${2:-default}"

  if [[ -z "$param1" ]]; then
    echo "Error: param1 required" >&2
    return 1
  fi

  echo "Result"
  return 0
}

Argument Parsing

usage() {
  cat << USAGE
Usage: ${0##*/} [-v] [-o OUTPUT] FILE
Options:
  -v          Verbose mode
  -o OUTPUT   Output file
  -h          Show this help
USAGE
}

verbose=false
output=""

while getopts "vo:h" opt; do
  case "$opt" in
    v) verbose=true ;;
    o) output="$OPTARG" ;;
    h) usage; exit 0 ;;
    *) usage >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))

file="${1:?Error: FILE required}"

Logging Pattern

readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

File Operations

# Safe file checks
[[ -f "$file" ]]        # File exists and is regular
[[ -d "$dir" ]]         # Directory exists
[[ -r "$file" ]]        # File is readable

# Safe temp files
temp_file=$(mktemp)
temp_dir=$(mktemp -d)

# Read file line by line
while IFS= read -r line; do
  echo "Line: $line"
done < "$file"

Common Patterns

# Check if command exists
command_exists() { command -v "$1" >/dev/null 2>&1; }

# Retry logic
retry() {
  local max=3 attempt=1
  while ((attempt <= max)); do
    "$@" && return 0
    echo "Attempt $attempt failed" >&2
    ((attempt++)); sleep 2
  done
  return 1
}

# Confirm prompt
confirm() {
  read -rp "${1:-Are you sure?} [y/N] " response
  [[ "$response" =~ ^[Yy]$ ]]
}

ShellCheck Integration

# Install: apt-get install shellcheck | brew install shellcheck
shellcheck script.sh

# Disable specific warnings (sparingly)
# shellcheck disable=SC2086
echo $unquoted_on_purpose

# CI/CD integration
find . -name "*.sh" -exec shellcheck {} +

Level 2: Implementation Guide

Script Structure

#!/usr/bin/env bash
#
# Script Name: backup-manager.sh
# Description: Manages automated backups with rotation
# Version: 1.0.0
#

set -euo pipefail

readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="1.0.0"

readonly EXIT_SUCCESS=0 EXIT_ERROR=1 EXIT_USAGE=2

main() {
  parse_arguments "$@"
  validate_dependencies
  perform_backup
  log_info "Backup completed successfully"
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi

Error Handling and Cleanup

error_handler() {
  local line_no=$1 error_code=$2
  log_error "Error on line $line_no: exit code $error_code"
  cleanup
  exit "$error_code"
}

trap 'error_handler ${LINENO} $?' ERR

cleanup() {
  local exit_code=$?
  [[ -n "${temp_dir:-}" && -d "$temp_dir" ]] && rm -rf "$temp_dir"
  [[ -n "${lock_file:-}" && -f "$lock_file" ]] && rm -f "$lock_file"
  return "$exit_code"
}

trap cleanup EXIT INT TERM

# Lock file pattern (prevent concurrent runs)
acquire_lock() {
  local lock_file="$1" lock_fd=200
  eval "exec $lock_fd>\"$lock_file\""
  flock -n "$lock_fd" || { log_error "Another instance running"; exit 1; }
}

Logging System

readonly LOG_LEVEL_DEBUG=0 LOG_LEVEL_INFO=1 LOG_LEVEL_WARN=2 LOG_LEVEL_ERROR=3
log_level=$LOG_LEVEL_INFO

log_message() {
  local level=$1 color=$2 prefix=$3; shift 3
  if ((level >= log_level)); then
    echo -e "${color}[${prefix}]${NC} $*" >&2
    [[ -w "$(dirname "${LOG_FILE:-/tmp/log}")" ]] && \
      echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${prefix}] $*" >> "${LOG_FILE:-/tmp/log}"
  fi
}

log_debug() { log_message $LOG_LEVEL_DEBUG "$BLUE" "DEBUG" "$@"; }
log_info()  { log_message $LOG_LEVEL_INFO "$GREEN" "INFO" "$@"; }
log_warn()  { log_message $LOG_LEVEL_WARN "$YELLOW" "WARN" "$@"; }
log_error() { log_message $LOG_LEVEL_ERROR "$RED" "ERROR" "$@"; }

Input Validation

validate_file() {
  local file="$1" desc="${2:-File}"
  [[ -z "$file" ]] && { log_error "$desc path is empty"; return 1; }
  [[ ! -f "$file" ]] && { log_error "$desc does not exist: $file"; return 1; }
  [[ ! -r "$file" ]] && { log_error "$desc is not readable: $file"; return 1; }
  return 0
}

validate_directory() {
  local dir="$1" desc="${2:-Directory}" writable="${3:-false}"
  [[ -z "$dir" ]] && { log_error "$desc path is empty"; return 1; }
  [[ ! -d "$dir" ]] && { log_error "$desc does not exist: $dir"; return 1; }
  [[ "$writable" == "true" && ! -w "$dir" ]] && { log_error "$desc not writable: $dir"; return 1; }
  return 0
}

validate_number() {
  local val="$1" desc="${2:-Value}" min="${3:-}" max="${4:-}"
  [[ ! "$val" =~ ^[0-9]+$ ]] && { log_error "$desc must be a number: $val"; return 1; }
  [[ -n "$min" && "$val" -lt "$min" ]] && { log_error "$desc must be >= $min"; return 1; }
  [[ -n "$max" && "$val" -gt "$max" ]] && { log_error "$desc must be <= $max"; return 1; }
  return 0
}

Dependency Management

validate_dependencies() {
  local missing=() required=(rsync gzip tar date mktemp)
  for cmd in "${required[@]}"; do
    command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
  done
  if ((${#missing[@]} > 0)); then
    log_error "Missing required commands: ${missing[*]}"
    exit 1
  fi
}

check_optional_tool() {
  local tool="$1" fallback="$2"
  command -v "$tool" >/dev/null 2>&1 && echo "$tool" || echo "$fallback"
}

Testing with Bats

# test-script.bats
#!/usr/bin/env bats

setup() {
  TEST_DIR="$(mktemp -d)"
  source ./script.sh
}

teardown() {
  rm -rf "$TEST_DIR"
}

@test "validate_file succeeds for existing file" {
  touch "$TEST_DIR/test.txt"
  run validate_file "$TEST_DIR/test.txt"
  [ "$status" -eq 0 ]
}

@test "validate_file fails for missing file" {
  run validate_file "$TEST_DIR/missing.txt"
  [ "$status" -eq 1 ]
  [[ "$output" =~ "does not exist" ]]
}

Security Best Practices

# 1. Never eval user input
case "$user_input" in
  start) start_service ;;
  stop) stop_service ;;
  *) log_error "Invalid command" ;;
esac

# 2. Sanitize paths
sanitize_path() {
  local path allowed_dir="/home/user/allowed"
  path="$(realpath -m "$1")"
  [[ "$path" != "$allowed_dir"* ]] && { log_error "Path outside allowed"; return 1; }
  echo "$path"
}

# 3. Secure temporary files
temp_file="$(mktemp)" && chmod 600 "$temp_file"

# 4. Avoid command injection - use arrays
local -a files
mapfile -t files < <(find . -name "${pattern}*.txt")

# 5. Validate environment
[[ "$PATH" =~ (^|:)\.(:|$) ]] && { log_error "PATH contains ."; return 1; }

# 6. Secure password handling
read_password() {
  read -rsp "Enter password: " password; echo >&2
  some_command <<< "$password"
  unset password
}

Performance Optimization

# Use built-ins instead of external commands
echo "${file%/*}"           # Fast (built-in)
dirname "$file"             # Slow (spawns process)

# Avoid unnecessary subshells
count=$(wc -l < file.txt)   # Fast
count=$(cat file.txt | wc -l) # Slow

# Use bash built-ins for string operations
result="${item^^}"          # Uppercase (Bash 4+)

# Batch operations
cp *.txt /dest/             # Single operation

Cross-Platform Compatibility

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

portable_sed() {
  [[ "$(detect_os)" == "macos" ]] && sed -i '' "$@" || sed -i "$@"
}

Quick ShellCheck Reference

CodeIssueFix
SC2086Unquoted variableecho "$var"
SC2006Backticks deprecated$(cmd)
SC2155Declare and assign separatelylocal var; var=$(cmd)
SC2034Unused variablePrefix with _
SC2064Unquoted traptrap 'cleanup' EXIT

Level 3: Deep Dive Resources

Documentation & Guides

Testing Frameworks

Static Analysis

Extended Reference

See REFERENCE.md for:

  • Complete script templates
  • Advanced argument parsing with long options
  • JSON parsing with jq
  • API request patterns with curl
  • Configuration management
  • Signal handling
  • Process management
  • Debugging techniques

Bundled Resources

  1. config/.shellcheckrc - ShellCheck configuration
  2. templates/script-template.sh - Production script template
  3. templates/library.sh - Reusable function library
  4. templates/test-template.bats - Bats testing template
  5. scripts/install-shell-tools.sh - Automated tooling setup
  6. resources/shell-best-practices.md - Comprehensive reference

Quick Wins

  1. Add shebang: #!/usr/bin/env bash
  2. Add error handling: set -euo pipefail
  3. Add cleanup trap: trap cleanup EXIT
  4. Quote all variables: "$var" not $var
  5. Use [[ instead of [ for tests
  6. Replace backticks with $()
  7. Run ShellCheck and fix warnings

Last Updated: 2025-12-30 Skill Version: 2.0.0