Bash Functions: How to Write, Call, and Return Values
March 17, 2026
Bash functions let you write reusable blocks of code that can be called with arguments and return results. They're essential for any script longer than a few dozen lines — they reduce repetition, make scripts easier to test, and help you structure complex automation. This guide covers both declaration syntaxes, argument handling, the return value problem and its solutions, variable scope, and how to build reusable function libraries.
1. Declaring Functions
Bash supports two equivalent declaration syntaxes:
#!/bin/bash
# Style 1: function keyword (explicit, readable)
function greet() {
echo "Hello, $1!"
}
# Style 2: POSIX-compatible (works in sh, dash, zsh too)
greet() {
echo "Hello, $1!"
}
# Call the function
greet "Alice" # Hello, Alice!
greet "Bob" # Hello, Bob!
Both styles are functionally identical in Bash. Prefer Style 2 (name() {}) for portability — it works in all POSIX shells.
2. Arguments: $1, $2, $@, $#
function describe() {
echo "Function name: ${FUNCNAME[0]}"
echo "Number of args: $#"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $@"
}
describe "hello" "world" "foo"
# Function name: describe
# Number of args: 3
# First arg: hello
# Second arg: world
# All args: hello world foo
# Iterate over all arguments
function print_all() {
for arg in "$@"; do
echo " - $arg"
done
}
print_all "alpha" "beta" "gamma"
3. Returning Values: The Problem and the Solution
The return keyword in Bash only returns an integer exit code (0-255), not a string or number. This confuses many developers coming from other languages.
#!/bin/bash
# WRONG: return only sends exit codes (0-255)
function add() {
return $(( $1 + $2 )) # works only if result <= 255
}
add 10 20
echo "$?" # 30 — only works by accident for small numbers
# RIGHT: echo the result and capture with $()
function add() {
echo $(( $1 + $2 ))
}
result=$(add 100 200)
echo "$result" # 300
# Return exit code for success/failure signaling
function file_exists() {
[[ -f "$1" ]] # returns 0 if true, 1 if false (no explicit return needed)
}
if file_exists "/etc/hosts"; then
echo "File exists"
fi
4. Local Variables and Scope
Without local, any variable set inside a function modifies the global scope — a common source of bugs in longer scripts.
#!/bin/bash
counter=10
function increment() {
local counter=0 # local: doesn't affect the global counter
counter=$(( counter + 1 ))
echo "Inside: $counter" # Inside: 1
}
increment
echo "Outside: $counter" # Outside: 10 (unchanged)
# Without local — global gets modified
function bad_increment() {
counter=$(( counter + 1 ))
}
bad_increment
echo "After bad: $counter" # After bad: 11
5. Sourcing Function Libraries
Keep reusable functions in separate files and source them into your scripts:
# lib/utils.sh — shared functions
function log_info() {
echo "[INFO] $(date '+%H:%M:%S') $*"
}
function log_error() {
echo "[ERROR] $(date '+%H:%M:%S') $*" >&2
}
function require_command() {
command -v "$1" >/dev/null 2>&1 || {
log_error "Required command not found: $1"
exit 1
}
}
# main.sh — source the library
source "$(dirname "$0")/lib/utils.sh"
# or: . "$(dirname "$0")/lib/utils.sh"
require_command "curl"
log_info "Starting process..."
6. Recursive Functions
function factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
else
local prev
prev=$(factorial $(( n - 1 )))
echo $(( n * prev ))
fi
}
echo $(factorial 5) # 120
echo $(factorial 10) # 3628800
For functions that check file existence or validate arguments, pair this guide with the bash check if file exists guide and the bash exit codes article for proper error handling patterns.
Summary
Define functions with name() { ... }, pass arguments via $1 $2 $@, return real values by echoing and capturing with $(), use local for all variables inside functions, and source shared function files with source lib.sh. Follow the one-function-one-purpose rule and your scripts will stay maintainable.