For loops allow us to repeat tasks and workflow in bash scripts by iteratively processing items in a collection. Whether iterating over a list of files, array of strings, or sequence of numbers – for loops are key to automating repetitive tasks.

In this comprehensive guide, we will explore the intricacies of for loops in bash with insightful real-world examples.

For Loop Syntax

The syntax of a for loop in bash is:

for item in LIST; do
   COMMANDS
   DONE

Where:

  • item: Holds the current item from the list on each iteration
  • LIST: A list of items to iterate over. This could be a literal list (array), a file list output using wildcards (*.txt), an integer range, the result of another command, etc.
  • COMMANDS: The commands to execute on each iteration. This block can contain multiple lines of bash commands.
  • DONE: Signals the end of the for loop body.

On every iteration, the item takes on the next value from LIST and the COMMANDS are executed. This continues until all items in LIST have been consumed.

So in a nutshell, the flow is:

  1. Set item to the first value in LIST
  2. Execute COMMANDS
  3. Set item to next value in LIST
  4. Execute COMMANDS
  5. Repeat steps 3-4 until LIST is finished.

Under the hood, for loops work by splitting the LIST into individual words, then iteratively taking the first word and executing COMMANDS repeatedly until no more words are left.

With this basic understanding of how for loops are structured, let‘s now look at some common examples.

Iterating Over a Literal List

One simple use case is iterating over a pre-defined list hardcoded into the script:

#!/bin/bash

fruits=("Apple" "Banana" "Orange")

for fruit in ${fruits[@]}; do
  echo $fruit
done

Output:

Apple
Banana
Orange

Here ${fruits[@]} expands the fruits array into the full list of items. On each iteration, $fruit takes on the next value, allowing us to process the items individually.

Some key points around iterating lists:

  • Use ${array[@]} syntax to fully expand arrays
  • The list can also be a comma or space separated literal
  • Indentation and spacing does not matter in bash
  • Remember to use done to signify loop end

This method of hardcoding lists works for simple cases, but often we need to construct lists programmatically.

Constructing Lists Programmatically

Instead of manually specifying lists, we can construct them dynamically using command substitutions and wildcards:

List Files in a Directory

for file in *.txt; do
  echo $file
done

This will print all files ending in .txt in the current working directory. The *.txt wildcard construct the list of matching files on each iteration.

List Running Processes

for process in $(ps aux); do
  echo $process
done

Here we use ps aux and command substitution to generate the list of running processes fed into the for loop.

List Files Modified in Last Day

for file in $(find . -mtime -1); do
  echo $file 
done

By nesting the find command we construct a list of recently modified files.

As you can see, commands like ls, ps, find and others can output lists of items that we can feed into our for loops.

Iterating Over Integer Ranges

Another common technique is iterating over an integer sequence using brace expansion:

for i in {1..10}; do
   echo $i
done

Outputs numbers 1 through 10.

We can also increment by 2, count backwards, set start/stop values:

for i in {10..1}; do
  echo $i 
done

for i in {1..10..2}; do
   echo $i 
done

This provides a simple method for repeating numerical operations.

Reading Files Line by Line

A common scripting task is processing a file line-by-line:

while read line; do
  echo $line
done < file.txt
  • The read builtin reads one line at a time from standard input
  • We pipe the file into the while loop using < file.txt to provide lines to read
  • This prints each line of the file

For larger files, while read is preferred over loading the entire contents at once.

Iterating Arrays

Arrays store multiple elements but need to be expanded into a list for iterating:

fruits=("Apple" "Banana" "Orange") 

for fruit in ${fruits[@]}; do
  echo $fruit
done

Observe the ${array[@]} expansion syntax. This fully expands the array into a list for the for loop.

Changing Behavior Mid-Iteration

We can dynamically alter loop behavior using conditionals like if/else:

for fruit in ${fruits[@]}; do
  if [ "$fruit" = "Banana"]; then
    continue 
  fi

  echo "Want: $fruit" 
done

Here we skip Banana iterations. Or only process files with .txt extensions:

for file in *; do
  if [[ "$file" != *.txt ]]; then
    continue
  fi 

  echo "Text file: $file"
