Linux Basic — Function Organization
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