Table of Contents

Shell Style Rules

Shell Start

Executables must start with #!/bin/bash and a minimum number of flags. Use set -euo pipefail for better error handling.

#!/bin/bash
set -euo pipefail

Shebang Best Practices

Use the most appropriate shebang for your script’s requirements.

# For bash-specific features
#!/bin/bash

# For POSIX-compliant scripts
#!/bin/sh

# For scripts that need to find bash in PATH
#!/usr/bin/env bash

File Extensions

Executable files must not have an extension or a .sh extension; libraries must have a .sh extension and no executable files.

When you run a program, you do not need to know what language the program is written in, and you do not need an extension for the shell, so it is not recommended to use the extension for the executable file.

SUID/SGID

SUID and SGID are forbidden on shell scripts.

Use sudo to provide elevated access if you need it.

Error message classification

All error messages should go to STDERR. Use structured logging with timestamps and log levels.

# Enhanced error logging function
log() {
  local level="$1"
  shift
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] [$level] $*" >&2
}

err() {
  log "ERROR" "$@"
}

warn() {
  log "WARN" "$@"
}

info() {
  log "INFO" "$@"
}

# Usage
if ! do_something; then
  err "Unable to do_something"
  exit 1
fi

Exit Codes

Use meaningful exit codes for different error conditions.

# Wrong
if ! command_exists git; then
  echo "Git not found" >&2
  exit 1
fi

# Right
if ! command_exists git; then
  err "Git not found. Please install Git."
  exit 2  # Specific exit code for missing dependency
fi

File Header Comment

Start each file with a description of its contents.

#!/bin/bash
#
# Starting for Pelagornis Test Script.

Function Comments

Any features that are neither clear nor short should be annotated.

All function annotations should describe the intended API behavior using the following.

#######################################
# Create File for pelagornis Project.
# Globals:
#   SOMEDIR
# Arguments:
#   File Name
# Outputs:
#   Indicates whether the file creation was successful.
#######################################
function create_file() {}

Shell Style Formatting

Indentation

Indent 2 spaces. No tabs.

while read -r f; do
  echo "file=${f}"
done < <(find /tmp)

Line Length and Long Strings

Maximum line length is 80 characters.

Meaningful Variable Names

Use uppercase letters for variable names and prefer underscores _ for readability.

OUTPUT_DIR="/path/to/output"
FILE_NAME="example.txt"

Pipelines

If the pipeline doesn’t fit all one line, you’ll need to split one line per line.

If the pipelines all fit in one line, they should be in one line.

# All fits on one line
command1 | command2

# Long commands
command1 \
  | command2 \
  | command3 \
  | command4

Loop

Put ; do and ; then on the same line as the while, for or if.

for dir in "${create_file[@]}"; do
  if [[ -d "${dir}/${FileName}" ]]; then
    ...
  fi
done

Case statement

Indent alternatives by 2 spaces.

A one-line alternative needs a space after the close parenthesis of the pattern and before the ;;.

Long or multi-command alternatives should be split over multiple lines with the pattern, actions, and ;; on separate lines.

case "${expression}" in
  a)
    variable="…"
    some_command "${variable}" "${other_expr}";;
  absolute)
    actions="relative"
    another_command "${actions}" "${other_expr}";;
  *)
    error "Unexpected expression '${expression}'"
    ;;
esac

Security Guidelines

Input Validation

Always validate and sanitize user input to prevent injection attacks.

# Wrong
read -p "Enter filename: " filename
rm "$filename"

# Right
read -p "Enter filename: " filename
if [[ ! "$filename" =~ ^[a-zA-Z0-9._-]+$ ]]; then
  err "Invalid filename: contains illegal characters"
  exit 1
fi
if [[ ! -f "$filename" ]]; then
  err "File does not exist: $filename"
  exit 1
fi
rm "$filename"

Safe Variable Expansion

Use proper quoting to prevent word splitting and pathname expansion.

# Wrong
cp $source $destination

# Right
cp "$source" "$destination"

Temporary Files

Use secure methods for creating temporary files.

# Wrong
temp_file="/tmp/script_$$"

# Right
temp_file=$(mktemp) || {
  err "Failed to create temporary file"
  exit 1
}
trap 'rm -f "$temp_file"' EXIT

Command Substitution Security

Be careful with command substitution to avoid code injection.

# Wrong
eval "echo $user_input"

# Right
printf '%s\n' "$user_input"

Error Handling

Trap Handlers

Use trap handlers for cleanup and error handling.

#!/bin/bash
set -euo pipefail

# Cleanup function
cleanup() {
  local exit_code=$?
  if [[ -n "${temp_dir:-}" && -d "$temp_dir" ]]; then
    rm -rf "$temp_dir"
  fi
  exit $exit_code
}

# Set up trap
trap cleanup EXIT INT TERM

