An Introduction to Shell Scripting with bash (continued)

In the last article, we dealt with variables and positional parameters. This month, it's time to examine the more advanced features that make scripts much more powerful, especially bash's flow control capabilities.

Functions

The bash shell allows us to define functions, which are effectively subroutines. This makes complex scripts significantly simpler, as well as making them faster. You can also define functions in your login scripts, rather like aliases, only more sophisticated. You can define a function like this:

funcname ()
{
shell statements
}

You can do this interactively - for example:

[les@sleipnir les]$ beep ()
> {
> echo -e -n \\a
> }
[les@sleipnir les]$ beep
[les@sleipnir les]$

You can see how, after the parentheses, the shell prompt changes to PS2 (>) to indicate the shell will accept more input without immediately processing it, until the closing curly brace. Once this has been done, you can execute a beep command at any time. (In case you're wondering, aliases take precedence over keywords like if, which take precedence over functions, which take precedence over builtins (like cd) which take precedence over executable files).

Flow Control

A shell script that simply runs some commands is all very well, but it would be a lot better if it could deal with errors rather than plowing on regardless. And looping would be useful - to process lots of files or directories.

if ... else

Almost all programs that run from the command line terminate through an exit() system call which returns an exit status. In general, an exit status value of 0 indicates successful execution, while increasingly large integers indicate increasingly serious errors.

Immediately after a program returns, its exit status can be accessed through the $? shell variable. For example:

[les@sleipnir scripts]$ ls
stat
[les@sleipnir scripts]$ echo $?
0
[les@sleipnir scripts]$ ls nonexistent
ls: nonexistent: No such file or directory
[les@sleipnir scripts]$ echo $?
1
[les@sleipnir scripts]$

As you can see, if the ls command matches some files, its exit status is zero, while if no files are found, the exit status is one. The exit status can be used in scripts - for example, here is an excerpt from a shell script that I commonly use to rebuild kernels:

make bzImage
if [ $? -gt 0 ] ; then
echo make bzImage failed
exit
fi

This introduces the if statement, as well as the -gt comparison operator. The syntax of if is:

if COMMANDS ; then
COMMANDS;
[ elif COMMANDS; then COMMANDS ; ]...
[ else COMMANDS; ]
fi

The square brackets in the syntax above indicate optional components of the statement. However, square brackets, to the bash shell - as in the preceding example - indicate a test (in fact, /usr/bin/[ is a symbolic link to the test binary on many systems). The expression in the square brackets will be evaluated and true or false returned. So in the example above, if the make bzImage step fails, it will return an exit status of 1 or higher. The test says that if the exit status is greater than zero, then the script will print an error message and exit.

You can also test files for various attributes: does the file exist, is it a directory, is it executable, do you have read permission, etc. This is particularly useful in scripts that install software - you can check whether a configuration file already exists, for example, and skip any steps that would over-write it.

Table 1: File operators
Operator Returns true if:
-d filefile exists and is a directory
-e filefile exists
-f filefile exists and is a regular file
-r filefile exists and you have read permission on it
-s filefile exists and is more than 0 bytes in size
-w fileYou have write permission on file
-x fileYou have execute permission on file, or search permission if it is a directory
-O fileYou are the owner of file
-G fileYou are a member of the group that owns the file
file1 -nt file2file1 is newer than file2
file1 -ot file2file1 is older than file2
Remember that many of these commands can be used interactively, at the command prompt, as well as in scripts. For example, here is a command line that will show the disk space used by every subdirectory of the current directory:

for n in * ; do if [ -d $n ]; then du -hs $n ; fi; done

You can also perform string comparisons, as shown in Table 2.

Table 2: String operators
OperatorReturns true if:
str1 = str2str1 matches str2
str1 != str2str1 does not match str2
str1 < str2str1 is less than str2
str1 > str2str1 is greater than str2
-n str1str1 is non-null (i.e. is not an empty string)
-z str1str1 is null (i.e. has length zero)
And finally, you can make comparisons between integer variables, as shown in Table 3. You'll see a little later how to perform arithmetic.

Table 3: Integer operators
OperatorReturns true if:
var1 -lt var2var1 is less than var2
var1 -le var2var1 is less than or equal to var2
var1 -eq var2var1 is equal to var2
var1 -ne var2var1 is not equal to var2
var1 -gt var2 var1 is greater than var2
var1 -ge var2 var1 is greater than or equal to var2

The for Statement

The for statement provides looping within scripts. However, bash's for statement is quite different from that found in C, Perl and similar languages, which surprises novice shell scripters. The syntax of the for statement is:

for varname [in list]
do
 statement
 ...
done

The statements in the loop will probably use $varname in some way. The effect of this statement is to substitute each value in list in turn, into the variable varname, and perform the body of the loop. The list can actually be a wildcard, which the shell will automatically expand into a list of matching filenames, and you can see an example of this in the directory size listing command above.

case

The case statement is essentially a multi-way branch, rather like an if . . ifelif . . . else . . . fi chain. It is particularly common in the SysVInit scripts which are used to start, stop, restart and reload daemons on distributions like Red Hat and Mandrake. The basic syntax of the case statement is:

case varname in
 value1)
   statements
   ;;
 value2)
   statements
   ;;
 *)
   statements
   ;;
