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.
1
source and . — loading library files
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)
2
Guard pattern — prevent double-sourcing
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; }
3
Complete library structure — db.sh
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}\`"
}
4
Main script consuming libraries
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 "$@"
Terminal output
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
█
5
Recommended library project structure
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.