Functions are the single biggest quality improvement you can make to a script. They eliminate repetition, make code testable, improve readability, and let you build progressively more complex logic from simple verified building blocks. Every script longer than 50 lines should use functions.
1
Declaring functions — two syntaxes
BASH
# ── Syntax 1: function keyword (bash-specific) ────────────
function greet {
echo "Hello, ${1}!"
}
# ── Syntax 2: POSIX style (recommended — more portable) ───
greet() {
echo "Hello, ${1}!"
}
# Both work identically in bash. Use syntax 2.
# ── Calling a function ────────────────────────────────────
greet "Vriddh" # Hello, Vriddh!
greet "World" # Hello, World!
# ── Functions must be defined BEFORE they are called ──────
main() {
setup # Fine — setup is defined below
run
}
setup() { echo "Setting up..."; }
run() { echo "Running..."; }
main # Called at the bottom — setup and run already defined
2
Arguments, local variables, and return values
BASH
#!/usr/bin/env bash
check_port() {
# Arguments accessed as $1, $2 ... inside function
local host="${1:?check_port requires a host}"
local port="${2:?check_port requires a port}"
local timeout="${3:-3}" # optional, default 3 seconds
if nc -z -w "${timeout}" "${host}" "${port}" >/dev/null 2>&1; then
echo " ✔ ${host}:${port} is open"
return 0 # success exit code
else
echo " ✘ ${host}:${port} is closed" >&2
return 1 # failure exit code
fi
}
# ── Calling with return value check ───────────────────────
if check_port "prod-db-01" "3306"; then
echo "DB accessible — proceeding"
else
echo "DB not accessible — aborting"
exit 1
fi
# ── Capturing function output ─────────────────────────────
get_db_version() {
local host="${1}"
mysql -h "${host}" -Nse "SELECT VERSION()" 2>/dev/null
}
version=$(get_db_version "prod-db-01")
echo "MySQL version: ${version}" # e.g. 8.0.36
Terminal output
Key
Port open
Port closed
Version info
vriddh@prod-01:~/scripts$bash functions_demo.sh
✔ prod-db-01:3306 is open
DB accessible — proceeding
MySQL version: 8.0.36
█
3
Returning data — beyond exit codes
Functions can only return exit codes (0-255). To return string data, use one of three patterns: echo and capture, global variable, or nameref (bash 4.3+).
BASH
# ── Pattern 1: echo and capture (most common) ─────────────
get_timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
ts=$(get_timestamp)
echo "Timestamp: ${ts}"
# ── Pattern 2: global variable (fast, no subshell) ────────
get_free_space() {
# Sets a global variable instead of echoing
__free_space=$(df -BG "${1}" | awk 'NR==2{print $4}')
}
get_free_space "/"
echo "Free: ${__free_space}" # Use __ prefix to avoid name clashes
# ── Pattern 3: nameref output variable (bash 4.3+) ────────
get_hostname() {
local -n _out="${1}" # nameref to caller's variable
_out=$(hostname -f)
}
get_hostname myhost
echo "Host: ${myhost}" # Populated by function via nameref
# ── Returning multiple values via nameref ─────────────────
parse_dsn() {
# parse_dsn "mysql://user:pass@host:3306/dbname" user_var host_var port_var
local dsn="${1}"
local -n _user="${2}" _host="${3}" _port="${4}"
_user=$(echo "${dsn}" | sed 's|.*://\([^:]*\):.*|\1|')
_host=$(echo "${dsn}" | sed 's|.*@\([^:]*\):.*|\1|')
_port=$(echo "${dsn}" | sed 's|.*:\([0-9]*\)/.*|\1|')
}
parse_dsn "mysql://appuser:secret@prod-db-01:3306/mydb" db_user db_host db_port
echo "User: ${db_user}, Host: ${db_host}, Port: ${db_port}"
4
Default arguments and argument validation
BASH
backup_db() {
# Named parameter style with defaults
local host="${1:?ERROR: host is required}"
local db="${2:?ERROR: database name is required}"
local output_dir="${3:-/backups}" # default /backups
local compress="${4:-true}" # default compress
local timestamp
timestamp=$(date '+%Y%m%d_%H%M%S')
local filename="${output_dir}/${db}_${timestamp}.sql"
echo " Backing up ${db} from ${host}..."
mysqldump -h "${host}" "${db}" > "${filename}" || {
echo " ERROR: Backup failed" >&2
return 1
}
if [[ "${compress}" == "true" ]]; then
gzip "${filename}"
echo " ✔ Saved: ${filename}.gz"
else
echo " ✔ Saved: ${filename}"
fi
}
# Call with all args
backup_db "prod-db-01" "myapp" "/backups/daily" "true"
# Call with defaults for 3rd and 4th args
backup_db "prod-db-01" "myapp"
# Missing required arg — exits with error
backup_db "prod-db-01" # ERROR: database name is required
5
Recursive functions
BASH
# ── Factorial ─────────────────────────────────────────────
factorial() {
local n="${1}"
(( n <= 1 )) && { echo 1; return; }
echo $(( n * $(factorial $(( n - 1 ))) ))
}
echo "5! = $(factorial 5)" # 120
# ── Directory tree traversal ─────────────────────────────
tree_size() {
local dir="${1}"
local depth="${2:-0}"
local indent
indent=$(printf '%*s' $(( depth * 2 )) '')
printf "%s%s/\n" "${indent}" "$(basename "${dir}")"
for item in "${dir}"/*; do
[[ -d "${item}" ]] && tree_size "${item}" $(( depth + 1 ))
[[ -f "${item}" ]] && printf "%s %s\n" \
"${indent}" "$(basename "${item}")"
done
}
tree_size "/etc/nginx"
6
Production function patterns
BASH
#!/usr/bin/env bash
# Standard logging and error functions used in every production script
readonly LOG_FILE="/var/log/myapp/deploy.log"
readonly TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { printf "[%s] INFO %s\n" "${TIMESTAMP}" "$*" | tee -a "${LOG_FILE}"; }
log_ok() { printf "[%s] OK %s\n" "${TIMESTAMP}" "$*" | tee -a "${LOG_FILE}"; }
log_warn(){ printf "[%s] WARN %s\n" "${TIMESTAMP}" "$*" | tee -a "${LOG_FILE}"; }
log_err() { printf "[%s] ERROR %s\n" "${TIMESTAMP}" "$*" | tee -a "${LOG_FILE}" >&2; }
die() { log_err "$*"; exit 1; }
require_cmd() {
for cmd in "$@"; do
command -v "${cmd}" >/dev/null 2>&1 \
|| die "Required command not found: ${cmd}"
done
}
require_env() {
for var in "$@"; do
[[ -n "${!var:-}" ]] \
|| die "Required env var not set: ${var}"
done
}
# Usage
require_cmd mysql curl jq
require_env DB_HOST DB_PASS APP_ENV
log "Deployment started"
log_ok "All prerequisites met"
log_warn "Running in ${APP_ENV} — double check targets"
vriddh@prod-01:~/scripts$APP_ENV=staging DB_HOST=stg-db-01 DB_PASS=secret ./deploy.sh
[2026-05-01 10:14:02] INFO Deployment started
[2026-05-01 10:14:02] OK All prerequisites met
[2026-05-01 10:14:02] WARN Running in staging — double check targets
vriddh@prod-01:~/scripts$./deploy.sh
[2026-05-01 10:14:05] ERROR Required env var not set: DB_HOST
█
✔ Function best practices — Always declare variables
local inside functions. Use ${1:?message} for required args and ${1:-default} for optional ones. Prefix "private" global output variables with __ to avoid name collisions. Keep functions short — if it doesn't fit on one screen, split it further.