Home DevOps Bash Scripting: Variables, Loops, and Functions — Write Scripts That Survive Production
Intermediate 3 min · July 18, 2026

Bash Scripting: Variables, Loops, and Functions — Write Scripts That Survive Production

Bash scripting variables, loops, and functions explained with production patterns.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Basic terminal familiarity
  • Ability to run a .sh file
  • Understanding of exit codes (0 vs non-zero)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use local in functions to avoid variable leaks. Quote all variable expansions ("$var") to prevent word splitting and globbing. Prefer for loops over while read when iterating command output. Always check the exit code of commands inside loops.

✦ Definition~90s read
What is Bash Scripting?

Bash scripting is the glue of Linux DevOps — automating tasks, orchestrating deployments, and parsing logs. Variables store data, loops repeat actions, and functions encapsulate logic. This article covers the production-grade patterns, not the hello-world versions.

Think of a bash script as a recipe.
Plain-English First

Think of a bash script as a recipe. Variables are the ingredient amounts — you write them down once and reuse. Loops are like repeating a step for each potato you peel. Functions are sub-recipes you can call from the main recipe. The trick is not to spill ingredients (unquoted variables) or burn the kitchen (infinite loops).

⚙ Browser compatibility
Latest versions — ✓ supported
bash 4.xbash 5.xzshdash

Every DevOps engineer has that one script. The one that runs fine in testing but deletes half the production logs at 2am because a variable was empty. Bash is unforgiving — a missing quote, an unset variable, a loop that silently skips errors. This isn't about syntax. It's about writing scripts that don't bite you. You're about to learn the patterns that separate a cron job that runs for years from the one that wakes you up at 3am. By the end, you'll write variables that don't leak, loops that fail fast, and functions that are actually reusable.

Variables: The Silent Killers

Variables seem harmless. Until an unquoted variable with a space in its value splits into two arguments, or an empty variable deletes the wrong directory. The problem is that bash, by default, treats missing variables as empty strings — not errors. That's why rm -rf /$important_dir becomes rm -rf / when $important_dir is unset. The fix: always quote expansions and use set -u to make unset variables fatal. Also, use ${var:-default} for safe defaults. In production, every variable expansion is a potential landmine.

safe_variables.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# io.thecodeforge — DevOps tutorial

set -euo pipefail  # Exit on error, unset variable, pipe failure

log_dir="/var/log/myapp"
# Use default if variable is unset
archive_dir="${ARCHIVE_DIR:-/tmp/archive}"

# Always quote to preserve spaces and special chars
rm -rf "${log_dir:?}"  # :? fails if variable is empty or unset

# Array example: safe iteration
files=("file one.txt" "file two.txt")
for f in "${files[@]}"; do
    echo "Processing: $f"
done
Output
Processing: file one.txt
Processing: file two.txt
⚠ Production Trap: Unquoted Variables
If a filename contains a space, rm $file deletes two files. Always use "$var". The one exception: when you explicitly want word splitting, like for word in $sentence.

Loops: Fail Fast or Fail Forever

Loops are where scripts go to die silently. A for loop over a glob that matches nothing runs once with the literal glob string. A while read loop that hits a permission error continues as if nothing happened. The fix: check that globs match before looping, and always check exit codes inside loops. Use shopt -s nullglob to make empty globs expand to nothing. For while read, add || break to stop on errors. And never pipe into while read — use process substitution to avoid subshell variable loss.

safe_loops.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# io.thecodeforge — DevOps tutorial

set -euo pipefail

