Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting | ||
---|---|---|
Prev | Chapter 10. Loops and Branches | Next |
A loop is a block of code that iterates (repeats) a list of commands as long as the loop control condition is true.
This is the basic looping construct. It differs significantly from its C counterpart.
for arg in [list]
do
�command...
done
During each pass through the loop, arg takes on the value of each variable in the list. |
for arg in "$var1" "$var2" "$var3" ... "$varN" # In pass 1 of the loop, $arg = $var1 # In pass 2 of the loop, $arg = $var2 # In pass 3 of the loop, $arg = $var3 # ... # In pass N of the loop, $arg = $varN # Arguments in [list] quoted to prevent possible word splitting. |
The argument list may contain wild cards.
If do is on same line as for, there needs to be a semicolon after list.
for arg in [list] ; do
Example 10-1. Simple for loops
#!/bin/bash # List the planets. for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto do echo $planet done echo # Entire 'list' enclosed in quotes creates a single variable. for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto" do echo $planet done exit 0 |
Each [list] element may contain multiple parameters. This is useful when processing parameters in groups. In such cases, use the set command (see Example 11-10) to force parsing of each [list] element and assignment of each component to the positional parameters. |
Example 10-2. for loop with two parameters in each [list] element
#!/bin/bash # Planets revisited. # Associate the name of each planet with its distance from the sun. for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483" do set -- $planet # Parses variable "planet" and sets positional parameters. # the "--" prevents nasty surprises if $planet is null or begins with a dash. # May need to save original positional parameters, since they get overwritten. # One way of doing this is to use an array, # original_params=("$@") echo "$1 $2,000,000 miles from the sun" #-------two tabs---concatenate zeroes onto parameter $2 done # (Thanks, S.C., for additional clarification.) exit 0 |
A variable may supply the [list] in a for loop.
Example 10-3. Fileinfo: operating on a file list contained in a variable
#!/bin/bash # fileinfo.sh FILES="/usr/sbin/privatepw /usr/sbin/pwck /usr/sbin/go500gw /usr/bin/fakefile /sbin/mkreiserfs /sbin/ypbind" # List of files you are curious about. # Threw in a dummy file, /usr/bin/fakefile. echo for file in $FILES do if [ ! -e "$file" ] # Check if file exists. then echo "$file does not exist."; echo continue # On to next. fi ls -l $file | awk '{ print $9 " file size: " $5 }' # Print 2 fields. whatis `basename $file` # File info. echo done exit 0 |
The [list] in a for loop may contain filename globbing, that is, using wildcards for filename expansion.
Example 10-4. Operating on files with a for loop
#!/bin/bash # list-glob.sh: Generating [list] in a for-loop using "globbing". echo for file in * do ls -l "$file" # Lists all files in $PWD (current directory). # Recall that the wild card character "*" matches everything, # however, in "globbing", it doesn't match dot-files. # If the pattern matches no file, it is expanded to itself. # To prevent this, set the nullglob option # (shopt -s nullglob). # Thanks, S.C. done echo; echo for file in [jx]* do rm -f $file # Removes only files beginning with "j" or "x" in $PWD. echo "Removed file \"$file\"". done echo exit 0 |
Omitting the in [list] part of a for loop causes the loop to operate on $@, the list of arguments given on the command line to the script. A particularly clever illustration of this is Example A-14.
Example 10-5. Missing in [list] in a for loop
#!/bin/bash # Invoke both with and without arguments, and see what happens. for a do echo -n "$a " done # The 'in list' missing, therefore the loop operates on '$@' #+ (command-line argument list, including whitespace). echo exit 0 |
It is possible to use command substitution to generate the [list] in a for loop. See also Example 12-34, Example 10-10 and Example 12-31.
Example 10-6. Generating the [list] in a for loop with command substitution
#!/bin/bash # A for-loop with [list] generated by command substitution. NUMBERS="9 7 3 8 37.53" for number in `echo $NUMBERS` # for number in 9 7 3 8 37.53 do echo -n "$number " done echo exit 0 |
This is a somewhat more complex example of using command substitution to create the [list].
Example 10-7. A grep replacement for binary files
#!/bin/bash # bin-grep.sh: Locates matching strings in a binary file. # A "grep" replacement for binary files. # Similar effect to "grep -a" E_BADARGS=65 E_NOFILE=66 if [ $# -ne 2 ] then echo "Usage: `basename $0` string filename" exit $E_BADARGS fi if [ ! -f "$2" ] then echo "File \"$2\" does not exist." exit $E_NOFILE fi for word in $( strings "$2" | grep "$1" ) # The "strings" command lists strings in binary files. # Output then piped to "grep", which tests for desired string. do echo $word done # As S.C. points out, the above for-loop could be replaced with the simpler # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]' # Try something like "./bin-grep.sh mem /bin/ls" to exercise this script. exit 0 |
More of the same.
Example 10-8. Listing all users on the system
#!/bin/bash # userlist.sh PASSWORD_FILE=/etc/passwd n=1 # User number for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" ) # Field separator = : ^^^^^^ # Print first field ^^^^^^^^ # Get input from password file ^^^^^^^^^^^^^^^^^ do echo "USER #$n = $name" let "n += 1" done # USER #1 = root # USER #2 = bin # USER #3 = daemon # ... # USER #30 = bozo exit 0 |
A final example of the [list] resulting from command substitution.
Example 10-9. Checking all the binaries in a directory for authorship
#!/bin/bash # findstring.sh: # Find a particular string in binaries in a specified directory. directory=/usr/bin/ fstring="Free Software Foundation" # See which files come from the FSF. for file in $( find $directory -type f -name '*' | sort ) do strings -f $file | grep "$fstring" | sed -e "s%$directory%%" # In the "sed" expression, #+ it is necessary to substitute for the normal "/" delimiter #+ because "/" happens to be one of the characters filtered out. # Failure to do so gives an error message (try it). done exit 0 # Exercise (easy): # --------------- # Convert this script to taking command-line parameters #+ for $directory and $fstring. |
The output of a for loop may be piped to a command or commands.
Example 10-10. Listing the symbolic links in a directory
#!/bin/bash # symlinks.sh: Lists symbolic links in a directory. ARGS=1 # Expect one command-line argument. if [ $# -ne "$ARGS" ] # If not 1 arg... then directory=`pwd` # current working directory else directory=$1 fi echo "symbolic links in directory \"$directory\"" for file in "$( find $directory -type l )" # -type l = symbolic links do echo "$file" done | sort # Otherwise file list is unsorted. # As Dominik 'Aeneas' Schnitzer points out, #+ failing to quote $( find $directory -type l ) #+ will choke on filenames with embedded whitespace. exit 0 |
The stdout of a loop may be redirected to a file, as this slight modification to the previous example shows.
Example 10-11. Symbolic links in a directory, saved to a file
#!/bin/bash # symlinks.sh: Lists symbolic links in a directory. ARGS=1 # Expect one command-line argument. OUTFILE=symlinks.list # save file if [ $# -ne "$ARGS" ] # If not 1 arg... then directory=`pwd` # current working directory else directory=$1 fi echo "symbolic links in directory \"$directory\"" for file in "$( find $directory -type l )" # -type l = symbolic links do echo "$file" done | sort > "$OUTFILE" # stdout of loop # ^^^^^^^^^^^^ redirected to save file. exit 0 |
There is an alternative syntax to a for loop that will look very familiar to C programmers. This requires double parentheses.
Example 10-12. A C-like for loop
#!/bin/bash # Two ways to count up to 10. echo # Standard syntax. for a in 1 2 3 4 5 6 7 8 9 10 do echo -n "$a " done echo; echo # +==========================================+ # Now, let's do the same, using C-like syntax. LIMIT=10 for ((a=1; a <= LIMIT ; a++)) # Double parentheses, and "LIMIT" with no "$". do echo -n "$a " done # A construct borrowed from 'ksh93'. echo; echo # +=========================================================================+ # Let's use the C "comma operator" to increment two variables simultaneously. for ((a=1, b=1; a <= LIMIT ; a++, b++)) # The comma chains together operations. do echo -n "$a-$b " done echo; echo exit 0 |
See also Example 26-7, Example 26-8, and Example A-6.
---
Now, a for-loop used in a "real-life" context.
Example 10-13. Using efax in batch mode
#!/bin/bash EXPECTED_ARGS=2 E_BADARGS=65 if [ $# -ne $EXPECTED_ARGS ] # Check for proper no. of command line args. then echo "Usage: `basename $0` phone# text-file" exit $E_BADARGS fi if [ ! -f "$2" ] then echo "File $2 is not a text file" exit $E_BADARGS fi fax make $2 # Create fax formatted files from text files. for file in $(ls $2.0*) # Concatenate the converted files. # Uses wild card in variable list. do fil="$fil $file" done efax -d /dev/ttyS3 -o1 -t "T$1" $fil # Do the work. # As S.C. points out, the for-loop can be eliminated with # efax -d /dev/ttyS3 -o1 -t "T$1" $2.0* # but it's not quite as instructive [grin]. exit 0 |
This construct tests for a condition at the top of a loop, and keeps looping as long as that condition is true (returns a 0 exit status). In contrast to a for loop, a while loop finds use in situations where the number of loop repetitions is not known beforehand.
while [condition]
do
�command...
done
As is the case with for/in loops, placing the do on the same line as the condition test requires a semicolon.
while [condition] ; do
Note that certain specialized while loops, as, for example, a getopts construct, deviate somewhat from the standard template given here.
Example 10-14. Simple while loop
#!/bin/bash var0=0 LIMIT=10 while [ "$var0" -lt "$LIMIT" ] do echo -n "$var0 " # -n suppresses newline. var0=`expr $var0 + 1` # var0=$(($var0+1)) also works. done echo exit 0 |
Example 10-15. Another while loop
#!/bin/bash echo while [ "$var1" != "end" ] # while test "$var1" != "end" do # also works. echo "Input variable #1 (end to exit) " read var1 # Not 'read $var1' (why?). echo "variable #1 = $var1" # Need quotes because of "#". # If input is 'end', echoes it here. # Does not test for termination condition until top of loop. echo done exit 0 |
A while loop may have multiple conditions. Only the final condition determines when the loop terminates. This necessitates a slightly different loop syntax, however.
Example 10-16. while loop with multiple conditions
#!/bin/bash var1=unset previous=$var1 while echo "previous-variable = $previous" echo previous=$var1 [ "$var1" != end ] # Keeps track of what $var1 was previously. # Four conditions on "while", but only last one controls loop. # The *last* exit status is the one that counts. do echo "Input variable #1 (end to exit) " read var1 echo "variable #1 = $var1" done # Try to figure out how this all works. # It's a wee bit tricky. exit 0 |
As with a for loop, a while loop may employ C-like syntax by using the double parentheses construct (see also Example 9-25).
Example 10-17. C-like syntax in a while loop
#!/bin/bash # wh-loopc.sh: Count to 10 in a "while" loop. LIMIT=10 a=1 while [ "$a" -le $LIMIT ] do echo -n "$a " let "a+=1" done # No surprises, so far. echo; echo # +=================================================================+ # Now, repeat with C-like syntax. ((a = 1)) # a=1 # Double parentheses permit space when setting a variable, as in C. while (( a <= LIMIT )) # Double parentheses, and no "$" preceding variables. do echo -n "$a " ((a += 1)) # let "a+=1" # Yes, indeed. # Double parentheses permit incrementing a variable with C-like syntax. done echo # Now, C programmers can feel right at home in Bash. exit 0 |
A while loop may have its stdin redirected to a file by a < at its end. |
This construct tests for a condition at the top of a loop, and keeps looping as long as that condition is false (opposite of while loop).
until [condition-is-true]
do
�command...
done
Note that an until loop tests for the terminating condition at the top of the loop, differing from a similar construct in some programming languages.
As is the case with for/in loops, placing the do on the same line as the condition test requires a semicolon.
until [condition-is-true] ; do
Example 10-18. until loop
#!/bin/bash until [ "$var1" = end ] # Tests condition here, at top of loop. do echo "Input variable #1 " echo "(end to exit)" read var1 echo "variable #1 = $var1" done exit 0 |