esac

You can find lots of examples in the / etc/rc.d/init.d or /etc/init.d directory on your system, and the example at the end of this article.

while & until

More conventional are the while and until looping constructs. The syntax for while is:

while COMMANDS ; do
 COMMANDS
done

Here is an example - a simple script that adds up the values of all integers between 1 and 100:

Listing 1: A shell script to sum the integers between 1 and 100:

#!/bin/bash
# Simple script to demonstrate while and arithmetic
count=0
sum=0
while [ $count -lt 101 ] ; do
 sum=$(( $sum + $count ))
 count=$(( $count + 1 ))
done
echo "Sum = $sum"

This example also shows one way of doing arithmetic in shell scripts, with the $(( expression )) construct.

The until statement is similar, except that it continues looping as long as the test condition fails. I find this quite useful at the command line, as a way of waiting for certain events to occur - for example, waiting for a network link to come up:

until ping -c 1 192.168.170.1; do sleep 60; done; ssh 192.168.170.1

shift

Another statement that is useful in connection with loops is shift. This works on Linux pretty much like it does on DOS/Windows - it shifts the positional parameters (except $0) down by one, so that the old value of $1 is lost and its new value is $2, the new value of $2 is $3, and so on. This allows one-by-one processing of an arbitrarily long list of arguments. Here is an example that demonstrates shift, along with the until statement:

Listing 2: A script to demonstrate shift.

#!/bin/bash
until [ -z $1 ] ; do
 echo $1
 shift
done

This simple script just echoes its arguments, each on a separate line.

Let's finish up with an example. This short script works to remind you to do things at some time (between 1 and 999 minutes) in the future, and illustrates many of the techniques previously discussed:


Listing 3: A short script to set a reminder timer

#!/bin/bash
# Simple reminder shell script
if [ $# -lt 2 ] ; then
 echo "usage: $0 minutes message"
 exit 1
fi

alarm()
{
 sleep ${delay}m
 echo -n $msg
 for i in 1 2 3 4 5 6 7 8 9 ; do
   if [ $i -eq 2 -o $i -eq 4 -o $i -eq 6 -o $i -eq 8 ] ; then
     sleep 1
   else
     echo -n -e \\a
   fi
 done
}

delay=$1
msg=$2

case $1 in
 [0-9] | [0-9][0-9] | [0-9][0-9][0-9] )
   alarm &
   ;;
 *)
   echo 'usage: minutes value must be in range 0 to 999'
   ;;
esac

Page last updated: 20/Nov/2004 Back to Home Copyright © 1987-2010 Les Bell and Associates Pty Ltd. All rights reserved. webmaster@lesbell.com.au

...........................