Skip to content

Bash For Loop Examples

bashloopsforscriptingbeginner
3 min read

Quick Answer

A bash for loop repeats a block of code once per item in a list. The safe forms are: loop a glob directly (for f in *.log) to iterate files, which handles spaces and special characters correctly; loop a brace range (for i in {1..10}) or a C-style counter (for ((i=0; i<10; i++))) for numbers; and loop an array with the quoted expansion (for x in "${items[@]}") so each element stays intact. The form to avoid is for f in $(ls) — command substitution word-splits on whitespace, so a file named report final.txt becomes two loop iterations, report and final.txt, and the real file is never touched. Always quote the loop variable when you use it ("$f"), because the split happens at use, not only at the loop header. Glob over parse, quote on use, and the loop stops surprising you.

Looping over files is a find problem

The text-processing guide covers the case this snippet warns about: when a glob is not enough, find -print0 | while read -d "" and find -exec are the two loops that survive spaces, newlines and 40,000 files. Full guide: Bash Text Processing: find, grep, sed, and awk.

The loop that backed up nothing for three weeks

A nightly job looped for f in $(ls /data/exports) and copied each file to a backup volume. It ran green every night. Three weeks later we needed the Q3 final.xlsx export and it was not in the backup — not the current one, not any of the twenty-one before it. The loop had been splitting Q3 final.xlsx into Q3 and final.xlsx, trying to copy two paths that did not exist, logging two harmless "no such file" lines, and moving on. The files without spaces backed up fine, which is exactly why nobody caught it.

The bug was not cp. It was looping over parsed command output instead of a glob.

Loop a glob — the form for files

bash
#!/bin/bash # Script: backup-exports.sh # Purpose: copy export files to a backup dir without losing ones with spaces # Usage: ./backup-exports.sh set -euo pipefail CHECK="✓" CROSS="✗" SRC_DIR="/data/exports" DEST_DIR="/backup/exports" shopt -s nullglob # an empty match expands to nothing, not the literal pattern for f in "$SRC_DIR"/*.xlsx; do if cp -- "$f" "$DEST_DIR/"; then echo "$CHECK backed up: $(basename "$f")" else echo "$CROSS failed: $(basename "$f")" fi done

for f in "$SRC_DIR"/*.xlsx lets bash expand the glob itself, which produces a correctly-separated list no matter what is in the names. Quoting "$f" on use is the other half — the split that bit us happens when the variable is used, not only in the loop header. The -- tells cp to stop reading options, so a file named -rf cannot turn into a flag.

Loop a range or a counter — the form for numbers

bash
# Fixed count, known at parse time for i in {1..5}; do echo "$CHECK attempt $i" done # Variable bound — brace ranges DON'T expand variables, so use C-style attempts=5 for ((i = 1; i <= attempts; i++)); do echo "$CHECK attempt $i of $attempts" done

{1..5} is fixed before variables expand, so {1..$attempts} produces the literal string {1..5}-style garbage, not a range. The moment your bound is a variable, the C-style for (( ... )) is the correct tool.

Loop an array — quote the expansion

bash
servers=("web-01" "db primary" "cache-02") for s in "${servers[@]}"; do echo "$CHECK checking: $s" done

"${servers[@]}" with quotes and [@] keeps db primary as one element. Drop the quotes and you are back to word-splitting — the same bug, new disguise.

The rule across all of these: glob over parse, quote on use. For reading a file's lines one at a time, a for loop is the wrong tool — see read a file line by line. And wrap any loop that touches real files in set -euo pipefail so a failure stops the script instead of looping past it.

Quoting correctly stops the loop doing the wrong thing quietly. Making the whole script survive unattended is a separate layer — strict mode, an ERR trap that names the failing line, cleanup on every exit, and a log worth reading afterwards. That layer is collected as bashlib.sh and a ready-made template.sh in The Production Bash Toolkit.

Raw script, MIT licensed: scripts/bash-for-loop-examples.sh on GitHub

PAID RESOURCE — $9

The Production Bash Toolkit

An operational script system + a 31-function shared library + a 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →
curl -O bashlib-starter.sh

Get the bashlib starter

Ten functions I source into every script on my own boxes — strict-mode setup, an ERR trap that names the failing line, lock and timeout wrappers, and cleanup that runs on every exit path. One email, no sequence.

BashSnippets logo

Written by Travis

Creator of BashSnippets.xyz

bashsnippets.xyz/about

Related Snippets

Frequently Asked Questions

faq — snippet

Why does my bash for loop break on filenames with spaces?

Because you are looping over the output of a command like ls or find without -print0, and bash word-splits that output on spaces. A file named 'project notes.txt' becomes two iterations. Loop a glob directly instead — for f in *.txt — and quote the variable on use: "$f".

faq — snippet

How do I loop a fixed number of times in bash?

Use a brace range for a simple count, for i in {1..10}, or a C-style loop when you need arithmetic, for ((i=0; i<10; i++)). The brace range is fixed at parse time, so for i in {1..$n} does NOT work — use the C-style form when the bound is a variable.

faq — snippet

What is the difference between a for loop and a while loop in bash?

A for loop iterates a known list (files, a range, an array). A while loop runs until a condition changes, which is the right tool for reading a file line by line or polling until something is ready. Reading lines with a for loop word-splits the content; use while IFS= read -r line for that.

faq — snippet

How do I loop over an array in bash?

Use the quoted full-array expansion: for x in "${arr[@]}". The quotes and [@] keep each element as one item even if it contains spaces. Without quotes, "${arr[@]}" word-splits again and you lose the element boundaries.