---
id: bash-basics-4-functions-in-bash
slug: bash-basics-4-functions-in-bash
title: "Bash Basics #4: Functions in Bash"
excerpt: "Structure and modularize your Bash scripts with functions: declaration, parameter passing, return values (return vs. echo), local variable scopes, and function libraries."
date: "2024-10-25T09:00:00+01:00"
updated: "2024-11-10T09:00:00+01:00"
author:
  name: "Sebastian Palencsar"
  handle: "spalencsar"
category: "bash-basics"
tags: ["bash", "linux", "shell", "scripting", "functions", "modularization", "basics"]
reading_time: 20
toc: true
---

**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](/en/bash-basics/bash-basics-1-create-your-first-bash-shell-script){.badge-link-text}, how [variables and parameter expansions](/en/bash-basics/bash-basics-2-using-variables-in-bash){.badge-link-text} work, and how to control program flow through [control structures and loops](/en/bash-basics/bash-basics-3-control-structures-in-bash){.badge-link-text}.

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.

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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:

```bash
# 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..."
}
```

<blockquote class="infobox infobox--info">
💡 **Best practice:** Use the standard form `functionname() { ... }`. It is short, readable, 100% POSIX-compliant, and works seamlessly in Bash, Dash, and Zsh.
</blockquote>

### Calling a Function

Unlike programming languages such as Python, C, or PHP, functions in the shell are called **without parentheses**:

```bash
# 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.

```bash
#!/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.):

```bash
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!

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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

```bash
#!/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'!)
```

<blockquote class="infobox infobox--warn">
⚠️ **Safety rule:** Within functions, **declare every helper variable with `local` without exception**. This prevents unpredictable bugs in larger scripts.
</blockquote>

## 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)`.

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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):

```bash
# 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`:

```bash
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`):

```bash
# 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`):

```bash
#!/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!$)

```bash
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

<blockquote class="infobox infobox--practice">
❗ **Practice tasks for lesson #4:**

1. **Task 1 (Host Reachability Checker):**
   Write a function `check_host()` that accepts a hostname or IP as parameter `$1`, executes `ping -c 1 -W 2 "$1"`, and returns `return 0` on success or `return 1` if unreachable.
2. **Task 2 (Universal Calculator):**
   Create a function `calc()` that expects three arguments (`Number1`, `Operator`, `Number2`), performs the operation (`+`, `-`, `*`, `/`) with `case`, and returns the result via `echo`. Catch division by zero with an error message and `return 1`.
3. **Task 3 (String Trimmer):**
   Write a function `trim()` that removes leading and trailing whitespace from a passed string.
</blockquote>

### Sample Solution for Task 2:

```bash
#!/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](https://www.gnu.org/software/bash/manual/html_node/Shell-Functions.html){.badge-link-text} | Official GNU reference on function declaration and scopes |
| [Bash Basics #3: Control Structures](/en/bash-basics/bash-basics-3-control-structures-in-bash){.badge-link-text} | The previous part: Branching, tests, and loops |
| [Linux Command Line Processor Guide](/en/linux-beginners/command-line-processor-in-linux){.badge-link-text} | Fundamental knowledge on shells, I/O streams, and pipes |
| [chmod and File Permissions](/en/linux-beginners/basics-to-best-practices-all-about-chmod-in-linux){.badge-link-text} | 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.

<blockquote class="infobox infobox--info">
💡 **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.
</blockquote>

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](/en/bash-basics/bash-basics-input-and-output-in-bash){.badge-link-text}

👉 **Course overview:** [All lessons of the Bash basics course](/en/category/bash-basics){.badge-link-text}
