Functions
Part of Essentials — Bash Scripting
Sixth and final article in the Essentials Bash series. Assumes you understand Loops. The next step is the Efficiency tier, which covers production-grade patterns like set -euo pipefail, signal handling, and structured logging.
As scripts grow past 20-30 lines, repeated logic becomes a maintenance problem. A check you run in three places has to be updated in three places. Functions solve this: write the logic once, call it from anywhere, and give it a name that makes the script self-documenting.
Where You've Seen This
If you've ever sourced a setup script (source ~/.bashrc or . ./env-setup.sh), you've already used a function library — that file defines functions your shell loads and can call by name. Writing your own is the same pattern.
Defining and Calling Functions
name() { }is the standard form. An alternativefunction name { }syntax exists but adds nothing — the first is what you'll see in most scripts.
Call a function exactly like any other command:
| Calling a Function | |
|---|---|
- Arguments work the same as script arguments —
$1,$2,"$@"inside the function refer to what was passed here.
Define before you call. Bash reads top to bottom — a function must appear in the file before any line that calls it. The standard pattern: define all functions at the top, put the calling code at the bottom.
Arguments and Local Variables
Functions receive arguments exactly as scripts do — $1, $2, $@, $#. Always declare function variables with local — without it, they're global and will overwrite variables with the same name in the main script or other functions:
| local Variables | |
|---|---|
- No
local— this modifies the globalcounter. localcreates a separate variable scoped to this function. The global is untouched.- Output:
11—incrementchanged the global. - Output:
11—safe_resetdid not, because itscounterwas local.
Rule: declare all function variables with local.
Return Values
Bash functions return exit codes (integers 0–255), not values. How you get data back to the caller depends on what you need to return:
The natural way to signal pass/fail. Works directly with if, &&, ||, and the guard-first patterns from Conditionals:
| Return via Exit Code | |
|---|---|
- The last command's exit code becomes the function's return value.
ncexits 0 if the port is open, non-zero if not — so the function inherits that result automatically.
When you need to return a string rather than just pass/fail, echo the value and capture it with command substitution in the caller:
| Return a String via echo | |
|---|---|
echoto stdout is the only way to return a string from a Bash function.- The caller captures it with
$()— the same command substitution used anywhere else.
Use only when a function needs to return multiple values. It creates invisible coupling between the function and its callers — document it clearly:
| Return via Global Variable | |
|---|---|
- Sets globals directly — uppercase names signal that these are intentional globals.
- After the call,
MAJOR,MINOR, andPATCHare available in the caller's scope.
Practical Function Patterns
A logging function is the most universally useful thing to add to any script — timestamps and severity without repeating date everywhere:
| Structured Logging | |
|---|---|
$*joins all arguments into one string — appropriate for a log message, which is a single unit.
Extracting guard checks into named functions keeps main() readable — the top-level flow reads as intent, not implementation:
For any script beyond a few functions, wrap the entry point in main() and call it at the bottom. This means nothing runs on source — every top-level statement is a function definition until main "$@":
- The only line that runs directly — passes all script arguments to
main. Everything above is a definition.
Sourcing Function Libraries
When functions are useful across multiple scripts, put them in a shared file and load it with source:
| lib/functions.sh | |
|---|---|
| deploy.sh | |
|---|---|
$(dirname "$0")resolves to the directory containing the running script — a reliable way to find sibling files without hardcoding absolute paths.
When this pattern grows to multiple sourced libraries shared across repos, that's usually the signal to cross over. The main()/library structure maps directly to Python modules — see My Bash Script Is Getting Out of Hand.
Practice Exercises
Exercise 1: Refactor to Functions
This script has repetitive code. Refactor it using a function:
| Before — Repetitive | |
|---|---|
Exercise 2: Function That Returns a Value
Write a function called get_disk_usage that:
- Accepts a directory path as its argument
- Returns (via
echo) the disk usage as a percentage — just the number, e.g.63 - In
main(), call the function and print:"/ is 63% full"
Quick Recap
name() { }is the standard function syntax — define before you call- Always use
localfor function variables — undeclared variables are global and will cause subtle bugs - Function arguments work exactly like script arguments:
$1,$2,"$@" - Return pass/fail via exit code; return strings via
echo+$(); avoid globals except for multiple return values main "$@"at the bottom of a script: the only line that runs directly, everything else is a definition- Shared functions go in a library file, loaded with
source "$(dirname "$0")/lib.sh"
Further Reading
Command References
man bash— the "Functions" section and thelocalandsourcebuiltinshelp local— Bash built-in help for thelocalkeywordhelp source— howsource(or.) loads function files
Deep Dives
- Google Shell Style Guide: Functions — naming conventions, structure, and when to use functions
- BashGuide: Functions — Wooledge guide to functions, local scope, and practical usage
Official Documentation
Exploring Python
- My Bash Script Is Getting Out of Hand — When functions, sourced libraries, and argument handling outgrow Bash: the migration path to Python with proper module structure
What's Next?
You've covered the complete Bash scripting foundation: scripts, variables, arguments, conditionals, loops, and functions. The Efficiency tier builds on these with patterns for production-grade scripts — set -euo pipefail, getopts, signal handling, and structured logging — coming soon.