Skip to main content
RunBook Academy

LinuxXXXV · Shell Scripting for SysadminsFunctions

Functions and arguments - reusable, testable script components

Intermediate⏱ ~10 minbash

What you'll learn

  • Define and call functions
  • Pass arguments correctly
  • Return values from functions
  • Use local variables

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

Functions make scripts modular, testable, and reusable. This lesson covers the right patterns.

Define a function

greet() {
    local name="$1"
    echo "Hello, $name"
}
greet alice    # Hello, alice

Functions are defined with name() { ... } (no function keyword in pure POSIX; bash accepts both).

Arguments

Inside a function, $1, $2, etc. are the function’s arguments, NOT the script’s:

my_func() {
    echo "arg1: $1, arg2: $2"
}
my_func a b    # arg1: a, arg2: b

$@ and $# similarly refer to the function’s positional parameters, not the script’s.

To pass all script arguments through a function:

my_func "$@"

Return values

Bash functions have two kinds of return:

  • Exit status: return N (0-255). Like the script’s exit.
  • Output: stdout via echo or printf. Captured with $(func).
is_even() {
    local n="$1"
    if ((n % 2 == 0)); then
        return 0    # true
    else
        return 1    # false
    fi
}

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

# Capture output
result=$(greet bob)
echo "Got: $result"

Local variables

Use local to avoid polluting the global namespace:

my_func() {
    local count=0        # local to this function
    count=$((count + 1))
    echo "$count"
}

Without local, variables leak to the caller scope.

Document with comments

# greet <name>
#   Print a greeting for <name>.
#   Returns: 0
greet() {
    local name="$1"
    echo "Hello, $name"
}

The header describes inputs, outputs, and side effects.

Test functions

Test functions in isolation:

test_greet() {
    local output
    output=$(greet alice) || { echo "FAIL: greet exited $?"; return 1; }
    [[ "$output" == "Hello, alice" ]] || { echo "FAIL"; return 1; }
    echo "PASS"
}
test_greet

Note the two lines. Writing local output=$(greet alice) in one statement discards greet’s exit status — $? becomes the status of local, which practically always succeeds. The test would then compare an empty string and report FAIL for the wrong reason, or worse, pass while the function under test crashed. ShellCheck flags this as SC2155; the same trap applies to declare, export and readonly.

A test script for each function is the discipline.

Library pattern

Group functions in a sourced file:

# lib/utils.sh
log() { echo "[$(date -Is)] $*" >&2; }
err() { log "ERROR: $*"; return 1; }

# Print the caller's usage block. Delimited, so it cannot swallow the
# shebang, the section dividers, or every other comment in the file.
usage() {
    sed -n '/^#---BEGIN-USAGE/,/^#---END-USAGE/{ /^#---/d; s/^# \?//; p; }' \
        "${BASH_SOURCE[1]}"
}

Source in the main script:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/utils.sh"

Knowledge check

Knowledge check · 3 questions

  1. Q1. How do functions return a value to use as a command output?

  2. Q2. A function body that reads `$@` sees the arguments passed to the function, not the ones passed to the script.

  3. Q3. Which of the following are valid bash function patterns? Select all that apply.

Passing score: 75%. Answers are checked in this browser.