Guide Bash-Scripting Intermediate

Functions

Defining and calling Bash functions, passing arguments, return values vs exit codes, local scope, and recursion.

3 min read

Defining and Calling Functions

greet() {
    echo "Hello, $1!"
}

greet "World"     # "Hello, World!"

# Alternative syntax (equivalent)
function greet {
    echo "Hello, $1!"
}

Functions must be defined before they’re called in the script — Bash doesn’t hoist function declarations like some languages do.

Arguments Inside a Function

deploy() {
    local app="$1"
    local environment="$2"
    echo "Deploying $app to $environment"
    echo "Total arguments: $#"
    echo "All arguments: $@"
}

deploy "myapp" "production"

Functions have their own $1, $2, $#, $@ — completely separate from the script’s own positional parameters.

Return Values: Exit Codes vs. Output

Bash functions don’t “return” data the way most languages do — return only sets a numeric exit status (0–255). To get actual data back, print it and capture with command substitution.

# Exit status only
is_valid_env() {
    [[ "$1" =~ ^(dev|staging|production)$ ]]
}

if is_valid_env "production"; then
    echo "Valid environment"
fi

# Returning DATA — echo it, capture with $()
get_current_branch() {
    git rev-parse --abbrev-ref HEAD
}

BRANCH=$(get_current_branch)
echo "Current branch: $BRANCH"

local Scope

COUNTER=0

increment() {
    local COUNTER=100   # shadows the global, doesn't affect it
    ((COUNTER++))
    echo "Inside function: $COUNTER"
}

increment            # "Inside function: 101"
echo "Outside: $COUNTER"   # "Outside: 0" — unaffected

Without local, an assignment inside a function modifies the global variable — a very common source of hard-to-trace bugs.

Functions Calling Other Functions

log() {
    echo "[$(date +'%H:%M:%S')] $*"
}

check_service() {
    local name="$1"
    if systemctl is-active --quiet "$name"; then
        log "$name is running"
        return 0
    else
        log "$name is NOT running"
        return 1
    fi
}

main() {
    check_service "nginx" || exit 1
    check_service "postgresql" || exit 1
    log "All services healthy"
}

main "$@"

Recursion

factorial() {
    local n="$1"
    if (( n <= 1 )); then
        echo 1
    else
        local prev
        prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

factorial 5    # 120

Recursion works but is rarely idiomatic in Bash — each level spawns a subshell via command substitution, making it slow. Prefer loops for anything performance-sensitive.

Production Considerations

  • Structure production scripts around a main() function called at the bottom with main "$@" — it makes the script’s entry point explicit and testable in isolation.
  • Always use local for function-internal variables to avoid silently corrupting global state.
  • Remember functions “return” only an exit code (0–255) — for actual data, echo the value and capture it with $(function_name).

Quick Interview Answer

“Bash functions are defined with name() { ... } and must be declared before use. return only sets a numeric exit code, not data — to get data back, the function echoes a value and the caller captures it with command substitution ($(function_name)). Always use local for variables inside a function, since without it, assignments leak into the global scope.”

Common Mistakes

  • Trying to return "some string" and expecting it to work like a normal return value — it silently truncates to an exit code or errors.
  • Forgetting local, causing a function to unintentionally overwrite a global variable of the same name.
  • Calling a function before it’s defined in the script, resulting in “command not found.”

Add More Questions to This Guide

Know a question that should be here? Share it and help the community!

Open Google Form