# Safe glob loop: nullglob prevents literal '*' when no files match
shopt -s nullglob
for config in /etc/myapp/*.conf; do
    echo "Processing $config"
    # Simulate processing; if it fails, break the loop
    process_file "$config" || { echo "Failed on $config"; break; }
done
shopt -u nullglob

# Safe while read with process substitution (no subshell)
count=0
while IFS= read -r line; do
    ((count++))
    echo "Line $count: $line"
done < <(grep 'ERROR' /var/log/app.log)
echo "Total errors: $count"
Output
Processing /etc/myapp/app.conf
Processing /etc/myapp/db.conf
Line 1: ERROR: connection timeout
Line 2: ERROR: disk full
Total errors: 2
⚠ The Classic Bug: Piped while read
grep pattern file | while read line; do ... done — the count variable inside the loop is lost after the pipe. Use while ... done < <(grep pattern file) instead.

Functions: Encapsulate or Contaminate

Functions in bash are just commands — they run in the same shell unless you explicitly spawn a subshell. That means any variable you set inside a function leaks to the global scope unless you declare it local. The rule: every variable inside a function must be local. Also, functions should return meaningful exit codes (0 for success, non-zero for failure). Use return not exitexit kills the entire script. And never use function keyword; it's non-portable. Just use name() { ... }.

safe_functions.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# io.thecodeforge — DevOps tutorial

set -euo pipefail

# Good: local variables, return codes
process_log() {
    local logfile="$1"
    local errors
    errors=$(grep -c 'ERROR' "$logfile") || true  # grep returns 1 if no match
    if [[ "$errors" -gt 100 ]]; then
        echo "Too many errors: $errors"
        return 1
    fi
    echo "Errors: $errors"
    return 0
}

# Call function and check return
if process_log "/var/log/app.log"; then
    echo "Log OK"
else
    echo "Log check failed"
fi
Output
Errors: 42
Log OK
💡Senior Shortcut: Local All The Things
Make it a habit: the first line of every function should be local var1 var2 var3. This prevents accidental global variable pollution that can cause heisenbugs.

When Not to Use Bash Functions

Bash functions are great for grouping commands, but they're terrible for complex logic. If your function has more than 20 lines, or uses arrays with complex manipulation, or needs to return structured data (like a list), switch to Python or a compiled language. Bash functions cannot return arrays — you have to echo and parse, which is fragile. Also, functions are slow in tight loops because each call forks a subshell (unless you use source). For performance-critical loops, inline the logic or use eval carefully.

when_not_to_use.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# io.thecodeforge — DevOps tutorial

# Bad: function called in tight loop (slow)
slow_function() {
    echo "$(( $1 * 2 ))"
}
for i in {1..1000}; do
    result=$(slow_function $i)  # subshell each iteration
done

# Good: inline or use eval (if safe)
for i in {1..1000}; do
    result=$(( i * 2 ))
done
🔥Performance Trap: Subshell in Loop
Calling a function with $() in a loop creates a subshell every iteration. For 1000 iterations, that's 1000 forks. Use eval or inline the logic for speed.

Debugging Bash Scripts in Production

When a script fails at 3am, you need fast diagnostics. First, enable set -x to trace every command. But be careful — it can leak secrets in logs. Use PS4='+ $BASH_SOURCE:$LINENO ' to get file and line numbers. Second, use trap to catch errors and print a stack trace. Third, log to syslog with logger instead of echo — it's timestamped and searchable. Never use echo for debugging in production; it mixes with normal output.

debug_script.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# io.thecodeforge — DevOps tutorial

set -euo pipefail

# Debug mode: trace with file and line
PS4='+ $BASH_SOURCE:$LINENO '
if [[ "${DEBUG:-}" == "true" ]]; then
    set -x
fi

# Error trap with stack trace
error_handler() {
    local line=$1
    local command=$2
    logger -p user.err "Error at line $line: $command"
    exit 1
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR

# Simulate a command that might fail
cp /nonexistent/file /tmp/ || true  # trap won't fire because of || true

# Real failure
cp /nonexistent/file /tmp/  # this triggers the trap
Output
Error at line 18: cp /nonexistent/file /tmp/
💡Interview Gold: Debugging Without set -x
In production, set -x can flood logs with sensitive data. Use trap DEBUG to log selectively, or redirect debug output to a separate file with exec 5> debug.log; BASH_XTRACEFD=5.
● Production incidentPOST-MORTEMseverity: high

The 4GB Log That Kept Growing

Symptom
A log rotation script ran every hour but never rotated. The log file grew to 4GB and filled the disk, crashing the application.
Assumption
The team assumed the log rotation config was wrong or cron wasn't running.
Root cause
The bash script used a while read loop with a pipe, which created a subshell. The variable rotated was set inside the loop but never visible outside — so the script always thought rotation wasn't needed.
Fix
Replace command | while read line; do ... done with a process substitution: while read line; do ... done < <(command). This keeps the loop in the current shell, preserving variable changes.
Key lesson
  • Pipes create subshells.
  • Any variable set inside a piped while read is lost.
  • Use process substitution or a here-string to keep variables in scope.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Script fails with 'unbound variable' after adding set -u
Fix
1. Identify the unset variable from the error message. 2. Check if it's optional — use ${var:-default}. 3. If it's required, ensure it's set before use.
Symptom · 02
Loop runs once with literal glob string (e.g., '*.txt')
Fix
1. Check if nullglob is set: shopt -s nullglob. 2. Verify files exist. 3. Use compgen -G '*.txt' to test glob expansion.
Symptom · 03
Function modifies global variable unexpectedly
Fix
1. Check function body for missing local declarations. 2. Add local to all variables. 3. Use declare -p var to inspect variable attributes.
★ Bash Scripting: Variables, Loops, and Functions Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Variable expansion gives wrong result or error `unbound variable`
Immediate action
Check if variable is set and quoted
Commands
echo "${var:?}"
set -u; echo "$var"
Fix now
Use ${var:-default} or ensure variable is set before use.
Loop processes no files or wrong files+
Immediate action
Test glob expansion
Commands
ls -la /path/*.conf
shopt -s nullglob; for f in /path/*.conf; do echo "$f"; done
Fix now
Enable nullglob or check file existence.
Function variable leaks to global scope+
Immediate action
Check for `local` keyword
Commands
type function_name
declare -f function_name | grep -E '^[[:space:]]+[a-zA-Z_]+='
Fix now
Add local before each variable assignment in the function.
Script exits silently without error message+
Immediate action
Enable error tracing
Commands
bash -x script.sh 2>&1 | head -50
set -e; trap 'echo "Error at line $LINENO"' ERR
Fix now
Add set -euo pipefail and error trap.
Featurefor loop over globwhile read loop
Handles spaces in filenamesYes, with quoted array expansionYes, with IFS= read -r
Subshell riskNoneYes if piped; use process substitution
Empty input behaviorLiteral glob if nullglob offNo iterations (empty input)
PerformanceFast (built-in)Slower (external command per line)
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
safe_variables.shset -euo pipefail # Exit on error, unset variable, pipe failureVariables
safe_loops.shset -euo pipefailLoops
safe_functions.shset -euo pipefailFunctions
when_not_to_use.shslow_function() {When Not to Use Bash Functions
debug_script.shset -euo pipefailDebugging Bash Scripts in Production

Key takeaways

1
Quote all variable expansions
"$var" — unless you explicitly want word splitting.
2
Use local in every function to prevent variable leaks.
3
Never pipe into while read
use process substitution to keep variables in scope.
4
Enable set -euo pipefail in every script to catch errors early.
5
Functions cannot return arrays; use nameref or global variables with care.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What happens when you set a variable inside a `while read` loop that is ...
Q02SENIOR
When would you choose a `for` loop over a `while read` loop in a bash sc...
Q03SENIOR
What is the difference between `export`, `local`, and `declare -g` in ba...
Q04JUNIOR
How do you return an array from a bash function?
Q05SENIOR
A cron job runs a bash script that processes logs. One day it stops work...
Q06SENIOR
How would you design a bash script that must process 10,000 files withou...
Q01 of 06SENIOR

What happens when you set a variable inside a `while read` loop that is part of a pipeline, and why?

ANSWER
The variable is set in a subshell and lost after the pipeline completes. Pipelines create subshells for each command. Use process substitution while ... done < <(cmd) to keep the loop in the current shell.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why should I quote variables in bash?
02
What's the difference between `for` and `while` loops in bash?
03
How do I return a value from a bash function?
04
What does `set -euo pipefail` do and why should I use it?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Linux. Mark it forged?

3 min read · try the examples if you haven't

Previous
Linux Disk and Storage Management
13 / 16 · Linux
Next
Grep, Awk, and Sed: Text Processing Power Tools