Bash for Loops in Linux
Learn how Bash for loops process lists of values, use loop variables, handle filenames safely, and repeat commands in Linux shell scripts.
A for loop in Bash repeats a command block once for each item in a list. This makes it useful for tasks such as printing several values, creating directories, processing files, or running a command for multiple servers.
Bash is a common Linux command shell and scripting language. Before studying loops, you should be comfortable running commands, passing arguments, using basic variables, and quoting text.
Why loops are useful in shell scripts
A loop repeats a group of commands. Instead of writing the same command separately for every file or value, you write the command block once and let Bash repeat it.
There are several common ways to control repetition:
- A for loop processes each item in a known or generated collection.
- A while loop continues as long as a condition succeeds.
- An until loop continues until a condition succeeds.
This lesson focuses on for loops, which are generally the clearest choice when you have a set of items to process.
What a Bash for loop does
A Bash for loop processes a list, meaning a sequence of values. During each iteration, Bash assigns one list item to a loop variable and runs the loop body. The loop body runs once per resulting list item.
For example, this loop has five items and therefore performs five iterations:
for num in 2 12 17 104 10003; do
printf 'The number is %s.\n' "$num"
done
The variable num receives 2 during the first iteration, 12 during the second, and so on.
Basic Bash for loop syntax
The standard multiline form is:
for variable in list; do
commands
done
Remove the extra leading space before done in a real script:
for variable in list; do
commands
done
The keywords do and done delimit the loop body. Bash uses these keywords rather than braces.
| Component | Role | Example |
|---|---|---|
for | Starts the loop definition. | for |
| Loop variable | Receives the current item. | file |
in | Introduces the items to process. | in |
| List | Supplies the values for the iterations. | one two three |
do | Begins the loop commands. | do |
| Loop commands | Runs once for each item. | printf ... |
done | Closes the loop body. | done |
One-line form
A loop can also be written on one line. Use a semicolon before do and before done when commands continue on the same line:
for item in red green blue; do printf '%s\n' "$item"; done
In a multiline loop, the line break separates the header from do. In a one-line loop, the semicolon provides that separation.
Using the loop variable
You choose the loop variable name after for. Use a descriptive name such as file, server, directory, or number. Shell variables do not require a type declaration.
Reference the current value with parameter expansion, using $name or ${name}:
for project in alpha beta gamma; do
printf 'Project: %s\n' "$project"
done
The variable changes at the start of every iteration. Always expand the variable that you declared in the loop header. If you declare project but print $item, you may get an empty or unrelated value.
Sources of items for a for loop
Literal words and numbers
A simple list is written as whitespace-separated words or numbers. Each whitespace-separated item normally becomes a separate list element:
for color in red green blue; do
printf 'Color: %s\n' "$color"
done
The loop runs three times. This style is convenient when the values do not contain spaces.
A list stored in a variable
A variable can provide the list:
servers='dev test staging'
for server in $servers; do
printf 'Checking %s\n' "$server"
done
Here, unquoted $servers is split into three words. This is useful for a deliberately simple list, but it is not suitable when an item may contain spaces. The command inside the loop uses "$server" so the current value is passed as one argument.
Brace expansion
Brace expansion generates simple sequences or alternatives before the command runs:
for n in {1..5}; do
mkdir "project-$n"
done
This creates project-1 through project-5. Brace expansion can also generate text alternatives:
for environment in dev test prod; do
printf 'Environment: %s\n' "$environment"
done
Brace expansion is performed by Bash itself; it is not a command named brace.
Pathname expansion with globs
Pathname expansion, also called globbing, expands wildcard patterns such as *.txt into matching paths:
for file in *.txt; do
printf 'Would process: %s\n' "$file"
done
Each matching filename becomes an item. Quote the variable when using it as a path:
for file in *.log; do
gzip -- "$file"
done
The quotes preserve spaces and other special characters in the filename. The -- tells many commands that following arguments are paths rather than options.
Command substitution
Command substitution uses a command's output as input, commonly with $(command):
for server in $(printf '%s\n' dev test staging); do
printf 'Target: %s\n' "$server"
done
This is acceptable when the generated values are simple words. Be careful with filenames: command substitution removes trailing newlines, and the resulting text can be split at whitespace. A filename such as quarterly report.txt could become multiple loop items.
For robust filename processing, prefer pathname expansion for straightforward cases. For recursive or generated file lists, use tools that support null-delimited output and consume it with a null-delimited reader, rather than building a whitespace-separated list.
| Source | Example form | Key caution |
|---|---|---|
| Literal values | red green blue | Whitespace separates items. |
| Brace expansion | {1..5} | Best for simple sequences and alternatives. |
| Glob patterns | *.log | Check what happens when nothing matches. |
| Variable expansion | $servers | Unquoted expansion can split values. |
| Command substitution | $(command) | Whitespace and newlines can make filename handling unsafe. |
Execution flow and iteration count
For a loop such as:
for item in first second third; do
printf 'Current item: %s\n' "$item"
done
- Bash assigns
firsttoitem. - Bash runs the loop body and prints
first. - Bash assigns
secondtoitemand runs the body again. - Bash assigns
thirdtoitemand runs the body again. - After the final item, Bash continues after
done.
The iteration count equals the number of list items after expansions have taken place. An empty list produces no iterations.
There is one important globbing detail. If *.txt matches no files, Bash commonly leaves the literal pattern unchanged, so the loop may run once with *.txt as the value. You can inspect matches first, or enable nullglob when a script requires unmatched patterns to disappear:
shopt -s nullglob
files=( *.txt )
for file in "${files[@]}"; do
printf 'Matched: %s\n' "$file"
done
Displaying and using current values
echo is convenient for quick output, but printf gives more predictable formatting:
for number in 2 12 17; do
printf 'The number is %s.\n' "$number"
done
The current item can also be an argument to another command:
for name in alpha beta gamma; do
mkdir -- "$name"
done
Other common uses include cp -- "$file" /backup/, rm -- "$file", or invoking a custom script such as ./check-server "$server". Always understand what the command does before applying it to many items.
Quoting and filenames with spaces
Unquoted parameter expansion is subject to word splitting, which can divide expanded text at whitespace. It can also allow pathname expansion to act on the resulting text. Quote path variables in commands:
for file in *.txt; do
cp -- "$file" /tmp/text-backup/
done
Without the quotes, a file named meeting notes.txt may be interpreted as two arguments. Quoting does not repair a list that was already incorrectly created with whitespace splitting, so avoid command substitution and simple variable lists for filenames that may contain spaces.
Safe practices for bulk operations
- Preview matches with
printfbefore usingrm,mv, or another destructive command. - Inspect a broad glob before applying an operation to every match.
- Quote path expansions such as
"$file". - Use
--before path arguments when the command supports it. - Be cautious with patterns such as
*, because they may match more entries than intended. - Test the loop in a safe directory or replace the real command with
printfwhile developing it.
Previewing a rename
First print the proposed operation:
for file in *.txt; do
new_name="${file%.txt}.bak"
printf 'Would rename %s to %s\n' "$file" "$new_name"
done
After checking the output, a real rename can use mv -- "$file" "$new_name". The preview step helps catch an overly broad glob or an unexpected filename.
Practical examples
Print project names
projects='website api documentation'
for project in $projects; do
printf 'Building project: %s\n' "$project"
done
Process every text file
for file in *.txt; do
printf 'Would process: %s\n' "$file"
done
Run a command for each server
for server in dev.example test.example staging.example; do
printf 'Would connect to %s\n' "$server"
done
Replace the preview command only after verifying the server list and the command arguments.
Generate a numeric sequence with brace expansion
for n in {1..5}; do
printf 'Value: %s\n' "$n"
done
For a sequence generated by a command, seq is another option:
for n in $(seq 1 5); do
printf 'Value: %s\n' "$n"
done
Brace expansion is concise for simple fixed ranges. seq can be convenient when the limits are command arguments or when formatted numeric output is needed, but command substitution has the whitespace cautions described earlier.
Choosing between Bash loop types
| Loop type | Best used for | Control mechanism |
|---|---|---|
for | Known items, files matched by a glob, or generated values. | One iteration per list item. |
while | Repeating while input or a condition remains valid. | Continues while the test succeeds. |
until | Repeating until a condition becomes valid. | Continues until the test succeeds. |
A for loop answers, “What should I do for each item?” A while loop answers, “Should I continue while this test succeeds?” An until loop answers, “Should I continue until this test succeeds?”
Troubleshooting Bash for loops
Syntax error near do or done
Check for a missing semicolon in a one-line loop, a missing done, or an incorrectly written header. These are valid:
for item in one two; do
printf '%s\n' "$item"
done
for item in one two; do printf '%s\n' "$item"; done
A filename with spaces is processed as multiple values
Use quotes around variable expansions, such as "$file". Also avoid making filename lists from whitespace-separated command output. Prefer globbing or null-delimited tools for robust filename processing.
The literal *.txt is processed
No file may match the pattern. Check the directory contents and consider nullglob if the script should perform zero iterations when there are no matches.
A command affects too many files
The wildcard may be too broad, or the expanded list may not be what you expected. Print every match first, verify it, quote path variables, and use -- where supported before enabling the destructive command.
The same value appears on every line
Confirm that the loop body expands the variable declared after for. For example, a loop declared as for server in ... should use "$server", not a fixed string or a different variable name.
Key points
- A Bash for loop repeats a command block once per list item.
- The loop variable receives a new current value at the start of each iteration.
- Lists can come from literal values, variables, globs, brace expansion, or command substitution.
- Use
printffor predictable output and quote path expansions such as"$file". - Preview bulk operations and use
--where supported before running commands such asrmormv. - Use
whileoruntilwhen repetition is controlled by a condition rather than a collection of items.