# Create temporary directory
temp_dir=$(mktemp -d)

Error Recovery

Implement proper error recovery mechanisms.

# Retry mechanism
retry() {
  local max_attempts="$1"
  shift
  local attempt=1

  while [[ $attempt -le $max_attempts ]]; do
    if "$@"; then
      return 0
    fi
    warn "Attempt $attempt failed, retrying..."
    ((attempt++))
    sleep 1
  done

  err "All $max_attempts attempts failed"
  return 1
}

# Usage
retry 3 curl -f "https://api.example.com/data"

Performance Optimization

Efficient Loops

Use efficient loop constructs and avoid unnecessary subshells.

# Wrong
for file in $(ls *.txt); do
  echo "$file"
done

# Right
for file in *.txt; do
  [[ -f "$file" ]] || continue
  echo "$file"
done

Process Substitution

Use process substitution instead of pipes when appropriate.

# Wrong
echo "data" | while read line; do
  process "$line"
done

# Right
while read -r line; do
  process "$line"
done < <(echo "data")

Parallel Processing

Use parallel processing for CPU-intensive tasks.

# Process files in parallel
process_file() {
  local file="$1"
  # Process the file
  echo "Processed: $file"
}

export -f process_file
find . -name "*.txt" -print0 | xargs -0 -P 4 -I {} bash -c 'process_file "$@"' _ {}

Testing

Unit Testing

Write testable shell scripts with proper function structure.

#!/bin/bash
# test_script.sh

# Function to test
is_valid_email() {
  local email="$1"
  [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}

# Test function
run_tests() {
  local passed=0
  local failed=0

  test_case() {
    local description="$1"
    local expected="$2"
    local actual="$3"

    if [[ "$expected" == "$actual" ]]; then
      echo "✓ $description"
      ((passed++))
    else
      echo "✗ $description (expected: $expected, got: $actual)"
      ((failed++))
    fi
  }

  # Run tests
  test_case "Valid email" "true" "$(is_valid_email "test@example.com" && echo true || echo false)"
  test_case "Invalid email" "false" "$(is_valid_email "invalid-email" && echo true || echo false)"

  echo "Tests: $passed passed, $failed failed"
  return $failed
}

# Run tests if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  run_tests
fi

Mock Functions

Use mock functions for testing external dependencies.

# Mock external command for testing
mock_curl() {
  echo '{"status": "success", "data": "test"}'
}

# Test with mock
test_api_call() {
  local result
  result=$(mock_curl)
  if [[ "$result" == *"success"* ]]; then
    echo "API call test passed"
  else
    echo "API call test failed"
  fi
}

Portability

POSIX Compliance

Write portable scripts that work across different shells and systems.

# Wrong (bash-specific)
array=(item1 item2 item3)
echo "${array[@]}"

# Right (POSIX-compliant)
items="item1 item2 item3"
for item in $items; do
  echo "$item"
done

Path Handling

Use portable path handling techniques.

# Wrong
script_dir="$(dirname "$0")"
include_file="$script_dir/../lib/utils.sh"

# Right
script_dir="$(dirname "$0")"
include_file="$(cd "$script_dir/.." && pwd)/lib/utils.sh"

Command Availability

Check for command availability before using.

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

# Use with fallback
if command_exists jq; then
  json_parser="jq"
elif command_exists python3; then
  json_parser="python3 -m json.tool"
else
  err "No JSON parser available (jq or python3 required)"
  exit 1
fi

Modern Shell Features

Associative Arrays

Use associative arrays for key-value data structures.

# Declare associative array
declare -A config

# Set values
config["database_host"]="localhost"
config["database_port"]="5432"
config["database_name"]="myapp"

# Access values
echo "Database: ${config[database_host]}:${config[database_port]}/${config[database_name]}"

Process Substitution with Arrays

Use process substitution with arrays for complex data processing.

# Read file into array
mapfile -t lines < <(grep -v "^#" config.txt)

# Process each line
for line in "${lines[@]}"; do
  echo "Processing: $line"
done

Here Documents

Use here documents for multi-line strings and templates.

# Generate configuration file
generate_config() {
  local db_host="$1"
  local db_port="$2"

  cat > config.ini << EOF
[database]
host = $db_host
port = $db_port
timeout = 30

[logging]
level = info
file = /var/log/app.log
EOF
}

Advanced Parameter Expansion

Use advanced parameter expansion for string manipulation.

# String manipulation examples
filename="/path/to/file.txt"

# Get basename
basename="${filename##*/}"

# Get directory
dirname="${filename%/*}"

# Get extension
extension="${filename##*.}"

# Remove extension
name_without_ext="${filename%.*}"

# Default values
output_dir="${OUTPUT_DIR:-/tmp}"
debug_mode="${DEBUG:-false}"