Functions turn a long script into small named steps that you can reuse, test and read. Bash functions behave a little differently from functions in other languages. Arguments are positional, “return” means an exit code, and variables are global unless you say otherwise.
Defining and calling
greet() {
echo "Hello, $1"
}
greet "world"
The function greet { … } form also works in Bash, but name() { … } is portable and more common. A function must be defined before it’s called.
Arguments
Inside a function, $1, $2, $# and "$@" refer to the function’s own arguments, not the script’s:
backup_db() {
local db="$1"
local dest="${2:-/var/backups}"
mysqldump --single-transaction "$db" | gzip > "$dest/$db-$(date +%F).sql.gz"
}
backup_db shop
backup_db blog /mnt/backups
Check required arguments:
deploy() {
if (( $# < 1 )); then
echo "usage: deploy <version>" >&2
return 2
fi
…
}
Return values
return sets the exit status (0–255). It doesn’t return data:
is_up() {
curl -fsS -o /dev/null --max-time 5 "$1"
}
if is_up https://example.com; then echo "up"; else echo "down"; fi
Without return, the status is that of the last command. That’s often all you need.
To return data, print it and capture it with command substitution:
disk_usage() {
df --output=pcent "${1:-/}" | tail -1 | tr -dc '0-9'
}
usage=$(disk_usage /var)
(( usage > 90 )) && echo "/var at ${usage}%"
Anything else the function prints also ends up in the captured value. Send log messages to stderr (>&2) so they don’t mix in.
Local variables
Variables are global by default. A function can accidentally overwrite the caller’s variables:
count=10
reset() { count=0; }
reset
echo "$count" # 0
Declare function variables with local:
process() {
local file="$1" lines
lines=$(wc -l < "$file")
echo "$file: $lines"
}
Write local x; x=$(cmd) on separate lines when you need the command’s exit status. local x=$(cmd) always returns 0, which hides failures, even with set -e.
Error handling
A common pattern: a die helper and a log helper.
log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
[[ -f /etc/app.conf ]] || die "config missing"
log "starting backup"
Clean up on exit with a trap:
tmp=$(mktemp -d)
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT
Libraries: source a file of functions
# /usr/local/lib/common.sh
log() { …; }
die() { …; }
# in scripts
source /usr/local/lib/common.sh
source (or .) runs the file in the current shell, so its functions and variables become available.
A main function
Wrapping the script’s logic in main keeps the top level tidy and lets you define helpers in any order:
#!/usr/bin/env bash
set -euo pipefail
log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >&2; }
backup() {
local db="$1"
log "dumping $db"
mysqldump --single-transaction "$db" | gzip > "/var/backups/$db-$(date +%F).sql.gz"
}
prune() {
find /var/backups -name '*.sql.gz' -mtime +14 -delete
}
main() {
local db
for db in "$@"; do backup "$db"; done
prune
log "done"
}
main "$@"
Useful bits
declare -f # list all functions with their bodies
declare -F # just the names
type greet # show a function's definition
unset -f greet # remove a function
export -f greet # make it available to child bash processes (e.g. xargs bash -c)
Functions in ~/.bashrc act like smarter aliases:
mkcd() { mkdir -p -- "$1" && cd -- "$1"; }
Common mistakes
- Using
return "some text". Return only takes a number. Print the text instead. - Forgetting
local, so loop variables leak and clobber each other. - Calling a function before it’s defined.
- Logging to stdout inside a function whose output you capture.
Related: Bash variables, if/else and for loops.