LinuxXXXV · Shell Scripting for SysadminsFunctions
Functions and arguments - reusable, testable script components
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
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
echoorprintf. 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
Q1. How do functions return a value to use as a command output?
Q2. A function body that reads `$@` sees the arguments passed to the function, not the ones passed to the script.
Q3. Which of the following are valid bash function patterns? Select all that apply.
Passing score: 75%. Answers are checked in this browser.