Functions

Defining Functions

Syntax

Two forms are equivalent; the second is preferred because it reads clearly and is valid in POSIX sh.

function greet { echo "hi"; }     # bash-specific keyword
greet() { echo "hi"; }            # POSIX form — preferred

A function must be defined before it is called, because Bash reads scripts top to bottom.

Calling

log() {
  printf '[%s] %s\n' "$(date +%T)" "$*"
}

log "starting backup"
log "done"

Arguments & Return Values

Positional Arguments

Inside a function $1, $2, $@ and $# refer to the function's arguments, not the script's.

add() {
  local a="$1" b="$2"
  echo $(( a + b ))          # print the result
}

sum="$(add 3 4)"             # capture the printed value
echo "sum=$sum"              # sum=7

Two Channels

A function has two output channels: the exit status (0–255) for success/failure, and stdout for data. Do not mix them.

return Sets the Status

is_root() {
  [[ "$(id -u)" -eq 0 ]]      # last command's status becomes the return value
}

if is_root; then
  echo "running as root"
else
  echo "need sudo" >&2
fi
return 300 is meaningless: the status is one byte (0–255) and 300 wraps to 44. Return statuses, not numbers.

Capturing Output

mktemp_dir() {
  mktemp -d "${TMPDIR:-/tmp}/job.XXXXXX"
}

work="$(mktemp_dir)" || { echo "cannot create temp dir" >&2; exit 1; }
trap 'rm -rf "$work"' EXIT
echo "working in $work"

Scope & local

By default a variable assigned in a function is global. local keeps it inside the call — always declare your working variables local.

counter=0

bump() {
  local step="${1:-1}"     # scoped to this call
  counter=$((counter + step))   # deliberately modifies the global
}

bump 5
bump
echo "$counter"            # 6

local -a and local -A declare local arrays; local -r makes them read-only.

first() {
  local -a items=("$@")
  printf '%s\n' "${items[0]}"
}
first alpha beta gamma     # alpha

Reusable Libraries

Sourcing

A library is just a file of function definitions you source (or .) into the current shell. Sourcing runs the file in the current shell, so its functions become available.

# lib/common.sh
log()  { printf '[%s] %s\n' "$(date +%T)" "$*" >&2; }
die()  { log "FATAL: $*"; exit 1; }

# main.sh
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
log "hello from main"
source finds the file relative to the current directory, which may change. Build an absolute path from ${BASH_SOURCE[0]} as shown above.

Guard Against Double Sourcing

[[ -n "${COMMON_SH_LOADED:-}" ]] && return 0
COMMON_SH_LOADED=1
# ... definitions ...

Pitfalls

  • Forgetting local silently clobbers the caller's variables.
  • return cannot return strings — print to stdout and use $( ), or use a global.
  • Functions defined in a | pipeline stage live in a subshell and disappear.
  • Recursion is allowed but has no tail-call optimisation — deep recursion can exhaust the stack.