Bash Functions: How to Write, Call, and Return Values

March 17, 2026

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.

main script call + args call + args greet_user() calculate() return value return value
Functions receive arguments from the main script and pass results back via echo or return codes

1. Declaring Functions

Bash supports two equivalent declaration syntaxes:

bash
#!/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, $@, $#

bash
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.

bash
#!/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.