done 

The continue statement avoids processing the current item and jumps to the next. This alters loop execution flow dynamically.

We can also terminate all iterations early using break:

for process in $(ps aux); do
   if [ "$process" = "bash loop.sh" ]; then
      echo "Found script process - stopping" 
      break
   fi

   echo "Evaluating: $process"
done

Once our script finds its own process running, we break entirely out of evaluating additional processes.

So break and continue introduce logic for fine tuning loops and avoiding unnecessary work.

Infinite Loops

We can intentionally create endless loops using:

while :
do
   COMMAND
   # Use sleep to prevent overwhelming CPU
   sleep 1
done

This infinite while loop executes COMMAND repeatedly until forced to stop by sending SIGINT with Ctrl+C.

Infinite loops are useful for long-running monitoring scripts.

For example, continuously check disk space every 60 seconds:

while : 
do
   echo "Disk usage:"
   df -h
   sleep 60
done

We need external intervention to stop this type of endless loop.

Nesting Loops

For more advanced scenarios, loops can be nested:

for i in {1..3}; do
   echo "Outer: $i"

   for j in {1..3}; do
      echo " Inner: $j"
   done
done

Output:

Outer: 1
   Inner: 1
   Inner: 2
   Inner: 3
Outer: 2
   Inner: 1
   Inner: 2
   Inner: 3 
Outer: 3
   Inner: 1
   Inner: 2
   Inner: 3

The inner loop executes fully for each iteration of the outer loop.

Nesting provides a further dimension of program control flow allowing complex procedural logic. For example, nest for loops to traverse two-dimensional arrays by rows and columns.

Pitfalls To Avoid

While working with for loops, be aware of some common pitfalls:

Forgetting done

Omitting the terminal done causes scripts to stall by entering an infinite loop. Remember the syntax:

for ITEM; do
   COMMAND
done ← Don‘t forget terminal done! 

Variables Not Available Outside Loop

Variables assigned in loop context vanish outside:

for fruit in ${fruits[@]}; do
   count=$((count + 1)) 
   echo "$count. $fruit"
done

# $count no longer accessible
echo $count 

Use different variable names inside vs outside of loops to avoid this issue.

No Spaces Around = In Conditional

Watch out for spaces in conditional tests like if/while:

# INCORRECT spacing causes syntax error
for fruit in ${fruits[@]}; do
  if [ "$fruit" = "Apple" ]; then  
     echo "$fruit"
  fi
done

# Correct - no spaces around =
for fruit in ${fruits[@]}; do
  if [ "$fruit"="Apple" ]; then
     echo "$fruit" 
  fi
done

Omit spaces around = in bash comparisons.

Globals Modified In Loop

Since loops can run commands multiple times, be cautious modifying global state like variables. Repeated mutations can leave state inconsistent post-loop.

Where possible minimize side effects by using locals inside loop bodies.

Following these best practices avoids frustrating syntax issues or logical flaws when working with for loops.

For Loop vs While Loop

The other common loop construct in bash is the while loop:

COUNT=0
while [ $COUNT -lt 10 ]; do
   echo $COUNT
   COUNT=$((COUNT+1)) 
done

In contrast to iterating a pre-defined list, while loops based on a condition. The block runs repeatedly while the condition holds true.

So in summary:

  • for loops iterate over list of items
  • while loops iterate until condition fails

Typically for loops are preferred since we usually have an explicit set of items to process. While loops suit use cases like reading streams that don‘t have definite length.

Other bash loops like until simply invert the while condition, and select allows interactive menus. But for and while are the workhorses for script iteration.

Conclusion

For loops enable us to automate multi-step routines in bash scripts by iterating over predefined lists.

We walked through different item sources from static arrays, to wildcards globs, output commands, integer ranges, and files. Then explored built-in conditional logic with break/continue to further control execution.

By leveraging the programming flow mechanisms provided by for loops, we can repeat tasks over changing inputs without tedious rework. This unlocks huge productivity gains writing reusable bash scripts.

Hopefully the 20+ examples in this guide provide a solid foundation applying for loops with bash for your next scripting project!

Similar Posts