Modular Scripting: Functions and Variable Scope
·TechSoftware Development

Modular Scripting: Functions and Variable Scope

Don't repeat yourself. Master the art of modular Linux scripting. Learn to define re-usable Functions, understand the difference between Local and Global variables, and learn how to return values and status codes from your modules.

Functions and Scope: Building Reusable Automation

In the previous lessons, we've written scripts as "one long list" of code. This is fine for small tasks, but as your automation grows to 500 or 1,000 lines, it becomes a nightmare to maintain. Professional developers follow the DRY (Don't Repeat Yourself) principle.

Instead of writing the same "Log an Error" code 20 times, you define it once as a Function and call it whenever you need it.

In this lesson, we will learn how to turn your Bash scripts into professional, modular software.


1. Defining and Calling Functions

A function is a named block of code. In Bash, you define it using either the function keyword or just the name followed by parentheses.

# Style 1
greet_user() {
    echo "Hello, $1!"
}

# Style 2
function log_msg() {
    echo "[$(date +'%H:%M:%S')] $1"
}

# Calling the function (Just use its name)
log_msg "Server starting"
greet_user "Sudeep"

2. Returning Values

Unlike Python or Javascript, Bash functions don't "return" data (like a string). They can only return an Exit Status (0 to 255).

If you want to return actual text, you must use Command Substitution.

get_status() {
    # Return text via echo
    echo "UP"
}

# Capture the output
CURRENT=$(get_status)
echo "System is $CURRENT"

3. Variable Scope: Local vs. Global

By default, every variable in a Bash script is Global. This is dangerous! If you change a variable named count inside a function, it changes it for the entire script.

The Fix: local

To keep a variable trapped inside a function, you MUST use the local keyword.

NAME="Public"

change_name() {
    local NAME="Secret"
    echo "Inside: $NAME" # Output: Secret
}

change_name
echo "Outside: $NAME" # Output: Public (Safe!)

4. Practical: The "Ultimate Logger" Utility

Here is a common pattern used by DevOps engineers to standardize their logging across multiple scripts.

#!/bin/bash

# Define the utility
log() {
    local LEVEL="$1"
    local MSG="$2"
    local COLOR=""

    case "$LEVEL" in
        "INFO")  COLOR="\e[32m" ;; # Green
        "WARN")  COLOR="\e[33m" ;; # Yellow
        "ERROR") COLOR="\e[31m" ;; # Red
    esac

    echo -e "${COLOR}[$LEVEL]\e[0m $(date): $MSG"
}

# Use the utility
log "INFO" "Starting backup process..."
log "WARN" "Disk space is low (85%)."
log "ERROR" "Backup failed: Permission denied."

5. Script Libraries: The 'source' Command

If you have 10 different scripts that all need your log function, you don't copy-paste it. You save the functions in a file called utils.sh and "Import" it.

# In your main script
source ./utils.sh

# Now you can use functions from utils.sh
log "INFO" "Import successful."

6. Example: Function Integrity Tester (Python)

Bash functions can be tricky because they don't have "Type Checking." Here is a Python script that acts as a "Linter" to ensure your shell script functions have proper local scope.

import re

def audit_bash_script(file_path):
    """
    Checks for global variables inside functions (missing 'local' keyword).
    """
    if not os.path.exists(file_path):
        return
        
    with open(file_path, 'r') as f:
        lines = f.readlines()
        
    in_function = False
    for i, line in enumerate(lines):
        # Detect start of function
        if re.match(r"\w+\(\s*\)\s*\{", line):
            in_function = True
            continue
        
        # Detect end of function
        if in_function and line.strip() == "}":
            in_function = False
            
        # Detect assignments without 'local'
        if in_function:
            # Look for VAR=val without 'local ' in front
            match = re.search(r"^\s*([A-Za-z_]+)=", line)
            if match:
                var_name = match.group(1)
                print(f"[WARN] Line {i+1}: Global variable '{var_name}' defined inside function.")
                print(f"       Recommendation: Add 'local' keyword.")

if __name__ == "__main__":
    # Test on a dummy content
    with open("test_script.sh", "w") as f:
        f.write("my_func() {\n  COUNT=10\n  local NAME='bob'\n}\n")
    
    print("Auditing shell script scope...")
    audit_bash_script("test_script.sh")

7. Professional Tip: Use 'readonly'

If you have a function or variable that should never be changed (like the path to your log folder), define it as readonly.

readonly LOG_DIR="/var/log/myapp"

If any part of your script tries to change this later, Bash will throw an error and stop.


8. Summary

Functions turn "Spaghetti Code" into a "Toolkit."

  • func_name() { ... } is how you build a tool.
  • local is the shield that protects your global variables.
  • source is the bridge that allows sharing code across files.
  • Return status via return; return text via echo.
  • Keep your utility functions in a separate file for maximum re-use.

In the final lesson of this module, we will learn how to make our scripts indestructible using Error Handling and Debugging.

Quiz Questions

  1. What happens if you define a variable inside a function without the local keyword?
  2. How do you pass two separate arguments to a function?
  3. How can you include functions from an external file (lib.sh) into your current script?

Continue to Lesson 6: Error Handling and Debugging—Bulletproofing Your Scripts.

Subscribe to our newsletter

Get the latest posts delivered right to your inbox.

Subscribe on LinkedIn