Shell Scripting Bash Functions Basics May 2026

Shell Scripting Functions

Write reusable bash functions with arguments, local scope, return values, default parameters, and recursive logic. Learn the patterns that separate scripts from proper programs.

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.

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
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
Key Port open Port closed Version info
bash — functions demo
vriddh@prod-01:~/scripts$bash functions_demo.sh
✔ prod-db-01:3306 is open
DB accessible — proceeding
MySQL version: 8.0.36

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}"
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
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"
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"
bash — deploy.sh
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.