8.4. Function Libraries#

8.4.1. Common Pitfalls#

1. Global variables polluting the namespace

# Bad: global variables in library
counter=0
increment() {
  counter=$((counter + 1))
}

# Good: use local or prefixed globals
inc_counter=0
increment() {
  inc_counter=$((inc_counter + 1))
}

2. Hardcoded paths instead of script-relative paths

# Bad: breaks if script location changes
source /home/user/lib_utils.sh

# Good: relative to script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib_utils.sh"

3. Not handling sourcing errors

# Bad: silently fails
source "$SCRIPT_DIR/lib_utils.sh"
my_function  # Error: command not found if sourcing failed

# Good: check success
source "$SCRIPT_DIR/lib_utils.sh" || {
  echo "Failed to load library"
  exit 1
}
my_function

8.4.2. Real-World Example: Complete Library System#

Create a file lib_deploy.sh:

#!/bin/bash
# lib_deploy.sh - Deployment utilities

readonly LOG_FILE="/var/log/deploy.log"

# Log deployment events
deploy_log() {
  local msg="$1"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $msg" >> "$LOG_FILE"
}

# Check deployment prerequisites
check_prerequisites() {
  local required=("git" "docker" "kubectl")
  
  for cmd in "${required[@]}"; do
    if ! command -v "$cmd" > /dev/null; then
      deploy_log "ERROR: $cmd not found"
      return 1
    fi
  done
  
  deploy_log "All prerequisites met"
  return 0
}

# Deploy Docker image
deploy_image() {
  local image=$1
  local tag=$2
  
  deploy_log "Deploying $image:$tag"
  docker pull "$image:$tag" || return 1
  kubectl set image deployment/app app="$image:$tag" || return 1
  deploy_log "Successfully deployed $image:$tag"
  return 0
}

Usage script deploy.sh:

#!/bin/bash

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib_deploy.sh"

if ! check_prerequisites; then
  echo "Prerequisites missing"
  exit 1
fi

deploy_image "myapp" "v1.2.3"

8.4.3. Library Design Best Practices#

8.4.3.1. 1. Namespace Convention#

Prefix function names to avoid conflicts:

# lib_string.sh
string_uppercase() {
  echo "$1" | tr '[:lower:]' '[:upper:]'
}

string_lowercase() {
  echo "$1" | tr '[:upper:]' '[:lower:]'
}

# lib_file.sh
file_exists() {
  [[ -f "$1" ]]
}

file_is_readable() {
  [[ -r "$1" ]]
}

8.4.3.2. 2. Document Function Purpose#

# Validate an email address (basic regex)
# Args: $1 = email to validate
# Returns: 0 if valid, 1 if invalid
validate_email() {
  local email=$1
  if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    return 0
  else
    return 1
  fi
}

8.4.3.3. 3. Use Local Variables#

All function variables should be local to prevent pollution:

# lib_math.sh
math_factorial() {
  local n=$1
  local result=1
  local i
  
  for ((i = 2; i <= n; i++)); do
    result=$((result * i))
  done
  
  echo $result
}

8.4.4. Handling Relative Paths in Libraries#

When sourcing libraries, use a technique to find them relative to the script location.

8.4.4.1. Robust Library Sourcing#

#!/bin/bash
# myscript.sh

# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Source the library relative to script location
source "$SCRIPT_DIR/lib_utils.sh"

# Now functions are available
log "Script started"

BASH_SOURCE[0] contains the path of the script being executed, allowing you to find related libraries in the same directory.

8.4.5. Sourcing Libraries with source or .#

Use the source command (or .) to load functions from another file.

8.4.5.1. Sourcing a Library#

#!/bin/bash
# myscript.sh

# Load the library
source ./lib_utils.sh

# Or use the dot notation (same thing)
# . ./lib_utils.sh

# Now use functions from the library
if is_root; then
  log "Running as root"
else
  error "This script requires root privileges"
  exit 1
fi

if command_exists "docker"; then
  log "Docker is installed"
fi

Important: When sourcing, use a path. source lib_utils.sh looks in PATH; source ./lib_utils.sh uses current directory.

8.4.6. Creating a Function Library#

A function library (or library script) is a file containing a collection of related functions, designed to be used by multiple scripts.

8.4.6.1. Library File Structure#

Create a file like lib_utils.sh:

#!/bin/bash
# lib_utils.sh - Common utility functions

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

# Function to check if user is root
is_root() {
  [[ $EUID -eq 0 ]]
}

# Function to log messages with timestamp
log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

# Function to print error messages
error() {
  echo "ERROR: $*" >&2
}

Best practice: Include a shebang and add comments documenting each function.