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
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
Table 3: Integer operators
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:
#!/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