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.

  • Bash
  • Scripting
  • Linux