the bash shell: "for" loops

There are two types of "for" loops in the bash(1) shell:

  1. using the "in" keyword with a list of values ...
    1. using brace expansion for simple numeric sequences
    2. using shell expansion with variable names
    3. using file name globbing
    4. using shell command expansion
  2. using C-like syntax, mostly for numeric sequences

Method 1: looping over a list of values

Syntax:


   for VARNAME in LIST
   do
      # COMMANDS. FOR EXAMPLE ...
         echo ENTERED LOOP
         ############################
         # break example
         #
         if [ "$VARNAME" = STOP ]
         then
            break # break out of loop
         fi
         ############################ #
         continue example
         #
         if [ "$VARNAME" = CONTINUE ]
         then
           echo "BACK TO TOP"
           continue # continue from the top of the loop
         fi
         ############################
         echo MADE IT TO BOTTOM OF LOOP
   done

"LIST" contains a list of values. The list can be variables that contain several words separated by spaces (or more rarely, other field delimiters as set by the IFS variable) the loop will be executed once for each item in the list.

VARNAME is an arbitrary variable name. The current item from the list will be stored in the variable "VARNAME" each time through the loop.

If you don't specify the keyword "in" followed by a list of values in the loop, it will use the positional parameters (i.e the arguments that are passed to the shell script). That is,

     for NAME
  

is essentially an abbreviation for

     for NAME in "$@"
  

which is NOT always equivalent to

     for NAME in $*
  

when the parameters have field separators (typically whitespace) in them. A quoted $@ retains the original parameters as entities, while a $* is parsed as a list of fields. To see this sometimes subtle difference, execute the following (we assume it is called "testit") using

     bash testit A B 'a and b' C D

The "testit" script ...

      #!/bin/bash

      # will print five strings. One will be "a and b"
      i=0
      for NAME in "$@"; do
      echo "@ NAME $((++i)) $NAME"
      done

      # will print five strings. One will be "a and b"
      i=0
      for NAME ; do
      echo " NAME $((++i)) $NAME"
      done

      # will print seven one-word strings.
      i=0
      for NAME in $*; do
      echo "\* NAME $((++i)) $NAME"
      done

      # will print one string.with all the arguments
      i=0
      for NAME in "$*"; do
      echo "\* NAME $((++i)) $NAME"
      done

So "$@" is a useful exception to the general rule of not quoting your list in a "for" loop.

Shell expansions are very useful

Using brace expansion to generate a simple sequence of numbers

   #
   for NUMBER in {1..100..3}  # print values from one to one hundred by threes
   do
      echo NUMBER is $NUMBER
   done
   # is a lot easier than
   #for NUMBER in 1 4 7 10 13 16 19 22 25 28 31 \
   #              34 37 40 43 46 49 52 55 58 61 \
   #              64 67 70 73 76 79 82 85 88 91 \
   #              94 97 100
   # NOTE:
   # to use variables in a brace expansion is difficult because
   # brace expansion is done before any other shell expansion!!!
   # This can lead to very non-intuitive behavior when using it
   # with variables. There are several ways that work, a lot
   # that do not...
   START=1
   END=100
   #for NUMBER in {$START..$END}              # WRONG: WILL NOT WORK
   #LIST={$START..$END}; for NUMER in $LIST   # WRONG AGAIN
   #eval for NUMBER in {$START..$END}         # STILL WRONG, YOU GET THE IDEA.
   for NUMBER in $(eval echo {$START..$END})  # THIS WILL WORK
   do
     echo NUMBER is $NUMBER
   done
   # so I only recommend brace expansion for a fixed sequence of numbers
   # C-style syntax can generate a numeric sequence quite nicely, by the way.
   # As we will see shortly, you can use
   START=1 END=100 INCREMENT=3
   for (( i=$START; i<=$END; i=i+$INCREMENT ))
   do
      echo " i is $i"
   done

You can use the output of any UNIX/GNU command as a list of values.

   #
   # So assuming you have the seq(1) command it is more intuitive to use
   # command output to generate the list, for example.
   START=1 END=100 INCREMENT=3
      for NUMBER in $(seq $START $INCREMENT $END)  # generate list using command
      do
         echo NUMBER is $NUMBER
      done
   This can be very powerful, with uses too numerous to list. Just one more
   example for now ...
      # cut(1) or awk(1) to select fields with different delimiters
      i=1
      for username in $(awk -F: '{print $1}' /etc/passwd)
      do
       echo "Username $((i++)) : $username"
      done

