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