Bash scripting crash course: loops, ifs and functions in one post
Tags: Bash, Linux, automation
Introduction – what, why, and who this is for
Bash (the Bourne‑Again SHell) is the default command interpreter on most Linux distributions and macOS. Writing a short Bash script lets you chain commands, make decisions, repeat work, and package logic that would otherwise require manual typing.
- What you’ll get: a single, fully‑runnable script that grows step‑by‑step, demonstrating loops, conditional statements, and functions.
- Why it matters: automation saves time, reduces human error, and is the first step toward DevOps‑style workflows.
- Who should read this: beginners who know how to run commands in a terminal and want to start automating, and intermediate users who need a quick refresher on Bash control structures.
Part 1 – Loops: repeat work without re‑typing
1.1 for loop over a list
#!/usr/bin/env bash
# Simple for‑loop: print each word in a list
for word in apple banana cherry; do
echo "Fruit: $word"
done
for word in …; do … done iterates over each element separated by spaces.
1.2 for loop over files
| Pattern | Meaning |
|---|---|
* |
every file in the current directory |
*.txt |
every file ending with .txt |
?.sh |
any one‑character name ending with .sh |
# Count lines in every .sh file
for file in *.sh; do
lines=$(wc -l < "$file")
echo "$file has $lines lines"
done
1.3 while loop with a condition
# Count down from 5
counter=5
while (( counter > 0 )); do
echo "Countdown: $counter"
((counter--))
done
echo "Liftoff!"
The double‑parentheses (( … )) let Bash evaluate arithmetic expressions directly.
Part 2 – Conditional statements (if, elif, else)
2.1 Basic file‑existence test
if [[ -f "$1" ]]; then
echo "File '$1' exists."
else
echo "File '$1' does NOT exist."
fi
| Test | Description |
|---|---|
-f |
true if path exists and is a regular file |
-d |
true if path exists and is a directory |
-z |
true if string is empty |
-n |
true if string is non‑empty |
== |
string equality (inside [[ … ]]) |
>, < |
numeric comparison (inside (( … ))) |
2.2 Combining tests with && and ||
if [[ -d "$HOME" && -w "$HOME" ]]; then
echo "Your home directory is writable."
fi
2.3 case for multiple branches (alternative to many elifs)
read -p "Enter a day (Mon‑Sun): " day
case "$day" in
Mon|Tue|Wed|Thu|Fri) echo "Weekday";;
Sat|Sun) echo "Weekend";;
*) echo "Unknown day";;
esac
Part 3 – Functions: reusable blocks of logic
3.1 Defining and calling a function
greet() {
local name=$1
echo "Hello, $name!"
}
greet "Bash learner"
local limits the variable’s scope to the function, preventing accidental overwrites.
3.2 Returning a status code
is_even() {
(( $1 % 2 )) && return 1 # odd → non‑zero exit status
return 0 # even → zero exit status
}
if is_even 42; then
echo "42 is even"
else
echo "42 is odd"
fi
3.3 Function with output capture
current_time() {
date +"%Y-%m-%d %H:%M:%S"
}
timestamp=$(current_time)
echo "Script started at $timestamp"
Hands‑on example – a single script that uses all three concepts
Below is the final script you can copy to backup.sh, make executable (chmod +x backup.sh), and run. It backs up all .txt files from a source directory into a timestamped archive, but only if the source exists and contains files.
#!/usr/bin/env bash
# backup.sh – demo script combining loops, conditionals, and functions
# Usage: ./backup.sh /path/to/source /path/to/destination
set -euo pipefail # strict mode: exit on error, undefined vars, and pipe failures
# ---------- Functions ----------
timestamp() {
date +"%Y%m%d_%H%M%S"
}
create_archive() {
local src=$1 dest=$2 ts=$3
local archive="${dest}/backup_${ts}.tar.gz"
tar -czf "$archive" -C "$src" . # compress everything in src
echo "$archive"
}
# ---------- Argument validation ----------
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <source_dir> <dest_dir>"
exit 1
fi
src_dir=$1
dest_dir=$2
# ---------- Conditional checks ----------
if [[ ! -d "$src_dir" ]]; then
echo "Error: source directory '$src_dir' does not exist."
exit 2
fi
if [[ ! -d "$dest_dir" ]]; then
echo "Destination directory '$dest_dir' does not exist – creating it."
mkdir -p "$dest_dir"
fi
# ---------- Loop: process each .txt file ----------
txt_count=0
for file in "$src_dir"/*.txt; do
[[ -e "$file" ]] || continue # skip if no .txt files match
((txt_count++))
echo "Will include: $(basename "$file")"
done
if (( txt_count == 0 )); then
echo "No .txt files found in '$src_dir'. Nothing to back up."
exit 0
fi
# ---------- Build archive ----------
ts=$(timestamp)
archive_path=$(create_archive "$src_dir" "$dest_dir" "$ts")
echo "Backup created: $archive_path"
How it works, line by line
| Section | What happens |
|---|---|
set -euo pipefail |
Makes the script stop on the first error, undefined variable, or failed pipe – a safety net for beginners. |
Functions (timestamp, create_archive) |
Encapsulate reusable logic. timestamp returns a formatted date; create_archive builds a .tar.gz. |
Argument validation (if [[ $# -ne 2 ]]) |
Guarantees the script receives exactly two paths. |
Directory checks (-d) |
Ensures source exists; creates destination if missing. |
for file in "$src_dir"/*.txt |
Loops over every .txt file; [[ -e "$file" ]] || continue skips the loop when the glob expands to itself (no matches). |
Counter txt_count |
Demonstrates arithmetic inside (( … )). |
| Final archive creation | Calls the functions, prints the resulting archive path. |
Run it like this (replace the paths with ones on your machine):
./backup.sh "$HOME/documents" "$HOME/backups"
You’ll see a list of the .txt files that will be archived, followed by the full path to the generated backup_YYYYMMDD_HHMMSS.tar.gz file.
Common mistakes & troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Script exits with “unbound variable” | set -u is on and a variable was never assigned. |
Initialise variables (var=${var:-default}) or remove set -u while you’re learning. |
Loop processes the literal string *.txt |
No .txt files exist, and the glob didn’t expand. |
Add [[ -e "$file" ]] || continue inside the loop (as shown) or test with shopt -s nullglob. |
tar: cannot open: No such file or directory |
Destination directory missing and mkdir -p failed. |
Verify you have write permission, or run script with sudo if needed. |
if [[ -f "$1" ]]; then always “does NOT exist” |
$1 is empty because you forgot to pass an argument. |
Check the usage line, or add a guard [[ -z "${1:-}" ]] && echo "Missing arg" && exit 1. |
| Function returns wrong status | Using return with a string instead of a numeric code. |
return 0 for success, return 1 for failure; use echo for textual output. |
Try it yourself – short exercise
Modify the script so that, in addition to .txt files, it also backs up any .md (Markdown) files found in the source directory. Hint: change the glob pattern and adjust the “no files found” test accordingly.
What’s next
- Parameter parsing with
getopts– give your script named flags like-vfor verbose. - Parallel execution – use
&andwaitto compress multiple directories at once. - Logging to a file – redirect
echooutput withexec > >(tee -a logfile) 2>&1. - Scheduling with
cron– turn your script into a daily backup job.
Happy scripting!
Bookmark this page for a quick reference.
Diagrams

Flowchart showing script execution: start → argument check → directory validation → file loop → archive creation → end

Timeline diagram of timestamp function output format (YYYYMMDD_HHMMSS) with arrows pointing to archive filename.
This tutorial was drafted with AI assistance and verified with runnable examples. Found a mistake? Email the author — corrections are welcome.