file name globbing can be used to generate the list

   # THIS IS VERY COMMONLY USED TO PERFORM MULTIPLE COMMANDS ON FILES
   #
   echo "Building library ..."
   for SOURCE_FILE in *.c  # loop on all files in current directory ending in .c
   do
      echo FILE IS $SOURCE_FILE
      cc -c $SOURCE_FILE
      ar rv ${SOURCE_FILE%.c}.o
      rm -f ${SOURCE_FILE%.c}.o
   done

COMMON PROBLEMS:

   # DO NOT QUOTE YOUR LIST OR IT WILL BE TREATED AS ONE VARIABLE.
   # MAKE SURE YOU KNOW WHY THE OUTPUT FROM THIS EXAMPLE IS WHAT IT IS
   A=first B=second
   for STRING in "$A $B" 'with blanks' $A $B
   do
      echo STRING "$STRING"
   done

Method 2: Loop using C-like syntax

The second form of the for loop is similar to the for loop in "C" programming, which has three expressions (initialization, conditional and update).

SYNTAX:

   for (( EXPR1; EXPR2; EXPR3 ))
   do
    COMMAND1
    COMMAND2
    ..
   done

Before the first iteration, EXPR1 is evaluated. This is usually used to initialize variables.

All the statements between "do" and "done" are executed repeatedly until the value of EXPR2 is TRUE.

After each iteration of the loop, EXPR3 is evaluated. This is usually used to increment a loop counter.

Infinite for loop

When you don't provide the conditional in the bash C-style for loop (or you provide one that will never be true), it will become an infinite loop.

   for ((i=1; ;i++ ))
   do
      echo "Sleeping for $i seconds (ctrl-C or ctrl-Z might stop me!)"
      sleep $i
   done

Typically, you press Ctrl-C to break out of this infinite loop. Otherwise it will take a LONG time to finally overflow.

Using multiple expressions (comma-separated ) in the for loop header

You can actually have multiple expressions in the three sections of the for loop header, separated by commas.

   for ((i=1, j=10; i*j <= 500 ; i++, j=j+5))
   do
    echo "Numbers:  i=$i: j=$j"
   done

   # ONLY THE LAST EXPRESSION IN THE CONDITIONAL STATEMENT COUNTS
   # FOR EXITING THE LOOP. SO THIS WILL DO THE SAME THING !!:
   # for ((\
           i=1, j=10             ;\
           i<4, j<30, i*j <= 500 ;\
           i++, j=j+5             \
           ))

MISCELLANEOUS:

Sometimes you want a terse one-liner

      for foo in a b c ; do echo $foo; done
  

Note how the semi-colons indicate the end of each statement.

using an alternate field separator instead of whitespace

(       # start subprocess
i=1     # initialize counter
set -f  # no file name globbing
IFS=:   # use : as field separator
for WORD in `grep -w "^$USER:" /etc/passwd`
do
 case "$i" in
  1) echo "==========================================="
     echo "USERNAME $WORD"    ;;
  2)                          ;;
  3) echo "UID $WORD"         ;;
  4) echo "GID $WORD"         ;;
  5) echo "DESCRIPTION $WORD" ;;
  6) echo "HOME $WORD"        ;;
  7) echo "SHELL $WORD"       ;;
  *) echo "SURPRISE: $WORD"   ;;
 esac
 i=$(( i % 7 + 1))
done
)

An array as a list

XNAME=(a b c d e f g)
XNAME[0]=A
XNAME[1]=B
XNAME[2]='C and another C'
echo $XNAME
echo ${XNAME[1]}
echo ${XNAME[2]}
echo ${XNAME[0]}
echo ${XNAME[*]}
echo ${XNAME[@]}

# Note the difference
for STRING in "${XNAME[@]}"
do
   echo "STRING $STRING"
done

for STRING in ${XNAME[*]}
do
   echo "STRING $STRING"
done

# note list is expanded when loop starts even for arrays
for STRING in ${XNAME[*]}
do
   XNAME[i++]=CHANGED$i
   echo "STRING $STRING"
done
echo ARRAY IS NOW ${XNAME[@]}

# note list is expanded when loop starts
A=A
B=B
for STRING in $A $B
do
   echo before STRING $STRING
   A=a
   B=b
   echo after STRING $STRING
done
echo $A $B

Generated by John. S. Urban