Functions
Define reusable shell functions, manage local variables, handle arguments with $@/$#, return values, and share logic across scripts.
- Define and call Bash functions
- Use local to keep variables from leaking out of functions
- Access positional arguments via $1-$9, $@, and $#
- Return values using echo and exit codes
- Source a function library from multiple scripts
Scripts grow. A 50-line script that does everything in one block becomes hard to debug, test, and reason about. Functions solve this the same way they do in every language: name a piece of logic, isolate its state, and call it by name. The difference in Bash is that functions live in the same shell process and share the global environment — which makes scoping discipline essential.
Defining and calling functions
Bash has two equivalent syntaxes; the second is more portable:
# Style 1 — function keyword
function greet {
echo "Hello, $1"
}
# Style 2 — POSIX compatible (preferred)
greet() {
echo "Hello, $1"
}
greet "world" # Hello, world
greet "Alice" # Hello, AliceFunctions must be defined before they are called — Bash reads scripts top to bottom. Define utility functions at the top of the file (or in a sourced library), and call them from the bottom.
Positional arguments: $1, $@, $#
Inside a function, $1–$9 refer to the function's own arguments, not the script's. $@ expands to all arguments as separate words; $# is the count:
list_args() {
echo "Received $# arguments"
for arg in "$@"; do
echo " - $arg"
done
}
list_args alpha beta "gamma delta"
# Received 3 arguments
# - alpha
# - beta
# - gamma deltaAlways quote "$@" when passing it through — it preserves per-argument boundaries even when arguments contain spaces.
local variables
Without local, any variable you set inside a function contaminates the global environment:
bad_counter() {
count=0 # sets the global $count
count=$((count + 1))
}
good_counter() {
local count=0 # visible only inside this function
count=$((count + 1))
echo "$count"
}Forgetting local is a common, silent bug. If a function and the script body both use a variable named tmp, result, or i, the function will clobber the caller's value unless local is used. Make local a reflex for every variable you declare inside a function.
Returning values
Bash return only passes a numeric exit code (0–255) — not a string. Return data by printing it and capturing with $():
get_timestamp() {
date +"%Y-%m-%d %H:%M:%S"
}
stamp=$(get_timestamp)
echo "Script started at $stamp"Use the exit code for true/false checks — keeping the function name as a boolean predicate makes the calling code read naturally:
is_even() {
local n=$1
(( n % 2 == 0 )) # arithmetic evaluates to 0 (true) if non-zero result... wait:
# (( expr )) returns 0 if expr != 0, 1 if expr == 0
}
# Actually: (( n % 2 == 0 )) is 0 (true) when n is even
if is_even 4; then echo "even"; fi
if ! is_even 7; then echo "odd"; fi(( expr )) as an exit code: The arithmetic compound (( n % 2 == 0 )) exits 0 (success/true) when the expression is non-zero in the C sense — i.e., when n % 2 == 0 evaluates to 1 (true). This is consistent with [[ ]]: exit 0 means success/true, exit 1 means failure/false.
Function libraries
Large projects put shared functions in a library file and source it at the top of each script:
# lib/utils.sh
log_info() { echo "[INFO] $(date +%T) $*"; }
log_error() { echo "[ERROR] $(date +%T) $*" >&2; }
die() {
log_error "$1"
exit "${2:-1}"
}#!/usr/bin/env bash
source "$(dirname "$0")/lib/utils.sh"
[[ -f "$1" ]] || die "File not found: $1"
log_info "Processing $1"source (or its alias .) runs the library in the current shell — functions and variables become available immediately. The $(dirname "$0") idiom finds the library relative to the script's own location, regardless of where the script is called from.
Check your understanding
- 1.Inside a Bash function, you set result="done" without using local. What happens?
- 2.A function needs to hand a filename string back to its caller. What is the correct approach?
- 3.Inside a function, $1 refers to the script's first argument, not the function's first argument.
Do it yourself
# Define and call a function
greet() {
local name=${1:-"stranger"}
echo "Hello, $name!"
}
greet "Alice"
greet
# Return a value via echo
get_extension() {
local file=$1
echo "${file##*.}" # parameter expansion strips up to last dot
}
ext=$(get_extension "report.csv")
echo "Extension: $ext"
# Source a small library inline
source /dev/stdin <<'EOF'
log() { echo "[$(date +%T)] $*"; }
EOF
log "Library sourced"Where to go next
Functions give you structure. The next lesson — Exit codes — shows how to make scripts fail loudly and correctly with set -e, set -u, trap, and explicit exit codes, turning functions into reliable building blocks.