Flow Control

The Bourne/bash shell supports a variety of conditionals, loops and other flow control operations. You’ll use these often.

if

The if statement is a simple conditional. Its syntax is:

if condition ; then
commands
[elif condition ; then
commands]...
[else
commands] 
fi

This is an if-block, optionally followed by one or more elif-blocks (elif is short for “else if”), optionally followed by an else-block, and terminated by fi.

The if statement does what you’d expect: if the condition is true, it executes the if-block. Otherwise, it executes the else-block, if there is one. The elif construct lets you avoid nesting multiple if statements. For instance:

#!/bin/sh
if [ $USER = root ]; then
echo "Welcome to FooSoft 3.0"
foosoft.sh
else
echo "You must be root to run this script"
exit 666
fi

Notice the square brackets in the condition statement. Actually, only the left bracket is important, because it is aliased to the test command.

The condition can actually be any command. If it returns a zero exit status, the condition is true; otherwise, it is false. Thus, you can write things like:

#!/bin/sh
user=arnie

mytest=`grep $user /etc/passwd|wc -l`
# Notice the backticks, not single quotes, above
if [ $mytest -gt 0 ]; then
echo "$user has an account"
else
echo "$user doesn't have an account"
fi

Create a “password” file named passwords, with two columns: one holding a user name, and the second holding a user password. There should be at least three name/password pairs.

Use the script above to model a test: that the user at least exists in the password file. Name your script passcheck.sh.

Test and run it.

for

The for loop iterates over all of the elements in a list. Its syntax is

for var in list
do
commands
done

The element list is zero or more words (elements). The for construct will assign the variable var to each word in turn, then execute commands. For example, the elements can be listed directly:

#!/bin/bash
for x in red green blue
do
echo $x
done

Which will return:

red
green
blue

Notice that spaces define individual strings. To deal with a string that contains spaces, enclose it in weak quotes (“).

#!/bin/bash
for i in foo bar baz "do be do"; do
echo "$i"
done

This will print:

foo
bar
baz
do be do

Note that if some of the elements will return with embedded spaces, you need to protect them with quotes.

#!/bin/bash
color1="red chile"
color2="green chile"
color3="christmas"
for x in "$color1" "$color2" "$color3"
do
echo "$x"
done

Here’s the point: variables should be protected with quotes unless you are sure that their value does not contain any spaces.

The elements in a for loop need not be listed separately. They can instead be supplied by a command.

#!/bin/bash
for x in $(ls)
do
echo "$x"
done

A for loop may contain two special commands: break and continue. break exits the for loop immediately, jumping to the next statement after done. continue skips the rest of the body of the loop, and jumps back to the top, to for.

Consider:

#remove spaces in file names
for i in *.wma; do mv "$i" `echo $i | tr ’ ’ ’_’`; done

See http://www.linux-mag.com/id/8797/ for an excellent example of for loop usage.

while

The while statement should also be familiar to you from any number of other programming languages. Its syntax in sh is

while condition
do
commands
done

The while loop executes commands as long as the condition is true. Again, the condition can be any command, and is true if the command exits with a zero exit status (in other words, there wasn’t an error). Consider a simple mathematical example:

#!/bin/bash
# set the variable
x=0
# test: less than or equal to 20
while [ $x -le 20 ]
# begin the actual loop
do
# notice that the do command could have been
# on the previous line using the in-line return
# character " ; "
echo $x
# increment x by 1
x=$(($x+1))
#loop
done

A while loop may contain two special commands: break and continue.

break exits the while loop immediately, jumping to the next statement after done.

continue skips the rest of the body of the loop, and jumps back to the top, to condition

Using Command Output to Supply the Elements of a Loop

The elements in a for loop need not be listed separately. They can instead be supplied by a command. Try this loop:

#!/bin/bash
for x in `ls`
do
echo $x
done
  1. What did you get?
  2. Now substitute `who` for the `ls` command.
  3. Create a file listing the contents of your home directory:
    ls ~ > files
  4. How can you make this file supply the list for the for loop?

Globbing

An interesting thing happens when you include a filename wildcard character in a command. Remember that the shell “gets to” the wildcards first, and expands them. By default, wildcards (to the shell) are always references to filenames, unless some other comparison is specified. This is known as “globbing.” Globbing is used mainly in case and for statements.

The shell expands a string containing a * to all filenames that match. The character * by itself expands to a space -delimited list of all files in the working directory (excluding those that start with a dot “.” ).

When a glob begins with * or ?, it does not match files that begin with a dot. To match these, you need to specify the dot explicitly (e.g., .* or /tmp/.*). Under DOS, the pattern *.* matches every file. In sh, it matches every file that contains a dot.

So:

echo *

lists all the non-hidden files and directories in the current directory.

echo *.jpg

lists all the jpeg files.

echo ${HOME}/public_html/*.jpg

lists all jpeg files in your public_html directory.

As it happens, this turns out to be very useful for performing operations on the files in a directory, especially used in conjunction with a for loop. For example:

#!/bin/bash
for x in ${HOME}/public_html/*.htm
do
grep -L '<bold>' "$x"
done

case

The case command’s syntax is:

case value in
pattern1)
commands ;;
#commands are executed until a double-semicolon is hit
pattern2)
commands ;;
esac

value is a string; this is generally either a variable or a back quoted command.

Notice that a case block ends with esac.

pattern is a glob pattern (see globbing, above). For instance,

case pattern in
gle*) command ;;
glen*) command ;;
fred) command ;;
barney) command ;;
esac

The patterns are evaluated in the order in which they occur, and only the first pattern that matches will be executed. To include a “none of the above” clause, use * as your last pattern.

case "$color" in
blue)
echo \$color is blue
;;
green)
echo \$color is green
;;
red|orange)
echo \$color is red or orange
;;
*) echo "Not a match"
;;
esac

The “|” is used to separate multiple patterns; it functions as a logical “or.”

echo -n "Enter the name of an animal: "
read ANIMAL
echo -n "The $ANIMAL has "

case $ANIMAL in
horse | dog | cat) echo -n "four";;
man | kangaroo ) echo -n "two";;
*) echo -n "an unknown number of";;
esac
echo "legs."