Functions๐
Part of a deep dive: Bash Scripting
Consult the map
-
Bash Scripting โ step 6 of 6
โ Loops ยท you are here ยท (last step) โ
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 Might Have 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:
flowchart TD
Q["What does the caller need back?"] --> A["Just pass/fail"]
Q --> B["A single string or number"]
Q --> C["Multiple values at once"]
A --> A2["Exit code โ<br/>works directly with if"]
B --> B2["echo, captured with $(...)"]
C --> C2["Global variables โ<br/>document the coupling"]
style Q fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#fff
style A fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style B fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style C fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style A2 fill:#2f855a,stroke:#cbd5e0,stroke-width:2px,color:#fff
style B2 fill:#2f855a,stroke:#cbd5e0,stroke-width:2px,color:#fff
style C2 fill:#2f855a,stroke:#cbd5e0,stroke-width:2px,color:#fff
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