Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Bash functions: declare, call, return

By updated

Improve this page

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 function keyword
  • Access the arguments of a function through $1, $2, $@
  • Use local to avoid polluting global variables
  • Return a value through echo captured 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 the echo >&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 to source

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.

Terminal window
# POSIX syntax, recommended
hello() {
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:

Terminal window
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).

Terminal window
greet() {
local name="$1"
local title="${2:-Mr}" # default value when $2 is missing
echo "Hello $title $name"
}
greet "Alice" # Hello Mr Alice
greet "Bob" "Dr" # Hello Dr Bob

shift, 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:

Terminal window
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
# - redis

Local 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:

Terminal window
name="global"
change() {
name="changed in the function" # changes the global variable!
}
change
echo "$name" # changed in the function

With local, the scope is limited to the function, and the global stays untouched after the call:

Terminal window
name="global"
change() {
local name="local to the function"
echo "In the function: $name"
}
change
echo "After: $name"
# In the function: local to the function
# After: global

Rule: 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:

Terminal window
extension() {
local file="$1"
echo "${file##*.}"
}
ext=$(extension "report.tar.gz")
echo "Extension: $ext"
# Extension: gz
Terminal window
snapshot_date() {
date '+%Y%m%d_%H%M%S'
}
backup="/backup/app_$(snapshot_date).tar.gz"
echo "$backup"
# /backup/app_20260410_080812.tar.gz

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

Terminal window
N=0
f() { N=5; echo "val"; }
r=$(f)
echo "N after \$(f): $N" # N after $(f): 0
f > /dev/null
echo "N after f : $N" # N after f : 5

Through 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).

Terminal window
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:

Terminal window
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 $( ):

Terminal window
compute() {
local a="$1"
local b="$2"
RESULT_SUM=$((a + b))
RESULT_PRODUCT=$((a * b))
}
compute 6 7
echo "Sum=$RESULT_SUM, Product=$RESULT_PRODUCT"
# Sum=13, Product=42

Common 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:

Terminal window
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" # OK
check_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.

Terminal window
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 failed

Shared 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:

lib/utils.sh
error() { echo "ERROR: $*" >&2; exit 1; }
log_info() { echo "[INFO] $(date '+%H:%M:%S') $*"; }
log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }
my_script.sh
#!/bin/bash
set -euo pipefail
# shellcheck source=lib/utils.sh
source "$(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.

SymptomLikely causeSolution
Variable unchanged after the callDeclared with local in the functionUse a global variable with an explicit prefix (RESULT_)
command not found at the callFunction declared after the callMove the declaration before the first call
$(function) returns nothingThe function uses return instead of echoecho the value; return is only for exit codes
A global changed by the function stays unchangedFunction called inside $( ), hence in a subshellCall the function directly, or return the value through echo
The whole script stops inside a functionexit instead of returnreturn N only leaves the function
source lib.sh: No such filePath relative to the current directoryUse 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: echo in 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 in if.
  • source loads 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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn