FeaturesSkillsShell/Bash

Metadata

FieldValue
Typecontext
Applies tobash, sh, shell, zsh, shellcheck, bats
File extensions.sh, .bash

Included Files

PathDescription
references/Reference documentation for Claude

Shell/Bash Coding Standards

Core Principles

  1. Simplicity: Simple, understandable scripts
  2. Readability: Readability over cleverness
  3. Maintainability: Scripts that are easy to maintain
  4. Testability: Scripts that are easy to test
  5. DRY: Don’t Repeat Yourself - but don’t overdo it
  6. Defensiveness: Fail early, fail loudly

General Rules

  • Defensive Header: Always use set -euo pipefail
  • Quote Variables: Always quote variables "$var"
  • Descriptive Names: Meaningful names for variables and functions
  • Minimal Changes: Only change relevant code parts
  • No Over-Engineering: No unnecessary complexity
  • ShellCheck Clean: All scripts must pass ShellCheck

Naming Conventions

ElementConventionExample
Variablessnake_caseuser_name, file_count
Functionssnake_caseget_user_by_id, validate_input
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, DEFAULT_TIMEOUT
Fileskebab-case or snake_casedeploy-app.sh, run_tests.sh
Environment VarsUPPER_SNAKE_CASEAPI_URL, DATABASE_HOST

Script Template

#!/bin/bash
set -euo pipefail
 
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
 
