Bash Loops, Arrays, and Functions Explained
Edited by Dev Kilaru, Senior Data Analyst. How we make our content
Introduction
Loops, arrays, and functions are how a short script grows into a tool. A loop repeats an action for every file, line, or item. An array stores a list of values without losing the spaces inside them. A function gives a name to a block of code so you can reuse it and keep long scripts readable. This lesson covers all three, including the details that trip people up, such as looping over files with spaces in their names and getting a value back from a function.
Prerequisites
- Variables, quoting, and if statements from the previous lessons
- Bash 4 or newer for associative arrays (macOS ships Bash 3.2, so install a newer one with Homebrew if needed)
Step-by-Step Guide
Step 1: Loop over lists and files with for
A for loop assigns each item in a list to a variable and runs the body once per item. The list can be words you type, a wildcard pattern that matches files, a brace range, or a counter in the C-style form. When a wildcard matches nothing, Bash passes the pattern itself as the item, so a guard that skips non-existent files keeps the loop from processing a file literally named star dot txt.
for color in red green blue; do
echo "Color: $color"
done
for file in *.txt; do
[[ -e $file ]] || continue # skip if nothing matched the pattern
echo "Processing $file"
done
for i in {1..5}; do echo "Step $i"; done
for ((i = 0; i < 3; i++)); do echo "i=$i"; doneStep 2: Repeat with while and until
A while loop repeats as long as its command succeeds, and an until loop repeats until its command succeeds. They suit situations where you do not know in advance how many times to loop, such as counting up to a limit or waiting for a service to come back. When you wait for something, include a sleep so the loop does not use all of the processor, and consider a maximum number of attempts so a script cannot hang forever.
count=1
while (( count <= 3 )); do
echo "Attempt $count"
count=$(( count + 1 ))
done
until ping -c 1 example.com > /dev/null 2>&1; do
echo "Waiting for network..."
sleep 5
doneStep 3: Read a file line by line
The reliable pattern to process a file is a while loop with read, fed by redirecting the file into the loop. Setting IFS to empty keeps leading and trailing spaces, and the -r option keeps backslashes. When each line has several fields, set IFS to the separator on the read command to split the line into variables. Avoid piping a command into a while loop when you need to keep variables afterwards, because the loop then runs in a subshell and any changes to variables disappear when it ends. Redirecting a file or using process substitution avoids that.
while IFS= read -r line; do
echo "Line: $line"
done < servers.txt
# Split each line on commas:
while IFS=, read -r name email; do
echo "Sending to $name at $email"
done < users.csv
# Keep variables set inside the loop by reading from process substitution:
total=0
while read -r size; do
total=$(( total + size ))
done < <(awk '{ print $1 }' sizes.txt)
echo "Total: $total"Step 4: Control a loop with break and continue
The continue command skips the rest of the current pass and moves to the next item, while break leaves the loop completely. They are useful for skipping items you do not want to process and for stopping once you have found what you were looking for. Both accept a number to affect an outer loop when loops are nested, though needing that is often a sign the code should be split into a function.
for n in {1..10}; do
(( n % 2 == 0 )) && continue # skip even numbers
(( n > 7 )) && break # stop after 7
echo "$n"
done
# prints 1 3 5 7Step 5: Store lists in arrays
An indexed array holds several values and keeps each one intact even when it contains spaces. Create one with parentheses, add elements with the plus-equals operator, read one with its index starting at zero, and expand all of them with the at-sign form inside double quotes. The hash form gives the number of elements. Using arrays instead of space-separated strings is the right way to build lists of file names or command options in Bash.
fruits=("apple" "banana" "cherry")
fruits+=("date")
echo "${fruits[0]}" # apple
echo "${#fruits[@]}" # 4
for f in "${fruits[@]}"; do echo "- $f"; done
echo "${fruits[@]:1:2}" # banana cherry (two items starting at index 1)
files=("my report.txt" "notes.txt")
ls -l "${files[@]}" # each name stays one argumentStep 6: Use associative arrays for key and value pairs
An associative array maps text keys to values, like a dictionary in other languages. You must declare it with declare -A first, and it needs Bash 4 or newer. Expanding the keys with the exclamation mark form lets you loop over them. The order of keys is not guaranteed, so sort the keys if you need a stable output order.
declare -A ports=([http]=80 [https]=443 [ssh]=22)
ports[smtp]=25
echo "${ports[https]}" # 443
for service in "${!ports[@]}"; do
echo "$service uses port ${ports[$service]}"
doneStep 7: Define and call functions
A function is defined with a name followed by parentheses and a block in braces. It receives its own arguments as $1, $2, and so on, separate from the script's arguments. Declare variables inside a function with local so they do not leak into the rest of the script and overwrite something with the same name. Define a function before the line that calls it, because Bash reads a script from top to bottom.
log() {
local level=$1
shift
echo "$(date +%T) [$level] $*"
}
log INFO "Starting backup"
log WARN "Disk is 85% full"Step 8: Return results from functions
The return command only sets an exit status between 0 and 255, which is ideal for functions that answer yes or no and can be used directly in an if statement. To pass back text or a number, have the function print it and capture the output with command substitution. Anything else the function prints to standard output is captured too, so send log messages to standard error when the function also returns data.
is_even() {
(( $1 % 2 == 0 )) # the exit status of this test is the return value
}
if is_even 10; then echo "10 is even"; fi
square() {
echo $(( $1 * $1 )) # "return" a value by printing it
}
result=$(square 7)
echo "$result" # 49FAQs
- How do I loop over files whose names contain spaces?
- Use a wildcard in the for loop and quote the variable inside the body, as in for f in *.txt followed by "$f". Do not loop over the output of the ls command, because it splits names on spaces. For recursive searches, use find with -print0 and read with the -d empty option, which you will see in a later lesson.
- Why is my variable empty after a while loop?
- When a loop is at the end of a pipe, Bash runs it in a subshell, and changes to variables inside it are lost when it finishes. Feed the loop with a redirect from a file or with process substitution, as in the third step, so it runs in the current shell.
- What is the difference between return and exit?
- The return command ends a function and gives back a status to the caller, and the script keeps running. The exit command ends the whole script. Use return inside functions unless you intentionally want the entire script to stop.
Summary
You can now repeat work with for, while, and until loops, read files line by line, store lists in indexed and associative arrays, and organize code into functions that return status codes and values. Together with variables and conditionals, these are the core building blocks of Bash. Next you will connect commands with pipes and redirection and learn the text tools that make shell scripting powerful.
Related tutorials
- Schedule and Debug Shell Scripts: cron, bash -x, and Portability
- Shell Scripting Project: A Backup Script with Rotation and a Log Report
- Writing Safe Bash Scripts: set -euo pipefail, getopts, and trap
- Shell Pipes, Redirection, grep, sed, and awk for Text Processing
- Bash Conditionals: if Statements, Test Operators, and case
Comments
Log in or create a free account to join the discussion.