Linux Basic — Function Organization

1 minute read

Function Organization

Large scripts are hard to debug. Break them into single-purpose functions with clear inputs and outputs.


The Pattern

#!/usr/bin/env bash
set -euo pipefail

# ========== Configuration ==========
readonly MAX_RETRIES=3
readonly TIMEOUT=30

# ========== Helper Functions ==========
log() { echo "$(date '+%F %T') $*" >&2; }
die() { log "ERROR: $*"; exit 1; }

validate_input() {
    [[ -n "$1" ]] || die "input required"
}

# ========== Core Logic ==========
process_file() {
    local file=$1
    log "processing $file"
    # ... work
}

cleanup() {
    log "cleaning up"
    rm -f "$TMPFILE"
}

# ========== Main ==========
main() {
    trap cleanup EXIT
    validate_input "$@"
    process_file "$1"
}

main "$@"

Principles

1. Single Responsibility

Each function does one thing:

# BAD — does three things
do_everything() {
    validate_input
    fetch_data
    send_report
}

# GOOD — three functions
validate_input() { ... }
fetch_data() { ... }
send_report() { ... }

2. Helper Functions First

Define utilities (log, die, retry) before the functions that use them. Read top-to-bottom.

3. main() Last

The entry point is at the bottom, making the script testable:

main() {
    parse_args "$@"
    do_work
}

# only run main if executed, not sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

This lets you source script.sh in tests without running main.


Function Interfaces

  • Use positional args for required inputs
  • Use return codes (0/1) for success/failure
  • Use stdout for output, stderr for logging
get_user_count() {
    local db=$1
    psql -d "$db" -tAc "SELECT count(*) FROM users"
}

count=$(get_user_count "prod") || die "query failed"
echo "users: $count"

Avoid Global Side-Effects

Functions should not modify globals unless they explicitly say so in their name:

# BAD — sneaky global change
process_user() {
    user_count=$((user_count + 1))
}

# GOOD — name signals intent
increment_user_count() {
    user_count=$((user_count + 1))
}

Or better: return the new value and let caller decide:

get_incremented_count() {
    echo $((user_count + 1))
}
user_count=$(get_incremented_count)

When to Split into Multiple Files

If the script is > 500 lines, consider splitting:

# lib/logging.sh
log() { ... }
die() { ... }

# lib/validation.sh
validate_email() { ... }

# main script
source "$(dirname "$0")/lib/logging.sh"
source "$(dirname "$0")/lib/validation.sh"

Next → ShellCheck Compliance

Updated: