Freebie

Functions

Functions group commands under a name so you can call them multiple times. Define a function before calling it:

function check_disk {
    local threshold=$1
    local usage
    usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
    if (( usage > threshold )); then
        echo "WARNING: disk usage at ${usage}%" >&2
        return 1
    fi
    echo "Disk usage OK: ${usage}%"
    return 0
}

check_disk 80

Inside a function, positional parameters ($1, $2, …) refer to the function’s own arguments, not the script’s arguments. $0 still refers to the script name. $# is the number of arguments passed to the function.

Functions return a status code. To return a value other than a status, store it in a variable:

function get_ip {
    local RESULT
    RESULT=$(hostname -I | awk '{print $1}')
    echo "$RESULT"
}

MY_IP=$(get_ip)
echo "My IP: $MY_IP"

Arguments

Functions receive arguments as positional parameters: $1 for the first argument, $2 for the second, and so on. $# holds the count. Assign arguments to named local variables at the top of the function so the code is self-documenting:

create_user() {
    local username=$1
    local group=$2

    useradd -m -G "$group" "$username"
    echo "Created $username in $group"
}

create_user alice developers

Function params vs. script params

Inside a function, positional parameters refer to the function’s own arguments, not the script’s. $1 is the first argument passed to the function call, not to the script. $0 always refers to the script name regardless of where it appears:

#!/usr/bin/env bash

deploy() {
    local env=$1             # "staging" — the function's $1
    echo "Deploying to $env"
    echo "Script: $0"        # still the script name, not the function name
}

# Script called as: ./deploy.sh production
echo "Script arg: $1"        # production — the script's $1
deploy staging               # function receives its own separate $1

Output:

Script arg: production
Deploying to staging
Script: ./deploy.sh

To use the script’s positional parameters inside a function, pass them explicitly:

process_all() {
    local item
    for item in "$@"; do
        echo "Processing $item"
    done
}

process_all "$@"    # forward all script arguments to the function

Default values

Use parameter expansion to make arguments optional:

backup_dir() {
    local src=$1
    local dest=${2:-/tmp/backup}    # defaults to /tmp/backup if not supplied

    cp -r "$src" "$dest"
    echo "Backed up $src to $dest"
}

backup_dir /etc/app              # uses default dest
backup_dir /etc/app /mnt/backup  # uses supplied dest

Validating required arguments

Check that required arguments are present before doing any work:

send_alert() {
    local message=$1
    local recipient=$2

    if [[ -z "$message" || -z "$recipient" ]]; then
        echo "send_alert: message and recipient are required" >&2
        return 1
    fi

    mail -s "Alert" "$recipient" <<< "$message"
}

Iterating over all arguments

Use "$@" to process every argument the function receives:

ping_hosts() {
    local host
    for host in "$@"; do
        if ping -c1 -q "$host" &>/dev/null; then
            echo "$host: up"
        else
            echo "$host: unreachable" >&2
        fi
    done
}

ping_hosts web-01 web-02 db-01

"$@" expands each argument as a separately quoted word, preserving arguments that contain spaces.

Returning values

Return codes

return exits the current function and sets its exit status. Use 0 for success and a non-zero value for failure. The caller reads the status from $?:

is_even() {
    local n=$1
    (( n % 2 == 0 ))    # sets exit status: 0 if true, 1 if false
}

if is_even 4; then
    echo "4 is even"
fi

is_even 7
echo $?    # 1

return without an argument returns the exit status of the last command that ran in the function.

Returning data with stdout

Functions cannot return strings directly. Print the value with echo and capture it with command substitution:

get_hostname() {
    echo "$(hostname -s)"
}

HOST=$(get_hostname)
echo "Running on $HOST"

Because command substitution runs the function in a subshell, variables set inside the function are not visible to the caller. Use this pattern when you only need the printed output.

Returning data with a named variable

To return multiple values, or to avoid a subshell, write the result into a caller-supplied variable name using a nameref (local -n, Bash 4.3+):

get_load() {
    local -n _result=$1    # _result is an alias for the variable named in $1
    _result=$(uptime | awk '{print $NF}')
}

get_load load_avg
echo "Load: $load_avg"

local -n makes _result a nameref: reads and writes to _result affect the variable whose name was passed as $1. Prefix the internal name with _ to avoid a collision if the caller passes a variable with the same name.