Shell Scripting Bash Libraries Basics May 2026

Shell Scripting Function Libraries & Sourcing

Build reusable bash function libraries using source and dot notation, design guard patterns to prevent double-sourcing, create colour output utilities, and organise scripts into maintainable multi-file projects.

Once you have more than two or three scripts, you'll notice you're copying the same logging functions, validation helpers, and colour output code into every file. Function libraries solve this — write once, source everywhere. This is the pattern that makes large bash codebases maintainable.

BASH
# ── source vs . (dot) — identical behaviour ───────────────
source /path/to/library.sh   # bash-specific, more readable
. /path/to/library.sh         # POSIX, works in sh too

# ── Source relative to script location ───────────────────
readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "${SCRIPT_DIR}/lib/logging.sh"
source "${SCRIPT_DIR}/lib/db.sh"
source "${SCRIPT_DIR}/lib/network.sh"

# ── Source with existence check ───────────────────────────
LIB="${SCRIPT_DIR}/lib/utils.sh"
[[ -f "${LIB}" ]] || { echo "ERROR: Missing library: ${LIB}" >&2; exit 1; }
source "${LIB}"

# ── Source config files ───────────────────────────────────
# Config files are just shell files with variable assignments
source /etc/myapp/config.env
source ~/.myapp/local.env   # user overrides (optional)
BASH
#!/usr/bin/env bash
# lib/logging.sh — Logging library with guard against double-source

# Guard: if already sourced, return immediately
[[ -n "${_LIB_LOGGING_LOADED:-}" ]] && return 0
readonly _LIB_LOGGING_LOADED=1

# ── Colours ───────────────────────────────────────────────
readonly RED=$'\033[0;31m'
readonly GREEN=$'\033[0;32m'
readonly YELLOW=$'\033[1;33m'
readonly BLUE=$'\033[0;34m'
readonly RESET=$'\033[0m'

# ── Log file (can be overridden before sourcing) ──────────
LOG_FILE="${LOG_FILE:-/var/log/app.log}"

_log() {
  local level="$1"; shift
  local colour="$1"; shift
  local ts
  ts=$(date '+%Y-%m-%d %H:%M:%S')
  printf "%b[%s] %-5s%b %s\n" "${colour}" "${ts}" "${level}" "${RESET}" "$*" \
    | tee -a "${LOG_FILE}"
}

log()  { _log "INFO"  "${BLUE}"   "$@"; }
ok()   { _log "OK"    "${GREEN}"  "$@"; }
warn() { _log "WARN"  "${YELLOW}" "$@"; }
err()  { _log "ERROR" "${RED}"    "$@" >&2; }
die()  { err "$@"; exit 1; }
BASH
#!/usr/bin/env bash
# lib/db.sh — MySQL database utility library

[[ -n "${_LIB_DB_LOADED:-}" ]] && return 0
readonly _LIB_DB_LOADED=1

# Requires lib/logging.sh to be sourced first
declare -f log >/dev/null 2>&1 || { echo "db.sh requires logging.sh" >&2; exit 1; }

db_query() {
  local host="${DB_HOST:?DB_HOST not set}"
  local user="${DB_USER:?DB_USER not set}"
  local pass="${DB_PASS:?DB_PASS not set}"
  local db="${1:?db_query: database required}"
  local sql="${2:?db_query: SQL required}"

  mysql -h "${host}" -u "${user}" -p"${pass}" \
        -D "${db}" -Nse "${sql}" 2>/dev/null
}

db_ping() {
  local host="${1:-${DB_HOST}}"
  mysql -h "${host}" -u "${DB_USER}" -p"${DB_PASS}" \
        -e "SELECT 1" >/dev/null 2>&1
}

db_table_exists() {
  local db="${1}" table="${2}"
  local result
  result=$(db_query "${db}" \
    "SELECT COUNT(*) FROM information_schema.tables
     WHERE table_schema='${db}' AND table_name='${table}'")
  [[ "${result}" -gt 0 ]]
}

db_row_count() {
  local db="${1}" table="${2}"
  db_query "${db}" "SELECT COUNT(*) FROM \`${table}\`"
}
BASH
#!/usr/bin/env bash
# deploy.sh — Main deployment script using libraries

set -euo pipefail

readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

# ── Load libraries in order ───────────────────────────────
source "${SCRIPT_DIR}/lib/logging.sh"
source "${SCRIPT_DIR}/lib/db.sh"
source "${SCRIPT_DIR}/lib/network.sh"

# ── Load config ───────────────────────────────────────────
source "${SCRIPT_DIR}/config/${APP_ENV:-production}.env"

main() {
  log  "Deployment starting — env: ${APP_ENV}"

  # Check DB reachable
  db_ping || die "Cannot reach database at ${DB_HOST}"
  ok   "Database is reachable"

  # Check table exists
  db_table_exists "myapp" "users" || die "users table missing!"
  ok   "Schema validated"

  # Get current row counts
  local users
  users=$(db_row_count "myapp" "users")
  log  "Current user count: ${users}"

  ok   "Deployment complete"
}

main "$@"
bash — deploy.sh
vriddh@prod-01:~/app$APP_ENV=production ./deploy.sh
[2026-05-01 10:14:02] INFO Deployment starting — env: production
[2026-05-01 10:14:02] OK Database is reachable
[2026-05-01 10:14:03] OK Schema validated
[2026-05-01 10:14:03] INFO Current user count: 48291
[2026-05-01 10:14:03] OK Deployment complete
BASH
# Recommended directory layout for a bash project
myapp-scripts/
├── bin/                 # Executable scripts (added to PATH)
│   ├── deploy.sh
│   ├── backup.sh
│   └── health-check.sh
├── lib/                 # Libraries (sourced, not executed directly)
│   ├── logging.sh       # Logging + colour functions
│   ├── db.sh            # Database utilities
│   ├── network.sh       # Network checks
│   └── utils.sh         # Generic helpers
├── config/              # Environment config files
│   ├── development.env
│   ├── staging.env
│   └── production.env
├── tests/               # BATS test files
│   ├── test_logging.bats
│   └── test_db.bats
└── README.md

# Each script in bin/ starts with:
#!/usr/bin/env bash
readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "${SCRIPT_DIR}/lib/logging.sh"
source "${SCRIPT_DIR}/lib/utils.sh"
✔ Library rules — Always add a double-source guard at the top of every library. Never put executable code (only function definitions and readonly constants) in library files. Source libraries in dependency order. Use $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) to find library files relative to the sourcing script — never hardcode paths.