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
localsilently clobbers the caller's variables. returncannot 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.