Welcome to the fourth part of our technical series on Linux and Bash programming!
In the first three modules of our basics course, we learned how to create and secure scripts, how variables and parameter expansions work, and how to control program flow through control structures and loops.
However, as script complexity grows, unmaintainable, redundant code ("spaghetti code") quickly emerges. When the same validation, the same log entry, or the same database query is repeated in ten different places, errors creep in and maintenance becomes a nightmare. The solution in professional scripting is: modularization through functions (DRY principle: Don't Repeat Yourself).
In this lesson, you will learn how to cleanly declare Bash functions, pass arguments, encapsulate variables with local to protect against global side effects, master the fundamental difference between return (status codes) and echo (data return), and create your own reusable function libraries.
Declaration and Calling Bash Functions
A function is a named, reusable code block within your script. For the calling shell, it behaves almost exactly like a standalone command or a shell built-in.
┌─────────────────────────────────────────────────────────────┐
│ CONTROL FLOW ON FUNCTION CALLS │
├─────────────────────────────────────────────────────────────┤
│ │
│ [ Main program ] │
│ Command A │
│ ├── Call: log_info "Start" ──┐ │
│ │ ▼ │
│ │ [ Function: log_info() ] │
│ │ - Reads parameter $1 │
│ │ - Outputs timestamp & text │
│ │ - return 0 │
│ │ │ │
│ ◄────────────────────────────┘ │
│ Command B (continues) │
│ │
└─────────────────────────────────────────────────────────────┘
Syntax Forms Compared
Bash provides three different ways to define a function:
# 1. POSIX-compliant syntax (RECOMMENDED):
my_function() {
echo "Executing the function..."
}
# 2. Using the 'function' keyword (Bash-specific):
function my_function {
echo "Executing the function..."
}
# 3. Combination (Bash-specific, redundant):
function my_function() {
echo "Executing the function..."
}
💡 Best practice: Use the standard form
functionname() { ... }. It is short, readable, 100% POSIX-compliant, and works seamlessly in Bash, Dash, and Zsh.
Calling a Function
Unlike programming languages such as Python, C, or PHP, functions in the shell are called without parentheses:
# Definition
greet() {
echo "Hello from the function!"
}
# Call (simply the name like a normal Linux command):
greet
Parameter Passing: Processing Arguments in Functions
Bash functions have their own isolated positional parameters. When you pass arguments to a function, $1, $2, $#, and "$@" within the function body are temporarily populated with the values of the function call – independent of the original script arguments.
#!/usr/bin/env bash
set -euo pipefail
# Function expects 2 parameters: first name and last name
greeting() {
local firstname="$1"
local lastname="$2"
echo "Hello $firstname $lastname! Great to have you here."
echo "Number of parameters passed to the function: $#"
}
# Call with two arguments:
greeting "Max" "Mustermann"
Important Parameter Variables Within Functions:
| Variable | Meaning in Function Context |
|---|---|
$1, $2 .. $9 |
First, second to ninth argument of the function |
${10}, ${11} |
Arguments from position 10 onwards (curly braces mandatory!) |
$# |
Number of all arguments passed to the function |
"$@" |
All function arguments as separately protected parameters |
$FUNCNAME |
Name of the currently executed function (ideal for logging) |
Processing Parameters Sequentially with shift
With the shift command, you shift the parameter list one position to the left ($2 becomes $1, $3 becomes $2, etc.):
copy_files() {
local target_folder="$1"
shift # Remove first argument (target folder)
# The rest in "$@" are now all source files:
for file in "$@"; do
echo "Copying $file to $target_folder..."
cp "$file" "$target_folder/"
done
}
copy_files "/backup" "app.log" "db.sql" "config.ini"
Scope: Enforcing Local Variables with local
In Bash, variables are always global by default. If you declare a variable inside a function without the local keyword, you silently overwrite same-named variables in the main program!
┌─────────────────────────────────────────────────────────────┐
│ SCOPING MATRIX: GLOBAL VS. LOCAL │
├─────────────────────────────────────────────────────────────┤
│ │
│ [ Script level ] │
│ STATUS="ONLINE" │
│ │ │
│ ├── Function call: check() │
│ │ ├── local TEMP="123" ──> Only visible HERE │
│ │ └── STATUS="OFFLINE" ──> Globally modified │
│ │ │
│ ▼ │
│ After the function: │
│ ├── TEMP is deleted (no longer exists) │
│ └── STATUS is now permanently "OFFLINE" (bug source!) │
│ │
└─────────────────────────────────────────────────────────────┘
Practical Example: Avoiding Fatal Side Effects
#!/usr/bin/env bash
set -euo pipefail
# Global counter variable in main program
i=100
counter_function() {
# WITHOUT 'local', the global variable $i would be set to 0 here!
local i=0
while [[ $i -lt 3 ]]; do
echo "Function loop: $i"
(( i++ ))
done
}
echo "Before: i = $i" # Outputs 100
counter_function
echo "After: i = $i" # Still outputs 100 (thanks to 'local'!)
⚠️ Safety rule: Within functions, declare every helper variable with
localwithout exception. This prevents unpredictable bugs in larger scripts.
Return Values: return (Status Codes) vs. echo (Data Return)
One of the biggest misunderstandings for Bash beginners is how return works:
return [0-255]: Returns no text data, only a numeric exit status code (0 to 255) to the shell.- Returning data/results: Done via standard output (
echo) and captured with command substitution$(function).
┌─────────────────────────────────────────────────────────────┐
│ RETURN METHODS COMPARED │
├─────────────────────────────────────────────────────────────┤
│ │
│ Method 1: return (status code for true/false) │
│ ├── Range: 0 (success) to 255 (error) │
│ └── Use: Direct use in if conditions │
│ │
│ Method 2: echo + command substitution (text / data) │
│ ├── Range: Any strings, numbers, arrays │
│ └── Use: RESULT=$(my_function "arg") │
│ │
└─────────────────────────────────────────────────────────────┘
1. return for Status Queries (Predicate Functions)
Functions that perform logical checks use return 0 for success (true) and return 1 for error (false):
# Check if the script is running with root privileges
is_root() {
if [[ $EUID -eq 0 ]]; then
return 0 # True / Success
else
return 1 # False / Error
fi
}
# Direct use in an if branch:
if is_root; then
echo "Script is running as administrator (root)."
else
echo "Error: This script requires root privileges!" >&2
exit 1
fi
2. Returning Data & Values via Command Substitution
When a function should produce a string or calculation result, it outputs the value via echo:
calculate_net() {
local gross="$1"
local tax_rate="$2"
# Mathematical integer calculation
local net=$(( gross * 100 / (100 + tax_rate) ))
echo "$net" # Text output on stdout
}
# Capture result in a variable:
NET_AMOUNT=$(calculate_net 119 19)
echo "Net amount: ${NET_AMOUNT} EUR"
Modularization: Including Function Libraries with source
In professional Linux environments, general helper functions (like logging, configuration parsing, or network checks) are outsourced into separate library files and loaded into scripts via source (or .).
1. Create a Logging Library (lib/logging.sh):
# lib/logging.sh - Reusable logging library
log_info() {
echo -e "\e[1;32m[INFO]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $*"
}
log_warn() {
echo -e "\e[1;33m[WARN]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}
log_error() {
echo -e "\e[1;31m[ERROR]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}
2. Include the Library in the Main Script (app.sh):
#!/usr/bin/env bash
set -euo pipefail
# Determine absolute path of the script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source the library
# shellcheck source=lib/logging.sh
source "$SCRIPT_DIR/lib/logging.sh"
log_info "Starting application backup..."
log_warn "Disk space on /backup is 85% full."
log_error "Could not establish database connection!"
Recursive Functions in Bash
A function can call itself (recursion). The key here is always a clear termination condition to avoid an infinite loop and stack overflow.
Example: Calculating Factorial ($n!$)
factorial() {
local n="$1"
# Termination condition: 0! = 1 and 1! = 1
if [[ $n -le 1 ]]; then
echo 1
return 0
fi
# Recursive call: n * factorial(n - 1)
local previous
previous=$(factorial $(( n - 1 )))
echo $(( n * previous ))
}
echo "Factorial of 5 is: $(factorial 5)" # 120
Exercises & Practice Check
❗ Practice tasks for lesson #4:
- Task 1 (Host Reachability Checker):
Write a function
check_host()that accepts a hostname or IP as parameter$1, executesping -c 1 -W 2 "$1", and returnsreturn 0on success orreturn 1if unreachable.
- Task 2 (Universal Calculator):
Create a function
calc()that expects three arguments (Number1,Operator,Number2), performs the operation (+,-,*,/) withcase, and returns the result viaecho. Catch division by zero with an error message andreturn 1.
- Task 3 (String Trimmer):
Write a function
trim()that removes leading and trailing whitespace from a passed string.
Sample Solution for Task 2:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
calc() {
local a="${1:?First number missing}"
local op="${2:?Operator missing (+, -, *, /)}"
local b="${3:?Second number missing}"
case "$op" in
+)
echo $(( a + b ))
;;
-)
echo $(( a - b ))
;;
\*)
echo $(( a * b ))
;;
/)
if [[ $b -eq 0 ]]; then
echo "Error: Division by zero not allowed!" >&2
return 1
fi
echo $(( a / b ))
;;
*)
echo "Error: Unknown operator '$op'!" >&2
return 1
;;
esac
}
# Test run
echo "Result: $(calc 42 + 8)"
echo "Result: $(calc 100 / 4)"
Command Reference (Cheatsheet)
| Syntax / Command | Description & Function |
|---|---|
name() { ... } |
Standard function declaration in Bash |
name "arg1" "arg2" |
Function call with two arguments (without parentheses!) |
local VAR="value" |
Declares a variable with local function scope |
$1, $2, "$@" |
Access to the function's positional parameters |
shift |
Shifts parameter list one position to the left |
return 0 |
Ends the function with success status (0 to 255) |
return 1 |
Ends the function with error status |
VAR=$(func) |
Captures text output (stdout) of the function into variable |
source lib.sh |
Sources external function library into the script |
$FUNCNAME |
Contains the name of the currently executed function |
Further Resources
| Resource | Description |
|---|---|
| GNU Bash Shell Functions | Official GNU reference on function declaration and scopes |
| Bash Basics #3: Control Structures | The previous part: Branching, tests, and loops |
| Linux Command Line Processor Guide | Fundamental knowledge on shells, I/O streams, and pipes |
| chmod and File Permissions | Linux permission concepts in detail |
Conclusion
Functions are the decisive step from simple linear scripts to professional software architecture in Linux administration. Through clean encapsulation of variables with local, clear separation of error return codes (return) and data (echo), and outsourcing into modular libraries, your scripts become clear, robust, and future-proof.
💡 Tip: Create a central
lib/folder for recurring functions like logging, configuration checks, or user validations. This saves valuable time with every new script and standardizes error handling across your servers.
In the next lesson, we focus on how your scripts communicate with the outside world: 👉 Next up: Bash Basics #5: Input and Output in Bash
👉 Course overview: All lessons of the Bash basics course