Learn BASH Scripting - Introduction to and Explanation of Functions
Episode 13 of 27

Learn BASH Scripting - Introduction to and Explanation of Functions

Wrapping repetitive logic into reusable blocks with functions: declaration syntax, global vs `local` variable scope, `$1 $2` arguments, and return values via `return` and `echo` + command substitution. Includes building a function library for logging, file checks, and root verification along with the traps that commonly catch people.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

In episode 12 we mastered the while and until loops and reading files line by line. In this episode we tackle a problem you're surely starting to feel: code that keeps getting longer and repeating itself. The same patterns — writing logs, checking whether a file exists, validating root access — start appearing over and over in different parts of your scripts.

This is where functions come in. Imagine a professional kitchen: no cook rewrites a recipe from scratch every time an order comes in. The recipe is stored once and called by name — "ayam bakar" — whenever needed. Functions are those recipes: you write the logic once, give it a name, then call it as often as you like from anywhere.

Functions transform a script from a mere linear list of commands into structured architecture: logic gets wrapped, named, tested once, and used in many places. This is the same DRY (Don't Repeat Yourself) principle that applies in all programming languages. In this episode we'll dissect Bash function syntax, variable scope, how to send and retrieve values, and build a function library reusable across scripts.

Main Discussion

Why Functions?

Before discussing syntax, let's understand what problem functions solve. Consider its three main benefits:

  1. Reusability — logic is written once and used many times. When the logging rule changes, you change one place, not ten places.
  2. Readability — scripts packed with long command blocks are hard to read. Functions turn do_a_lot_of_stuff() into a self-describing name, so the script's main flow stays concise.
  3. Testability — small, independent functions can be tested one by one before being combined, far easier than debugging one monolithic 500-line script.

Without functions, your scripts keep piling up duplication like photocopies stacking up on a desk — one change to a logging rule means finding and fixing every photocopy. With functions, one fix at the source fixes every caller.

Basic Syntax: nama() { ... }

There are two ways to write functions in Bash — the POSIX style nama() and the style with the function keyword:

Two function declaration styles
sapa() {
    echo "Halo, selamat datang!"
}
 
function ucap_terima_kasih {
    echo "Terima kasih sudah datang!"
}
The nama() style is more portable, the function style is more explicit

For projects that must be portable across shells, the nama() style is the safest choice. The function style is a Bash extension that doesn't exist in POSIX sh (remember: you can run bash script.sh, but a script targeting sh must avoid it).

Calling a function is as easy as writing its name — without parentheses:

Calling a function
sapa() {
    echo "Halo, selamat datang!"
}
 
sapa
sapa
sapa
# Output: tiga kali "Halo, selamat datang!"
Call a function with just its name, without ()

Two important notes that set Bash apart from most other languages:

  • Calls without ()sapa() is only for declaring, not calling. To call, just use sapa.
  • Functions must be declared before being called. Bash executes the script line by line from the top. If sapa is called before the sapa() declaration, Bash looks for a command named sapa — and since there is none, you get a command not found error.

Variable Scope: Global vs local

This is one of the most important differences between Bash and modern languages: variables in Bash are global by default — even inside functions. A variable changed inside a function keeps its new value after the function finishes:

Global variables change inside functions
warna="biru"
 
catat_warna() {
    warna="merah"
    echo "Di dalam fungsi: $warna"
}
 
catat_warna
echo "Di luar fungsi : $warna"
# Di dalam fungsi: merah
# Di luar fungsi : merah   <- nilainya ikut berubah!
Changing warna inside the function = changing warna outside

This is a side effect that's often unwanted — a function accidentally overwriting a global variable can corrupt script state mysteriously. The solution: declare variables used only inside the function with the local keyword:

Locking variables with local
warna="biru"
 
catat_warna() {
    local warna="merah"
    echo "Di dalam fungsi: $warna"
}
 
catat_warna
echo "Di luar fungsi : $warna"
# Di dalam fungsi: merah
# Di luar fungsi : biru   <- aman!
local keeps the outside warna blue

local makes the variable live only while the function runs and disappear afterwards — like a sticky note thrown away once the task is done, leaving no trace on the desk.

Important

Always use local for variables only needed inside a function. This is the discipline that separates amateur script writers from professionals. Global variables outside functions are still fine — for shared configuration like LOG_DIR or TIMEOUT — but never let a function pollute variables unintentionally. The rule: default to local; global only for things genuinely shared.

Function Arguments: $1, $2, etc.

A rigid function — always doing the same thing — is only half useful. To be flexible, a function needs to accept input. The way: give arguments when calling, and read them inside the function via $1, $2, $3:

A function with arguments
tambah() {
    local a="$1"
    local b="$2"
    local hasil=$((a + b))
    echo "Hasil: $a + $b = $hasil"
}
 
tambah 5 3
tambah 100 200
Function arguments are local; they don't disturb the script's $1

Notice two important things:

  1. $1, $2 inside a function refer to the function's arguments, not the script's arguments. This is a very common mistake: beginners read $1 inside a function and assume it's the script's first argument. In fact, when the function is called, $1 is temporarily replaced with the function's first argument — and is restored after the function finishes. This is why these variables are safe to use inside a function without local.
  2. There's no hard limit on the number of arguments. You can pass $1 through $9, then $10, $11, and so on — and "$@" inside the function holds all the function's arguments as a separate list.

Tip

For functions that accept many arguments, immediately copy them to named variables at the top: local src="$1"; local dst="$2". This turns mysterious $1, $2 into self-describing src and dst — and keeps the original arguments unchanged if another function call happens mid-way. A good function is one whose first 5 lines document what it accepts.

return vs Returning Strings

Functions in Bash can "give results back" in two very different ways, and understanding the difference will save you from confusing bugs.

return returns an exit status — not a value. It accepts a number 0255, and that number becomes the function's exit status, readable with $? or tested in a condition:

return returns an exit status
cek_file() {
    if [ -f "$1" ]; then
        return 0
    fi
    return 1
}
 
if cek_file "/etc/hostname"; then
    echo "File ditemukan."
else
    echo "File tidak ditemukan."
fi
return 0 = success, return 1 = failure

return 0 signals success, return with any other value (usually 1) signals failure. This is why a function can be used directly inside if — Bash tests its exit status.

To return data (a string), use echo + command substitution:

Returning a string with echo
get_ext() {
    local file="$1"
    echo "${file##*.}"
}
 
ekstensi=$(get_ext "laporan.pdf")
echo "Ekstensi: $ekstensi"
# Ekstensi: pdf
Command substitution captures the function's output as the value

Here get_ext doesn't formally "return" anything — it prints to stdout, and $( ... ) (command substitution, already covered in the expansion episode) captures all that output as a string.

Warning

Never try to return a string through returnreturn "halo" will error (numeric argument required) because return only accepts exit status numbers. And don't mix the two styles: if a function must return a string, don't add helper echo statements in the middle, because every echo will be captured by command substitution too. Choose one: the function is a status returner (return), or a data returner (echo) — not both mixed.

Practice: Building a Function Library

Now let's tie it all together in a very real case study: a function library (lib.sh) that can be sourced from any script. The library contains three functions every production script needs: writing timestamped logs, checking file existence, and verifying root access:

lib.sh — common function library
#!/usr/bin/env bash
 
log_info() {
    local msg="$1"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  : $msg"
}
 
log_error() {
    local msg="$1"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR : $msg" >&2
}
 
is_file() {
    [ -f "$1" ]
}
 
check_root() {
    if [ "$(id -u)" -ne 0 ]; then
        log_error "Script harus dijalankan sebagai root."
        return 1
    fi
    return 0
}
Every variable local, every result via return or echo

The way to use this library from a main script is with source (alias .), then call its functions as if they were defined right there:

A main script using the library
#!/usr/bin/env bash
source "$(dirname "$0")/lib.sh"
 
if ! check_root; then
    exit 1
fi
 
if is_file "/etc/hostname"; then
    log_info "/etc/hostname ada, lanjut konfigurasi."
else
    log_error "/etc/hostname tidak ditemukan."
    exit 1
fi
source makes lib.sh's functions available in this script

Notice how the patterns we've learned come together here:

  • check_root uses $() (command substitution) to capture id -u's output, then compares it with 0.
  • is_file is a test function — it executes [ -f "$1" ] and the test's exit status becomes the function's exit status. Concise and idiomatic.
  • log_error writes to stderr (>&2) — an important decision: error logs are distinguished from normal logs so they can be separated in pipelines and log files.
  • source "$(dirname "$0")/lib.sh" loads the library from the script's own directory, no matter where the script is called from.

Common Mistakes in Functions

MistakeSymptomSolution
Calling a function before it's declaredcommand not foundDeclare the function above its calls
Forgetting localThe function's variables pollute globalslocal for all internal variables
return "string" for datanumeric argument requiredreturn only takes status numbers; use echo for strings
Using the script's $1 inside a functionWrong/unexpected arguments$1 in a function is the function's argument, copy to a clear name
function in a script targeting shError in other shellsUse the nama() style
Function using unexpected global variablesObscure bug that's hard to traceCheck all function variables are local

Note

There's another scoping trap often treated as mystical: a function called inside a pipeline also runs in a subshell (remember episode 12). If a function changes a global variable, then is called from within cat file | my_func, its changes are lost as soon as the pipeline finishes. This is the same subshell behavior — not a Bash bug. For functions that must change global state, call them outside a pipeline or use the same techniques as the episode 12 subshell solutions.

Conclusion

In this episode 13 we changed how you write scripts: from linear command lists to function-based architecture. We covered the nama() { ... } syntax, why functions matter (reuse, readability, testability), variable scope with local — mandatory for internal variables, passing arguments via $1 $2, and the two ways to "return values" — return for exit status and echo + command substitution for strings. All of it was assembled into a lib.sh function library ready to be sourced by any script.

The principle to take home: a good function is small, clearly named, keeps all its variables local, and has only one way to return a result. If a function exceeds 40 lines or does three things at once, break it up.

Now that you can wrap logic, there's one ingredient still missing for advanced automation: storing lots of data in a single variable. In the next episode, episode 14, we'll cover indexed arrays — how to store a list of values in one variable, access them by index, iterate over all elements, add, slice, and even build a list from command output. That's where server lists, backup file lists, and other data collections become light to handle. See you in the next episode!

Learn BASH Scripting - Introduction to and Explanation of Functions | Learn BASH Scripting