# Cleanup on exit
cleanup() {
    rm -f "$SCRIPT_DIR"/*.tmp 2>/dev/null || true
}
trap cleanup EXIT
 
# Error handler
error_handler() {
    echo "Error on line $1" >&2
    exit 1
}
trap 'error_handler $LINENO' ERR
 
main() {
    # Script logic here
    echo "Running $SCRIPT_NAME"
}
 
main "$@"

Defensive Scripting

# REQUIRED: Always start with this
set -euo pipefail
# -e: Exit on error
# -u: Error on undefined variables
# -o pipefail: Pipe fails if any command fails
 
# RECOMMENDED: Safer IFS
IFS=$'\n\t'
 
# REQUIRED: Quote all variables
echo "$var"                     # Good
echo $var                       # Bad - word splitting
 
# REQUIRED: Use [[ ]] for conditionals (Bash)
if [[ -f "$file" ]]; then       # Good
if [ -f "$file" ]; then         # POSIX only

Parameter Expansion

# Defaults and validation
${var:-default}             # Use default if unset
${var:=default}             # Assign default if unset
${var:?error message}       # Error if unset
 
# String manipulation
${var#pattern}              # Remove prefix (shortest)
${var##pattern}             # Remove prefix (longest)
${var%pattern}              # Remove suffix (shortest)
${var%%pattern}             # Remove suffix (longest)
${var/old/new}              # Replace first
${var//old/new}             # Replace all
${#var}                     # Length
 
# Examples
file="document.txt"
echo "${file%%.*}"          # "document" (remove extension)
echo "${file##*.}"          # "txt" (get extension)

Functions

# REQUIRED: Use local variables
get_user_name() {
    local user_id=$1
    local name
    name=$(grep "^${user_id}:" /etc/passwd | cut -d: -f5)
    echo "$name"
}
 
# Return values via stdout
result=$(get_user_name "1000")
 
# Return status codes
validate_file() {
    local file=$1
    if [[ ! -f "$file" ]]; then
        echo "Error: File not found: $file" >&2
        return 1
    fi
    return 0
}
 
if validate_file "$input_file"; then
    process_file "$input_file"
fi

Arrays

# Indexed arrays
files=("file1.txt" "file2.txt" "file3.txt")
echo "${files[0]}"          # First element
echo "${files[@]}"          # All elements
echo "${#files[@]}"         # Array length
 
# Iterate safely
for file in "${files[@]}"; do
    echo "Processing: $file"
done
 
# Associative arrays (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}:${config[port]}"

File Operations

# Read file line by line
while IFS= read -r line; do
    echo "Line: $line"
done < "input.txt"
 
# Read into array
mapfile -t lines < "input.txt"
 
# Write to file (heredoc)
cat > output.txt <<EOF
Line 1
Line 2
EOF
 
# Temp files with cleanup
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT

Error Handling

# Trap for cleanup
cleanup() {
    echo "Cleaning up..."
    rm -f "$temp_file"
}
trap cleanup EXIT
 
# Trap for errors
error_handler() {
    local line=$1
    echo "Error occurred on line $line" >&2
}
trap 'error_handler $LINENO' ERR
 
# Check command exists
if ! command -v python3 &>/dev/null; then
    echo "Error: python3 not found" >&2
    exit 1
fi
 
# Conditional execution
command1 && command2        # Run command2 only if command1 succeeds
command1 || command2        # Run command2 only if command1 fails

Argument Parsing with getopts

usage() {
    echo "Usage: $0 [-v] [-o output] [-h]"
    echo "  -v        Verbose mode"
    echo "  -o FILE   Output file"
    echo "  -h        Show help"
    exit 1
}
 
verbose=false
output_file=""
 
while getopts "vo:h" opt; do
    case $opt in
        v) verbose=true ;;
        o) output_file="$OPTARG" ;;
        h) usage ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))
 
# Remaining args in $@

Logging

log() {
    local level=$1
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
 
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
 
# Usage
log_info "Starting process"
log_error "Failed to connect"

Debugging

# Enable debugging
set -x                      # Print commands
PS4='+ ${BASH_SOURCE}:${LINENO}: '  # Better debug output
 
# Debug specific section
set -x
# code to debug
set +x
 
# Run script with debug
bash -x script.sh
bash -n script.sh           # Syntax check only

Common Patterns

# Check if root
if [[ $EUID -ne 0 ]]; then
    echo "This script must be run as root" >&2
    exit 1
fi
 
# Safe directory change
cd "$target_dir" || exit 1
 
# Process files safely (handles spaces)
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    echo "Processing: $file"
done
 
# Retry pattern
retry() {
    local max_attempts=$1
    local delay=$2
    shift 2
    local attempt=1
 
    while [[ $attempt -le $max_attempts ]]; do
        if "$@"; then
            return 0
        fi
        log_warn "Attempt $attempt failed, retrying in ${delay}s..."
        sleep "$delay"
        ((attempt++))
    done
    return 1
}
 
retry 3 5 curl -f "https://api.example.com/health"
ToolPurpose
shellcheckStatic analysis (required)
shfmtCode formatting
bats-coreTesting framework
bash 5.xModern features (avoid macOS default 3.2)

ShellCheck Usage

# Run ShellCheck
shellcheck script.sh
 
# Disable specific warning (sparingly)
# shellcheck disable=SC2086
echo $UNQUOTED_VAR
 
# Follow sourced files
shellcheck -x script.sh

Testing with bats-core

#!/usr/bin/env bats
# File: test_script.bats
 
source ./script.sh
 
@test "add function returns correct sum" {
    result=$(add 5 3)
    [ "$result" = "8" ]
}
 
@test "validate_file fails on missing file" {
    run validate_file "nonexistent.txt"
    [ "$status" -eq 1 ]
}

Run tests:

bats tests/

POSIX Compatibility

For maximum portability (sh, dash, ash):

#!/bin/sh
# Use [ ] instead of [[ ]]
if [ -f "file.txt" ]; then
    echo "File exists"
fi
 
# No arrays, use positional parameters
set -- "apple" "banana" "cherry"
echo "First: $1"
 
# No $() in older shells, use backticks
current_date=`date +%Y-%m-%d`

Production Best Practices

  1. Defensive header - Always set -euo pipefail
  2. Quote everything - Prevent word splitting and glob expansion
  3. Local variables - Use local in functions
  4. ShellCheck clean - No warnings before commit
  5. Cleanup traps - Always clean up temp files
  6. Meaningful exit codes - 0 for success, non-zero for errors
  7. Logging to stderr - Keep stdout for data, stderr for logs
  8. Check dependencies - Verify required commands exist
  9. Handle signals - Trap SIGTERM for graceful shutdown
  10. Document usage - Include --help option

References