Functions, arrays and associative arrays

Function definition and return versus output, local variables, positional parameters and shift, indexed arrays, mapfile, and associative arrays for lookups.

Functions

log() {
    local level=$1; shift                 # local keeps the caller's variables intact
    printf '%s [%s] %s
' "$(date -Is)" "$level" "$*" >&2
}

first_line() {
    local file=$1
    [[ -r $file ]] || return 1            # a status, not a message
    head -n 1 -- "$file"
}

if value=$(first_line "$config"); then
    log info "first line is $value"
else
    log error "cannot read $config"
fi
  • Functions are called like commands and receive positional parameters $1, $2, $@ — there is no parameter list in the definition.
  • return sets an exit status between 0 and 255. Data comes back on stdout, which is why the example captures it with $(...).
  • local is what makes functions safe to compose; without it every helper overwrites the caller's variables.
  • A function inside a subshell or behind a pipe cannot change the parent shell's state — cd or variable changes are lost.
# keyword arguments are just a convention on top of positional ones
deploy() {
    local env=staging tag=latest
    while (( $# )); do
        case "$1" in
            --env)  env=$2; shift 2 ;;
            --tag)  tag=$2; shift 2 ;;
            --)     shift; break ;;
            *)      break ;;
        esac
    done
    printf 'deploy env=%s tag=%s targets=%s
' "$env" "$tag" "$*"
}

Indexed arrays

servers=(web1 web2 web3)
servers+=(web4)                       # append one element

echo "${#servers[@]}"                # element count
echo "${servers[0]}"                 # first element
echo "${servers[-1]}"                # last element (bash 4.3+)

for s in "${servers[@]}"; do         # quoted: each element stays one argument
    printf '%s
' "$s"
done

printf '%s
' "${servers[@]}"        # same, one element per line

mapfile -t lines < hosts.txt          # file -> array, newline separated
mapfile -t -d '' paths < <(find . -type f -print0)   # NUL separated, any filename

unset 'servers[1]'                    # leaves a gap; indices do not renumber
echo "${!servers[@]}"                # which indices exist
ExpressionMeaning
arr=(a b c)Create or replace the whole array
arr+=(d)Append without losing existing elements
${arr[0]}Single element; unquoted expansions still word-split
${arr[@]}Every element — always quote it
${#arr[@]}Number of elements
${!arr[@]}The list of valid indices
${arr[@]:1:2}Slice: two elements starting at index 1
  • "$@" behaves exactly like an array and is the right way to forward arguments: wrapper() { real_tool --flag "$@"; }.
  • Omitting the [@] (${arr}) silently returns element zero — a frequent and confusing bug.

Associative arrays

declare -A port                  # required; plain assignment creates an indexed array

port[web]=80
port[api]=8080
port[db]=5432

declare -A region=(              # or all at once
    [eu-west]=ireland
    [us-east]=virginia
)

echo "${port[api]}"
for key in "${!region[@]}"; do
    printf '%s -> %s
' "$key" "${region[$key]}"
done

[[ -v port[api] ]] && echo "api port is set"     # bash 4.2+
unset 'port[db]'

An associative array replaces the usual case ladder or the grep-the-config-file trick. Lookups are one operation and typo-safe compared with string comparison chains.

  • Keys are arbitrary strings, so you do not need a naming convention like CONFIG_EU_WEST.
  • Associative arrays need bash 4. The bash 3.2 that ships with macOS does not support them — that alone is a reason to check the target.
  • declare -A on a name that is already an indexed array fails; unset it first.
⚠️
Always create associative arrays with declare -A. Without it map[key]=value is silently evaluated as an arithmetic index, so map[api]=8080 becomes element number 0 and looks like it worked until you read it back.

FAQ

How do I return a value from a function?
Print it to stdout and capture with $(fn); use return only for the status. Return two things at once as two lines, or set a variable whose name was passed in with printf -v.
Why is my array empty after a function call?
An assignment such as out=$(fn) runs the function in a subshell, so array changes do not survive. Have the function print one element per line and use mapfile -t arr < <(fn).

Parameter expansion and string manipulation Shells, startup files and running scripts

Last refreshed 2026-09-18.