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.

ComponentRoleExample
forStarts the loop definition.for
Loop variableReceives the current item.file
inIntroduces the items to process.in
ListSupplies the values for the iterations.one two three
doBegins the loop commands.do
Loop commandsRuns once for each item.printf ...
doneCloses 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.

SourceExample formKey caution
Literal valuesred green blueWhitespace separates items.
Brace expansion{1..5}Best for simple sequences and alternatives.
Glob patterns*.logCheck what happens when nothing matches.
Variable expansion$serversUnquoted 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
  1. Bash assigns first to item.
  2. Bash runs the loop body and prints first.
  3. Bash assigns second to item and runs the body again.
  4. Bash assigns third to item and runs the body again.
  5. 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 printf before using rm, 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 printf while 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 typeBest used forControl mechanism
forKnown items, files matched by a glob, or generated values.One iteration per list item.
whileRepeating while input or a condition remains valid.Continues while the test succeeds.
untilRepeating 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 printf for predictable output and quote path expansions such as "$file".
  • Preview bulk operations and use -- where supported before running commands such as rm or mv.
  • Use while or until when repetition is controlled by a condition rather than a collection of items.