A Bash function is a named block of commands, reusable inside the script. Without functions, every repeated piece of logic (argument validation, error display, string transformation) must be duplicated. With well-split functions, a 200-line script becomes readable and testable, and utility functions are shared between scripts through source.
What you will learn
- Declare and call a function with and without the
functionkeyword - Access the arguments of a function through
$1,$2,$@ - Use
localto avoid polluting global variables - Return a value through
echocaptured with$() - Return an exit code usable in an
if - Organise utility functions in a separate file and load them with
source
When is it useful?
Functions appear naturally as soon as a script goes beyond about twenty lines:
- wrap the validation logic of an argument in
check_arg()called for each parameter - centralise error handling in
error()so as not to duplicate theecho >&2; exit 1 - extract a transformation (get the extension of a file, normalise a name) into a reusable function
- share logging functions (
log_info,log_warn,log_error) between several scripts thanks tosource
The RHCSA objective "Create simple shell scripts" (EX200, RHEL 10) does not mention functions: they make your scripts readable and maintainable, they do not tick an exam box. A thirty-line exam script does fine without them; a two-hundred-line operations script does not.
Declare and call a function
Two syntaxes exist, and they are not equally portable. The name() { ... } form is the POSIX one: Bash, dash and every shell understand it. The function keyword is specific to Bash and ksh: measured on Debian 13, dash rejects it with Syntax error: "}" unexpected.
# POSIX syntax, recommendedhello() { echo "Hello from a function!"}
# Syntax with the function keyword (Bash and ksh, rejected by dash)function hello { echo "Hello from a function!"}The call is written like a command, without parentheses:
hello# Hello from a function!A function must be declared before its first call: unlike some languages, Bash runs files top to bottom, without a prior pass. Called too early, it causes a command not found.
Function arguments
Functions receive their arguments like a script: $1, $2… $@, $#. They are the function's parameters, distinct from the script's: changing them in the function (with shift or set --) does not touch the script's arguments (measured).
greet() { local name="$1" local title="${2:-Mr}" # default value when $2 is missing echo "Hello $title $name"}
greet "Alice" # Hello Mr Alicegreet "Bob" "Dr" # Hello Dr Bobshift, move the arguments along
shift removes $1 and moves all the following ones down by one. Handy to treat the first argument as a title, then iterate over the others:
print_list() { local title="$1" shift # $2 becomes $1, $3 becomes $2... echo "$title:" for item in "$@"; do echo " - $item" done}
print_list "Services" "nginx" "postgresql" "redis"# Services:# - nginx# - postgresql# - redisLocal variables with local
Without local, an assignment inside a function changes the global variable of the same name, a side effect that is hard to follow:
name="global"
change() { name="changed in the function" # changes the global variable!}
changeecho "$name" # changed in the functionWith local, the scope is limited to the function, and the global stays untouched after the call:
name="global"
change() { local name="local to the function" echo "In the function: $name"}
changeecho "After: $name"# In the function: local to the function# After: globalRule: declare all the internal variables of a function with local, it is the default defensive form.
Return a value
Through echo + $()
Bash has no return text_value. The standard method is to echo the value in the function, then capture it with $() at the call:
extension() { local file="$1" echo "${file##*.}"}
ext=$(extension "report.tar.gz")echo "Extension: $ext"# Extension: gzsnapshot_date() { date '+%Y%m%d_%H%M%S'}
backup="/backup/app_$(snapshot_date).tar.gz"echo "$backup"# /backup/app_20260410_080812.tar.gzA counterpart to know: $( ) runs the function in a subshell. A global variable it changes along the way is therefore lost on return. Measured: with f() { N=5; echo val; }, the call r=$(f) leaves N at 0, whereas a direct call f sets it to 5.
N=0f() { N=5; echo "val"; }
r=$(f)echo "N after \$(f): $N" # N after $(f): 0f > /dev/nullecho "N after f : $N" # N after f : 5Through the exit code, for conditions
return without an argument returns the code of the last command. return N returns code N (0 = success, 1–255 = error). Not to be confused with exit, which stops the whole script even when called in a function (measured: f() { exit 3; }; f ends the script with code 3).
is_readable() { local f="$1" [[ -r "$f" ]] # returns 0 when readable, 1 otherwise}
if is_readable "/etc/passwd"; then echo "/etc/passwd: readable"fi
if ! is_readable "/etc/shadow"; then echo "/etc/shadow: not readable (normal as a user)"fi# /etc/passwd: readable# /etc/shadow: not readable (normal as a user)This pattern is widely used to validate preconditions before acting, on a single readable line:
service_active() { systemctl is-active --quiet "$1"}
service_active "nginx" || { echo "nginx stopped, giving up." >&2; exit 1; }Return several values
Bash does not support multiple returns. The convention is to assign global variables with an explicit prefix, calling the function directly, not inside $( ):
compute() { local a="$1" local b="$2" RESULT_SUM=$((a + b)) RESULT_PRODUCT=$((a * b))}
compute 6 7echo "Sum=$RESULT_SUM, Product=$RESULT_PRODUCT"# Sum=13, Product=42Common utility functions
Error function
A centralised error() function avoids duplicating echo >&2; exit 1 everywhere, and guarantees that every message goes to the error output with the same format:
error() { echo "ERROR: $*" >&2 exit 1}
check_arg() { local val="$1" local name="$2" [[ -n "$val" ]] || error "The argument '$name' is mandatory."}
check_arg "myapp" "APP_NAME" # OKcheck_arg "" "APP_NAME" # ERROR: The argument 'APP_NAME' is mandatory.$* in error() concatenates all the arguments into a single string, handy for a composed error message.
Logging functions
Three one-line functions are enough to timestamp messages and separate the levels: information goes to the standard output, warnings and errors to the error output.
log_info() { echo "[INFO] $(date '+%H:%M:%S') $*"; }log_warn() { echo "[WARN] $(date '+%H:%M:%S') $*" >&2; }log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }
log_info "Application starting"log_warn "Default configuration used"log_error "Database connection failed"# [INFO] 08:08:12 Application starting# [WARN] 08:08:12 Default configuration used# [ERROR] 08:08:12 Database connection failedShared library with source
source (or .) loads and runs a file in the context of the current shell: its functions become available immediately, as if they were written in the script:
error() { echo "ERROR: $*" >&2; exit 1; }log_info() { echo "[INFO] $(date '+%H:%M:%S') $*"; }log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }#!/bin/bashset -euo pipefail
# shellcheck source=lib/utils.shsource "$(dirname "$0")/lib/utils.sh"
log_info "Script started"[[ $# -ge 1 ]] || error "Usage: $(basename "$0") <argument>"log_info "Argument: $1"$(dirname "$0") points to the script's directory, so the library is found even when the script is called from another directory, by cron for example. The # shellcheck source= comment helps static analysis follow the file.
Troubleshooting
Function errors almost all come down to scope: what the function sees, what it changes, and what survives the call. The cases below were reproduced on Bash 5.2.
| Symptom | Likely cause | Solution |
|---|---|---|
| Variable unchanged after the call | Declared with local in the function | Use a global variable with an explicit prefix (RESULT_) |
command not found at the call | Function declared after the call | Move the declaration before the first call |
$(function) returns nothing | The function uses return instead of echo | echo the value; return is only for exit codes |
| A global changed by the function stays unchanged | Function called inside $( ), hence in a subshell | Call the function directly, or return the value through echo |
| The whole script stops inside a function | exit instead of return | return N only leaves the function |
source lib.sh: No such file | Path relative to the current directory | Use source "$(dirname "$0")/lib.sh" |
Key points
- The
function_name() { ... }syntax is the POSIX form, recommended in Bash. - Always declare internal variables with
local, it is the default defensive form. - To return a text value:
echoin the function +$(function)at the call, knowing that$( )runs a subshell. - To return a boolean: the function ends with a command or a
[[ ]]test, usable directly inif. sourceloads a function library into the current shell, the path should be based on$(dirname "$0").
Knowledge check
Check that the essentials of this guide stuck. The questions cover only what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Debug a Bash script: bash -x, set -x, shellcheck
A new full quiz with random questions
Work again only on the questions you missed