Полезная информация



TOC
BACK
FORWARD
HOME

UNIX Unleashed, System Administrator's Edition

- 9 -

Bourne Shell

Written By Richard E. Rummel

Revised By William A. Farra

Presented in this chapter are the fundamentals and many useful specifics of the Bourne shell, currently the most popular of the UNIX shells for execution of application programs. Also described are the steps to organize and script shell commands to produce a program that you can run by name at the shell prompt, or other UNIX methods of program initiation.

The following topics are discussed in this chapter:

  • Shells Basics
    • Invocation
    • Environment
    • Options
    • Special Characters
  • Shell Variables
    • User Defined Variables
    • Environment Variables
    • Positional Variables or Shell Arguments
  • Shell Script Programming
    • Conditional Testing
    • Repetition and loop control
  • Customizing The Shell

Shell Basics

Stephen Bourne wrote the Bourne shell at Bell Laboratories, the development focal point of UNIX. At the time, Bell Laboratories was a subsidiary of AT&T. Since then, many system corporations have produced hardware specific versions of UNIX, but have remarkably kept Bourne Shell basics consistent.


NOTE: $man sh or $man bsh on most UNIX systems will list the generalities of the Bourne Shell as well as detail the specifics to that version of UNIX. It is recommended that the reader familiarize herself with the version she is using before and after reading this chapter.

The Shell Invocation and Environment

The first level of invocation occurs when a user logs on to a UNIX system and is specified by his entry into /etc/passwd file. For example:

farrawa:!:411:102:William Farra, Systems Development,x385:/home/farrawa:/bin/bsh

This entry (which is : delimited) has the login id, encrypted password (denoted by !), user id #, default group id #, comment field, home directory, and startup shell program. In this case, it is the Bourne shell. As the shell executes, it will read the system profile /etc/profile. This may set up various environment variables, such as PATH, that the shell uses to search for executables and TERM, the terminal type being used. Then the shell continues to place the user into the associated home directory and reads the local .profile. Finally the shell displays the default prompt $.


NOTE: On UNIX systems the super-user, also referred to as root, is without restriction. When the super-user logs in, she sees the pound sign (#) as a default prompt. It is a reminder that as super-user some of the built-in protections are not available and that extra care is necessary in this mode. Since the super-user can write to any directory and can remove any file, file permissions do not apply. Normally the root login is only used for system administration and adding or deleting users. It is strongly recommended that only well-experienced UNIX users be given access to root.

Shell Invocation Options When invoking or executing the shell, you can use any of the several options available to the Bourne shell. To test a shell script for syntax, you can use the -n option, which reads the script but does not execute it. If you are debugging a script, the -x option will set the trace mode, displaying each command as it is executed.

The following is a list of Bourne shell options available on most versions of UNIX.
-a Tag all variables for export.
-c "string" Commands are read from string.
-e Non-interactive mode.
-f Disable shell filename generation.
-h Locate and remember functions as defined.
-i Interactive mode.
-k Put arguments in the environment for a command.
-n Reads commands but does not execute them.
-r Restricted mode.
-s Commands are read from the standard input.
-t A single command is executed, and the shell exits.
-u Unset variables are an error during substitution.
-v Verbose mode, displays shell input lines.
-x Trace mode, displays commands as they are executed.

There are many combinations of these options that will work together. Some obviously will not, such as -e, which sets noninteractive mode, and -i, which sets interactive mode. However, experience with options gives the user a multitude of alternatives in creating or modifying his shell environment.

The Restricted Shell

bsh -r or /bin/rsh or /usr/bin/rsh.

Depending on the version of UNIX, this will invoke the Bourne shell in the restricted mode. With this option set, the user cannot change directories (cd), change the PATH variable, specify a full pathname to a command, or redirect output. This ensures an extra measure of control and security to the UNIX system. It is typically used for application users, who never see a shell prompt, and dialup accounts, where security is a must. Normally, a restricted user is placed, from login, into a directory in which she has no write permission. Not having write permission in this directory does not mean that the user has no write permission anywhere. It does mean he cannot change directories or specify pathnames in commands. Also he cannot write a shell script and later access it in his working directory.


NOTE: If the restricted shell calls an unrestricted shell to carry out the commands, the restrictions can be bypassed. This is also true if the user can call an unrestricted shell directly. Remember that programs like vi and more allow users to execute commands. If the command is sh, again it is possible to bypass the restrictions.

Changing Shell Options with set Once the user is at the command prompt $, she can modify her shell environment by setting or unsetting shell options with the set command. To turn on an option, use a - (hyphen) and option letter. To turn off an option, use a + (plus sign) and option letter. Most UNIX systems allow the options a, e, f, h, k, n, u, v, and x to be turned off and on. Look at the following examples:

$set -xv

This enables the trace mode in the shell so that all commands and substitutions are printed. It also displays the line input to shell.

$set +u

This disables error checking on unset variables when substitution occurs. To display which shell options have been set, type the following.

$echo $-
is

This indicates that the shell is in interactive mode and taking commands from standard input. Turning options on and off is very useful when debugging a shell script program or testing a specific shell environment.

The User's Shell Startup File: .profile Under each Bourne shell user's home directory is a file named .profile. This is where a system administrator or user (if given write permission) can make permanent modifications to his shell environment. To add a directory to the existing execution path, just add the following as line into .profile.

PATH=$PATH:/sql/bin ; export PATH

With this line in .profile, from the time the user logs in, the directory /sql/bin is searched for commands and executable programs. To create a variable that contains environment data for an applications program, follow the same procedure.

Shell Environment Variables Once at the command prompt, there are several environment variables that have values. The following is a list of variables found on most UNIX systems.
CDPATH Contains search path(s) for the cd command.
HOME Contains the user's home directory.
IFS Internal field separators, normally space, tab, and newline.
MAIL Path to a special file (mail box), used by UNIX e-mail.
PATH Contains search path(s) for commands and executables.
PS1 Primary prompt string, by default $.
PS2 Secondary prompt string, by default >.
TERM Terminal type being used.

If the restricted mode is not set, these variables can be modified to accommodate the user's various needs. For instance, to change your prompt, type the following.

$PS1="your wish:" ; export PS1

Now instead of a $ for a prompt, your wish: appears. To change it back, type the following.

$PS1="\$" ; export PS1

To display the value(s) in any given variable, type the echo command, space and a $ followed by the variable name.

$echo $MAIL
/usr/spool/mail/(user id)

Care should be used when modifying environment variables. Incorrect modifications to shell environment variables may cause commands either not to function or not to function properly. If this happens, it is recommended that the user log out and log back in. Experience with these variables will give you even more control over your shell environment. Also there is a set of environment variables that are identified by special characters. These are detailed in the next section of the chapter.

Special Characters and Their Meanings

The Bourne shell uses many of the non-alphanumeric characters to define specific shell features. Most of these features fall into four basic categories: special variable names, filename generation, data/program control, and quoting/escape character control. While this notation may seem cryptic at first, this gives the shell environment the ability to accomplish complex functions with a minimal amount of coding.

Special Characters for Shell Variable Names There are special characters that denote special shell variables, automatically set by the shell. As with all variables, they are preceded by a $. The following is a list of these variables.
$# The number of arguments supplied to the command shell.
$- Flags supplied to the shell on invocation or with set.
$? The status value returned by the last command.
$$ The process number of the current shell.
$! The process number of the last child process.
$@ All arguments, individually double quoted.
$* All arguments, double quoted.
$n Positional argument values, where 'n' is the position.
$0 The name of the current shell.

To display the number of arguments supplied to the shell, type the following.

$echo $#
0

This indicates that no arguments were supplied to the shell on invocation. These variables are particularly useful when writing a shell script, which is described later in this chapter in the section Positional Variables or Shell Arguments.

Special Characters for Filename Generation The Bourne shell uses special characters or meta-characters to indicate pattern matches with existing filenames. These are those characters:
* Matches any string or portion of string
? Matches any single character
[-,!] Range, list or not matched

To match files beginning with f, type the following:

$ls f*

To match files with a prefix of invoice, any middle character and a suffix of dat, type the following:

$ls invoice?dat

To match files starting with the letter a through e, type the following:

$ls [a-e]*

To match file starting with a, c, and e, type the following:

$ls [a,c,e]*

To exclude a match with the letter m, type following:

$ls [!m]* 


NOTE: To use the logical not symbol, !, it must be the first character after the left bracket, [.

Special Characters for Data/Program Control The Bourne shell uses special characters for data flow and execution control. With these characters, the user can send normal screen output to a file or device or as input to another command. These characters also allow the user to run multiple commands sequentially or independently from the command line. The following is a list of those characters.
>(file) Redirect output to a file.
>>(file) Redirect and append output to the end of a file.
<(file) Redirect standard input from a file.
; Separate commands.
| Pipe standard output to standard input.
& Place at end of command to execute in background.
'' Command substitution, redirect output as arguments.

There are many ways to use these controls and those are described in more detail later in this chapter in the section Entering Simple Commands.

Special Characters for Quoting and Escape The Bourne shell uses the single quotes, '', and double quotes, "", to encapsulate special characters or space delineated words to produce a single data string. The major difference between single and double quotes is that in using double quotes, variable and command substitution is active as well as the escape character.

$echo "$HOME $PATH"
$/u/farrawa /bin:/etc:/usr/bin:

This example combined the values of $HOME and $PATH to produce a single string.

$echo '$HOME $PATH'
$$HOME $PATH

This example simply prints the string data enclosed. The shell escape character is a backslash \, which is used to negate the special meaning or shell function of the following character.

$echo \$HOME $PATH
$$HOME /bin:/etc:/usr/bin:

In this example, only the $ of $HOME is seen as text, and the variable meaning the shell is negated, $PATH, is still interpreted as a variable.

How the Shell Interprets Commands

The first exposure most people have to the Bourne shell is as an interactive shell. After logging on the system and seeing any messages from the system administrator, the user sees a shell prompt. For users other than the super-user, the default prompt for the interactive Bourne shell is a dollar sign ($). When you see the dollar sign ($), the interactive shell is ready to accept a line of input, which it interprets.

The shell sees a line of input as a string of characters terminated with a newline character, which is usually the result of pressing Enter on your keyboard. The length of the input line has nothing to do with the width of your computer display. When the shell sees the newline character, it begins to interpret the line.

Entering Simple Commands

The most common form of input to the shell is the simple command, in which a command name is followed by any number of arguments. In the example

$ ls file1 file2 file3

ls is the command and file1, file2, and file3 are the arguments. The command is any UNIX executable. It is the responsibility of the command, not the shell, to interpret the arguments. Many UNIX commands, but certainly not all, take the following form:

$ command -options filenames

Although the shell does not interpret the arguments of the command, the shell does make some interpretation of the input line before passing the arguments to the command. Special characters, when you enter them on a command line, cause the shell to redirect input and output, start a different command, search the directories for filename patterns, substitute variable data, and substitute the output of other commands.

Substitution of Variable Data Many of the previous examples in this chapter have used variables in the command line. Whenever the shell sees(not quoted or escaped) a dollar sign $, it interprets the following qualified text as a variable name. Whether the variable is environmental or user defined, the data stored in the variable is substituted on the command line. For example, the command

$ ls $HOME

lists the contents of the user's home directory, regardless of what the current working directory is. HOME is an environment variable. Variables are discussed in more detail in the next major section of this chapter. As in filename substitution, the ls command sees only the result of the substitution, not the variable name.

You can substitute variable names anywhere in the command line, including for the command name itself. For example,

$ dir=ls
$ $dir f*
file1
file1a
form

This example points out that the shell makes its substitutions before determining what commands to execute.

Redirection of Input and Output When the shell sees the input (<) or output (>) redirection characters, the argument following the redirection symbol is sent to the subshell that controls the execution of the command. When the command opens the input or output file that has been redirected, the input or output is redirected to the file.

$ ls -l >dirfile

In this example, the only argument passed on to ls is the option -l. The filename dirfile is sent to the subshell that controls the execution of ls. To append output to an existing file, use (>>).

$ ls -l /tmp >> dirfile

This example takes the file listing from the /tmp directory and appends it to the end of the local file dirfile.

Entering Multiple Commands on One Line Ordinarily, the shell interprets the first word of command input as the command name and the rest of the input as arguments to that command. The shell special character--the semicolon, ;,--indicates to the shell that the preceding command text is ended and the following is a new command. For example, the command line

$ who -H; df -v; ps -e

is the equivalent of

$ who -H
$ df -v
$ ps -e

In the second case, however, the results of each command appear between the command input lines. When you use the semicolon to separate commands on a line, the commands are executed in sequence. The shell waits until one command is complete before executing the next. Also, if there is an error, the shell will stop executing the command line at the position the error occurred.

Linking Multiple Commands with Pipes One of the most powerful features of the Bourne shell is its ability to take standard output from one command and used it as standard input to another. This is accomplished with the pipe symbol, |,. When the shell sees a pipe, it executes the preceding command and then creates a link to the standard input of the following command in the order the commands are on the command line. For example

$who | grep fred

This takes the list of users logged in from the who command and searches the list for the string fred using the grep. This creates output only if user fred is logged in.

$ls -ls | sort -nr | pg

This creates a list of files in the current directory with the block size as the first data item of each line. It hen sorts the list in reverse numeric order and finally pages the output on the screen. This results in paged listing of all files by size with the largest on top. This is useful when trying to determine where disk space is being consumed. Any UNIX command that takes input from standard input and sends output to standard output can be linked using the pipe.

Entering Commands to Process in Background To take advantage of the UNIX ability to multitask, the shell allows commands to be processed in background. This is accomplished by placing the ampersand symbol, &, at the end of a command. For example,

$find / -name "ledger" -print > find.results 2>/dev/null &

This command line searches the entire file system for files named ledger, sends its output to a local file named find.results, eliminates unwanted errors, and processes this command independent of the current shell (in background).

$wc -l < chapter5.txt > chapter5.wcl 2> chapter5.err &

In this example, the command wc takes it input from the file chapter5.txt, sends its output (a line count) to the file chapter5.wcl, send errors to the file chapter5.err, and executes in background.


NOTE: If a user has processes running in the background and she logs off, most UNIX systems will terminate the processes owned by that login. Also when you enter a command to process in background, the shell will return and display the process ID number.

Substituting the Results of Commands in a Command Line

Sometimes it is useful to pass the output or results of one command as arguments to another command. You do so by using the shell special character, the back quotation mark (´´). You use the back quotation marks in pairs. When the shell sees a pair of back quotation marks, it executes the command inside the quotation marks and substitutes the output of that command in the original command line. You most commonly use this method to store the results of command executions in variables. To store the five-digit Julian date in a variable, for example, you use the following command:

$ julian=´´date ´´+%y%j´´´´

The back quotation marks cause the date command to be executed before the variable assignment is made. Back quotation marks can be extremely useful when you're performing arithmetic on shell variables; see "Shell Programming" later in this chapter.

Shell Variables

In algebra, variables are symbols that stand for some value. In computer terminology, variables are symbolic names that stand for some value. Earlier in this chapter, you saw how the variable HOME stood for the name of a user's home directory. If you enter the change directory command, cd, without an argument, cd takes you to your home directory. Does a generic program like cd know the location of every user's home directory? Of course not, it merely knows to look for a variable, in this case HOME, which stands for the home directory.

Variables are useful in any computer language because they allow you to define what to do with a piece of information without knowing specifically what the data is. A program to add two and two is not very useful, but a program that adds two variables can be, especially if the value of the variables can be supplied at execution time by the user of the program. The Bourne shell has four types of variables: user-defined variables, positional variables or shell arguments, predefined or special variables, and environment variables.

Storing Data or User-Defined Variables

As the name implies, user-defined variables are whatever you want them to be. Variable names are comprised of alphanumeric characters and the underscore character, with the provision that variable names do not begin with one of the digits 0 through 9. (Like all UNIX names, variables are case sensitive. Variable names take on values when they appear in a command line to the left of an equal sign (=). For example, in the following command lines, COUNT takes on the value of 1, and NAME takes on the value of Stephanie:

$ COUNT=1
$ NAME=Stephanie


TIP: Because most UNIX commands are lowercase words, shell programs have traditionally used all capital letters in variable names. It is certainly not mandatory to use all capital letters, but using them enables you to identify variables easily within a program.

To recall the value of a variable, precede the variable name by a dollar sign ($):

$ NAME=John
$ echo Hello $NAME
Hello John

You also can assign variables to other variables, as follows:

$ JOHN=John
$ NAME=$JOHN
$ echo Goodbye $NAME
Goodbye John

Sometimes it is useful to combine variable data with other characters to form new words, as in the following example:

$ SUN=Sun
$ MON=Mon
$ TUE=Tues
$ WED=Wednes
$ THU=Thurs
$ FRI=Fri
$ SAT=Satur
$ WEEK=$SAT
$ echo Today is $WEEKday
Today is
$

What happened here? Remember that when the shell's interpreter sees a dollar sign ($), it interprets all the characters until the next white space as the name of a variable, in this case WEEKday. You can escape the effect of this interpretation by enclosing the variable name in curly braces ({,}) like this:

$ echo Today is ${WEEK}day
Today is Saturday
$

You can assign more than one variable in a single line by separating the assignments with white space, as follows:

$ X=x Y=y

The variable assignment is performed from right to left:

$ X=$Y Y=y
$ echo $X
y
$ Z=z Y=$Z
$ echo $Y

$

You may notice that when a variable that has not been defined is referenced, the shell does not give you an error but instead gives you a null value.

You can remove the value of a variable using the unset command, as follows:

$ Z=hello
$ echo $Z
hello
$ unset Z
$ echo $Z

$

Conditional Variable Substitution

The most common way to retrieve the value of a variable is to precede the variable name with a dollar sign ($), causing the value of the variable to be substituted at that point. With the Bourne shell, you can cause variable substitution to take place only if certain conditions are met. This is called conditional variable substitution. You always enclose conditional variable substitutions in curly braces ({ }).

Substituting Default Values for Variables As you learned earlier, when variables that have not been previously set are referenced, a null value is substituted. The Bourne shell enables you to establish default values for variable substitution using the form

${variable:-value}

where variable is the name of the variable and value is the default substitution. For example,

$ echo Hello $UNAME
Hello
$ echo Hello ${UNAME:-there}
Hello there
$ echo $UNAME
$
$ UNAME=John
$ echo Hello ${UNAME:-there}
Hello John
$

As you can see in the preceding example, when you use this type of variable substitution, the default value is substituted in the command line, but the value of the variable is not changed. Another substitution construct not only substitutes the default value but also assigns the default value to the variable as well. This substitution has the form

${variable:=value}

which causes variable to be assigned value after the substitution has been made. For example,

$ echo Hello $UNAME
Hello
$ echo Hello ${UNAME:=there}
Hello there
$ echo $UNAME
there
$ UNAME=John
$ echo Hello ${UNAME:-there}
Hello John
$

The substitution value need not be literal; it can be a command in back quotation marks:

USERDIR={$MYDIR:-'pwd'}

A third type of variable substitution substitutes the specified value if the variable has been set, as follows:

${variable:+value}

If variable is set, then value is substituted; if variable is not set, then nothing is substituted. For example,

$ ERROPT=A
$ echo ${ERROPT:+"Error Tracking is Active"}
Error Tracking is Active
$ ERROPT=
$ echo ${ERROPT:+"Error Tracking is Active"}

$

Conditional Variable Substitution with Error Checking Another variable substitution method allows for error checking during variable substitution:

${variable:?message}

If variable is set, its value is substituted; if it is not set, message is written to the standard error file. If the substitution is made in a shell program, the program immediately terminates. For example,

$ UNAME=
$ echo ${ UNAME:?"UNAME has not been set"}
UNAME has not been set
$ UNAME=Stephanie
$ echo ${UNAME:?"UNAME has not been set"}
Stephanie
$

If no message is specified, the shell displays a default message, as in the following example:

$ UNAME=
$ echo ${UNAME:?}
sh: UNAME: parameter null or not set
$

Positional Variables or Shell Arguments

You may recall that when the shell's command-line interpreter processes a line of input, the first word of the command line is considered to be an executable file, and the remainder of the line is passed as arguments to the executable. If the executable is a shell program, the arguments are passed to the program as positional variables. The first argument passed to the program is assigned to the variable $1, the second argument is $2, and so on up to $9. Notice that the names of the variables are actually the digits 1 through 9; the dollar sign, as always, is the special character that causes variable substitution to occur.

The positional variable $0 always contains the name of the executable. Positional variables are discussed later in this chapter in the section "Shell Programming."

Preventing Variables from Being Changed If a variable has a value assigned, and you want to make sure that its value is not subsequently changed, you may designate a variable as a read-only variable with the following command:


readonly variable

From this point on, variable cannot be reassigned. This ensures that a variable won't be accidentally changed.

Making Variables Available to Subshells with export When a shell executes a program, it sets up a new environment for the program to execute in. This is called a subshell. In the Bourne shell, variables are considered to be local variables; in other words, they are not recognized outside the shell in which they were assigned a value. You can make a variable available to any subshells you execute by exporting it using the export command. Your variables can never be made available to other users.

Now suppose you start a new shell.

Enter Command: sh
$ exit
Enter Command:

When you started a new shell, the default shell prompt appeared. This is because the variable assignment to PS1 was made only in the current shell. To make the new shell prompt active in subshells, you must export it as in the following example.

$ PS1="Enter Command: "
Enter Command: export PS1
Enter Command: sh
Enter Command:

Now the variable PS1 is global; that is, it is available to all subshells. When a variable has been made global in this way, it remains available until you log out of the parent shell. You can make an assignment permanent by including it in your .profile, see "Customizing the Shell."

Shell Script Programming

In this major section, you learn how to put commands together in such a way that the sum is greater than the parts. You learn some UNIX commands that are useful mainly in the context of shell programs. You also learn how to make your program perform functions conditionally based on logical tests that you define, and you learn how to have parts of a program repeat until its function is completed. In short, you learn how to use the common tools supplied with UNIX to create more powerful tools specific to the tasks you need to perform.

What Is a Program?

A wide assortment of definitions exist for what is a computer program, but for this discussion, a computer program is an ordered set of instructions causing a computer to perform some useful function. In other words, when you cause a computer to perform some tasks in a specific order so that the result is greater than the individual tasks, you have programmed the computer. When you enter a formula into a spreadsheet, for example, you are programming. When you write a macro in a word processor, you are programming. When you enter a complex command like

$ ls -R / | grep myname | pg

in a UNIX shell, you are programming the shell; you are causing the computer to execute a series of utilities in a specific order, which gives a result that is more useful than the result of any of the utilities taken by itself.

A Simple Program

Suppose that daily you back up your data files with the following command:

$ cd /usr/home/myname; ls * | cpio -o >/dev/rmt0

As you learned earlier, when you enter a complex command like this, you are programming the shell. One of the useful things about programs, though, is that they can be placed in a program library and used over and over, without having to do the programming each time. Shell programs are no exception. Rather than enter the lengthy backup command each time, you can store the program in a file named backup:

$ cat >backup
cd /usr/home/myname
ls * | cpio -o >/dev/rmt0
Ctrl+d

You could, of course, use your favorite editor (see UNIX Unleashed, Internet Edition, Chapter 3, "Text Editing with vi and emacs"), and in fact with larger shell programs, you almost certainly will want to. You can enter the command in a single line, as you did when typing it into the command line, but because the commands in a shell program (sometimes called a shell script) are executed in sequence, putting each command on a line by itself makes the program easier to read. Creating easy-to-read programs becomes more important as the size of the programs increase.

Now to back up your data files, you need to call up another copy of the shell program (known as a subshell) and give it the commands found in the file backup. To do so, use the following command:

$ sh backup

The program sh is the same Bourne shell that was started when you logged in, but when a filename is passed as an argument, instead of becoming an interactive shell, it takes its commands from the file.

An alternative method for executing the commands in the file backup is to make the file itself an executable. To do so, use the following command:

$ chmod +x backup

Now you can back up your data files by entering the newly created command:

$ backup

If you want to execute the commands in this manner, the file backup must reside in one of the directories specified in the environment variable $PATH.

The Shell as a Language

If all you could do in a shell program was to string together a series of UNIX commands into a single command, you would have an important tool, but shell programming is much more. Like traditional programming languages, the shell offers features that enable you to make your shell programs more useful, such as: data variables, argument passing, decision making, flow control, data input and output, subroutines, and handling interrupts.

By using these features, you can automate many repetitive functions, which is, of course, the purpose of any computer language.

Using Data Variables in Shell Programs

You usually use variables within programs as place holders for data that will be available when the program is run and that may change from execution to execution. Consider the backup program:

cd /usr/home/myname
ls | cpio -o >/dev/rmt0

In this case, the directory to be backed up is contained in the program as a literal, or constant, value. This program is useful only to back up that one directory. The use of a variable makes the program more generic:

cd $WORKDIR
ls * | cpio -o >/dev/rmt0

With this simple change, any user can use the program to back up the directory that has been named in the variable $WORKDIR, provided that the variable has been exported to subshells. See "Making Variables Available to Subshells with export" earlier in this chapter.

Entering Comments in Shell Programs

Quite often when you're writing programs, program code that seemed logical six months ago may be fairly obscure today. Good programmers annotate their programs with comments. You enter comments into shell programs by inserting the pound sign (#) special character. When the shell interpreter sees the pound sign, it considers all text to the end of the line as a comment.

Doing Arithmetic on Shell Variables

In most higher level programming languages, variables are typed, meaning that they are restricted to certain kinds of data, such as numbers or characters. Shell variables are always stored as characters. To do arithmetic on shell variables, you must use the expr command.

The expr command evaluates its arguments as mathematical expressions. The general form of the command is as follows:

expr integer operator integer

Because the shell stores its variables as characters, it is your responsibility as a shell programmer to make sure that the integer arguments to expr are in fact integers. Following are the valid arithmetic operators:
+ Adds the two integers.
- Subtracts the second integer from the first.
* Multiplies the two integers.
/ Divides the first integer by the second.
% Gives the modulus (remainder) of the division.

$ expr 2 + 1
3
$ expr 5 - 3
2

If the argument to expr is a variable, the value of the variable is substituted before the expression is evaluated, as in the following example:

$ $int=3
$ expr $int + 4
7

You should avoid using the asterisk operator (*) alone for multiplication. If you enter

$ expr 4 * 5

you get an error because the shell sees the asterisk and performs filename substitution before sending the arguments on to expr. The proper form of the multiplication expression is

$ expr 4 \* 5
20

You also can combine arithmetic expressions, as in the following:

$ expr 5 + 7 / 3
7

The results of the preceding expression may seem odd. The first thing to remember is that division and multiplication are of a higher precedence than addition and subtraction, so the first operation performed is 7 divided by 3. Because expr deals only in integers, the result of the division is 2, which is then added to 5, giving the final result 7. Parentheses are not recognized by expr, so to override the precedence, you must do that manually. You can use back quotation marks to change the precedence, as follows:

$ int='expr 5 + 7'
$ expr $int / 3
4

Or you can use the more direct route:

$ expr 'expr 5 + 7' / 3
4

Passing Arguments to Shell Programs

A program can get data in two ways: either it is passed to the program when it is executed as arguments, or the program gets data interactively. An editor such as vi is usually used in an interactive mode, whereas commands such as ls and expr get their data as arguments. Shell programs are no exception. In the section "Reading Data into a Program Interactively," you see how a shell program can get its data interactively.

Passing arguments to a shell program on a command line can greatly enhance the program's versatility. Consider the inverse of the backup program presented earlier:

$ cat >restoreall
cd $WORKDIR
cpio -i </dev/rmt0
Ctrl+d

As written, the program restoreall reloads the entire tape made by backup. But what if you want to restore only a single file from the tape? You can do so by passing the name of the file as an argument. The enhanced restore1 program is now:

# restore1 - program to restore a single file
cd $WORKDIR
cpio -i $1 </dev/rmt0

Now you can pass a parameter representing the name of the file to be restored to the restore1 program:

$ restore1 file1

Here, the filename file1 is passed to restore1 as the first positional parameter. The limitation to restore1 is that if you want to restore two files, you must run restore1 twice.

As a final enhancement, you can use the $* variable to pass any number of arguments to the program:

# restoreany - program to restore any number of files
cd $WORKDIR
cpio -i $* </dev/rmt0

$ restoreany file1 file2 file3

Because shell variables that have not been assigned a value always return null, or empty, if the restore1 or restoreany programs are run with no command-line parameters, a null value is placed in the cpio command, which causes the entire archive to be restored.

Consider the program in listing 9.1; it calculates the length of time to travel a certain distance.

Listing 9.1. Program example with two parameters.

# traveltime - a program to calculate how long it will
# take to travel a fixed distance
# syntax: traveltime miles mph
X60=´´expr $1 \* 60´´
TOTMINUTES=´´expr $X60 / $2´´
HOURS=´´expr $TOTMINUTES / 60´´
MINUTES=´´expr $TOTMINUTES % 60´´
echo "The trip will take $HOURS hours and $MINUTES minutes"

The program in listing 9.1 takes two positional parameters: the distance in miles and the rate of travel in miles per hour. The mileage is passed to the program as $1 and the rate of travel as $2. Note that the first command in the program multiplies the mileage by 60. Because the expr command works only with integers, it is useful to calculate the travel time in minutes. The user-defined variable X60 holds an interim calculation that, when divided by the mileage rate, gives the total travel time in minutes. Then, using both integer division and modulus division, the number of hours and number of minutes of travel time is found.

Now execute the traveltime for a 90-mile trip at 40 mph with the following command line:

$ traveltime 90 40
The trip will take 2 hours and 15 minutes

Decision Making in Shell Programs

One of the things that gives computer programming languages much of their strength is their capability to make decisions. Of course, computers don't think, so the decisions that computer programs make are only in response to conditions that you have anticipated in your program. The decision making done by computer programs is in the form of conditional execution: if a condition exists, then execute a certain set of commands. In most computer languages, this setup is called an if-then construct.

The if-then Statement The Bourne shell also has an if-then construct. The syntax of the construct is as follows:

if command_1
then
  command_2
  command_3
fi
command_4

You may recall that every program or command concludes by returning an exit status. The exit status is available in the shell variable $?. The if statement checks the exit status of its command. If that command is successful, then all the commands between the then statement and the fi statement are executed. In this program sequence, command_1 is always executed, command_2 and command_3 are executed only if command_1 is successful, and command_4 is always executed.

Consider a variation of the backup program, except that after copying all the files to the backup media, you want to remove them from your disk. Call the program unload and allow the user to specify the directory to be unloaded on the command line, as in the following example:

# unload - program to backup and remove files
# syntax - unload directory
cd $1
ls -a | cpio -o >/dev/rmt0
rm *

At first glance, it appears that this program will do exactly what you want. But what if something goes wrong during the cpio command? In this case, the backup media is a tape device. What if the operator forgets to insert a blank tape in the tape drive? The rm command would go ahead and execute, wiping out the directory before it has been backed up! The if-then construct prevents this catastrophe from happening. A revised unload program is shown in listing 9.2.

Listing 9.2. Shell program with error checking.

# unload - program to backup and remove files
# syntax - unload directory
cd $1
if ls -a | cpio -o >/dev/rmt0
then
   rm *
fi

In the program in listing 9.2, the rm command is executed only if the cpio command is successful. Note that the if statement looks at the exit status of the last command in a pipeline.

Data Output from Shell Programs The standard output and error output of any commands within a shell program are passed on the standard output of the user who invokes the program unless that output is redirected within the program. In the example in listing 9.2, any error messages from cpio would have been seen by the user of the program. Sometimes you may write programs that need to communicate with the user of the program. In Bourne shell programs, you usually do so by using the echo command. As the name indicates, echo simply sends its arguments to the standard output and appends a newline character at the end, as in the following example:

$ echo "Mary had a little lamb"
Mary had a little lamb

The echo command recognizes several special escape characters that assist in formatting output. They are as follows:
\b Backspace
\c Prints line without newline character
\f Form Feed: advances page on a hard copy printer; advances to new screen on a display terminal
\n Newline
\r Carriage return
\t Tab
\v Vertical Tab
\\ Backslash
\0nnn A one-, two-, or three-digit octal integer representing one of the ASCII characters

If you want to display a prompt to the user to enter the data, and you want the user response to appear on the same line as the prompt, you use the \c character, as follows:

$ echo "Enter response:\c"
Enter response$

The if-then-else Statement A common desire in programming is to perform one set of commands if a condition is true and a different set of commands if the condition is false. In the Bourne shell, you can achieve this effect by using the if-then-else construct:

if command_1
then
   command_2
   command_3
else
   command_4
   command_5
fi

In this construct, command_1 is always executed. If command_1 succeeds, the command_2 and command_3 are executed; if it fails, command_4 and command_5 are executed.

You can now enhance the unload program to be more user friendly. For example,

# unload - program to backup and remove files
# syntax - unload directory
cd $1
if ls -a | cpio -o >/dev/rmt0
then
   rm *
else
   echo "A problem has occurred in creating the backup."
   echo "The directory will not be erased."
   echo "Please check the backup device and try again."
fi


TIP: Because the shell ignores extra whitespace in a command line, good programmers use this fact to enhance the readability of their programs. When commands are executed within a then or else clause, indent all the commands in the clause the same distance.

Testing Conditions with test You've seen how the if statement tests the exit status of its command to control the order in which commands are executed, but what if you want to test other conditions? A command that is used a great deal in shell programs is the test command. The test command examines some condition and returns a zero exit status if the condition is true and a nonzero exit status if the condition is false. This capability gives the if statement in the Bourne shell the same power as other languages with some enhancements that are helpful in shell programming.

The general form of the command is as follows:

test condition

The conditions that can be tested fall into four categories: 1) String operators that test the condition or relationship of character strings; 2) Integer relationships that test the numerical relationship of two integers; 3) File operators that test for the existence or state of a file; 4) Logical operators that allow for and/or combinations of the other conditions.

Testing Character Data You learned earlier that the Bourne shell does not type cast data elements. Each word of an input line and each variable can be taken as a string of characters. Some commands, such as expr and test, have the capability to perform numeric operations on strings that can be translated to integer values, but any data element can be operated on as a character string.

You can compare two strings to see whether they are equivalent or not equivalent. You also can test a single string to see whether it has a value or not. The string operators are as follows:
str1 = str2 True if str1 is the same length and contains the same characters as str2
str1 != str2 True if str1 is not the same as str2
-n str1 True if the length of str1 is greater than 0 (is not null)
-z str1 True if str1 is null (has a length of 0)
str1 True if str1 is not null

Even though you most often use test with a shell program as a decision maker, test is a program that can stand on its own as in the following:

$ str1=abcd
$ test $str1 = abcd
$ echo $?
0
$

Notice that unlike the variable assignment statement in the first line in the preceding example, the test command must have the equal sign surrounded by white space. In this example, the shell sends three arguments to test. Strings must be equivalent in both length and characters by character.

$ str1="abcd "
$ test "$str1" = abcd
$ echo $?
1
$

In the preceding example, str1 contains five characters, the last of which is a space. The second string in the test command contains only four characters. The nonequivalency operator returns a true value everywhere that the equivalency operator returns false.

$ str1=abcd
$ test $str1 != abcd
$ echo $?
1
$

Two of the string operations, testing of a string with no operator and testing with the -n operator, seem almost identical, as the following example shows.

$ str1=abcd
$ test $str1
$ echo $?
0
$ test -n $str1
$ echo $?
0
$

The difference between the two commands in the preceding example is a subtle one, but it points out a potential problem in using the test command, as shown in the following example of two different tests:

$ str1="      "
$ test $str1
$ echo $?
1
$ test "$str1"
$ echo $?
0
$ test -n $str1
test: argument expected
$ test -n "$str1
$ echo $?
0

In the preceding example, the first test is false. Why? Remember that the shell interpreter makes variable substitutions before it processes the command line, and when it processes the command line, it removes excess whitespace. Where $str1 does not have double quotation marks, the blanks are passed to the command line and stripped; when the double quotation marks are used, the blanks are passed on to test. What happens in the third test? When the interpreter removes the whitespace, test is passed only the -n option, which requires an argument.

Testing Numeric Data The test command, like expr, has the capability to convert strings to integers and perform numeric operations. Whereas expr performs arithmetic on integers, test performs logical comparisons. The available numerical comparisons are as follows:
int1 -eq int2 True if int1 is numerically equal to int2
int1 -ne int2 True if int1 is not equal to int2
int1 -gt int2 True if int1 is greater than int2
int1 -ge int2 True if int1 is greater than or equal to int2
int1 -lt int2 True if int1 is less than int2
int1 -le int2 True if int1 is less than or equal to int2
This difference between numeric equivalency and string equivalency is shown in the following example, which defines two strings and then compares them using numeric equivalency first and then string equivalency.

$ str1=1234
$ str2=01234
$ test $str1 = $str2
$ echo $?
1
$ test $str1 -eq $str2
$ echo $?
0
$

In the second case here, the strings were converted to integers and found to be numerically equivalent, whereas the original strings were not.

Testing for Files The third type of condition that test can examine is the state of files. Using the test command in your program, you can determine whether a file exists, whether it can be written to, and several other conditions. All the file test options return true, only if the file exists. The file test options are
-r filenm True if the user has read permission
-w filenm True if the user has write permission
-x filenm True if the user has execute permission
-f filenm True if filenm is a regular file
-d filenm True if filenm is a directory
-c filenm True if filenm is a character special file
-b filenm True if filenm is a block special file
-s filenm True if the size of filenm is not zero
-t fnumb True if the device associated with the file descriptor fnumb (1 by default) is a terminal device

Combining and Negating test Conditions The expressions that have been discussed thus far are called primary expressions because each tests only one condition. The characters following the hyphen are the operators, and the terms to the right and left of the operators are the arguments. Some of the operators, like the numeric comparison operators, are binary because they always have two arguments, one on the right and one on the left. Some of the operators, like the file test options, are unary because the operator takes only one argument, which is always placed on the right.

Sometimes you may not be interested in what is true, but in what is not true. To find out what is not true, you can use the unary negation operator, the exclamation (!), in front of any primary. Create an empty file and try some of the file operators shown in the following example:

$ cat >empty
Ctrl+d
$ test -r empty
$ echo $?
0
$ test -s empty
$ echo $?
1
$ test ! -s empty
$ echo $?
0
$

The primary expressions in a test command can be combined with a logical and operator, -a, or with a logical or operator, -o. When you use the -a operator, the combined expression is true if and only if both of the primary expressions are true. When you use the -o operator, the combined expression is true if either of the primary expressions is true. Using the empty file from above, test to see whether the file is readable and contains data:

$ test -r empty -a -s empty
$ echo $?
1
$

The combined expression is false. The first expression is true because the file is readable, but the second expression fails because the file has a size of 0.

A Shorthand Method of Doing Tests Because the test command is such an important part of shell programming, and to make shell programs look more like programs in other languages, the Bourne shell has an alternative method for using test: you enclose the entire expression in square brackets ([]).

$ int1=4
$ [ $int1 -gt 2 ]
$ echo $?
0
$

Remember that even though it looks different, the preceding example is still the test command and the same rules apply.

Using test, you can make the unload program from listing 9.2 more user friendly, as well as more bullet proof, by making sure that a valid directory name is entered on the command line. The revised program is shown in listing 9.3.

Listing 9.3. Program using test for error checking.

# unload - program to backup and remove files
# syntax - unload directory
# check arguments
if [ $# -ne 1 ]
then
   echo "usage: unload directory"
   exit 1
fi
# check for valid directory name
if [! -d "$1" ]
then
   echo "$1 is not a directory"
   exit 2
fi
cd $1
ls -a | cpio -o >/dev/rmt0
if [ $? -eq 0 ]
then
   rm *
else
   echo "A problem has occurred in creating the backup."
   echo "The directory will not be erased."
   echo "Please check the backup device and try again."

   exit 3
fi

There are several items of interest in the revised program in listing 9.3. One is the introduction of the exit statement. The exit statement has two purposes: to stop any further commands in the program from being executed and to set the exit status of the program. By setting a nonzero exit status, subsequent programs can check the $? variable to see whether unload is successful. Notice that in the test to see whether the argument is a valid directory, the variable substitution is made within double quotation marks. Using double quotation marks prevents the test command from failing if the program were called with an argument containing only blanks; the test still fails, but the user does not see the error message from test. One other change to the program is to remove the actual backup command from the if statement and place it on a line by itself and then use test on the exit status to make the decision. Although using if to check the exit status of the backup is legitimate and probably more efficient, the meaning may be unclear to the casual observer.

Consider the traveltime program shown in listing 9.1. Suppose you execute the program with the following command line:

$ traveltime 61 60
The trip will take 1 hours and 1 minutes

Although this answer is correct, it may make your English teacher cringe. You can use numeric testing and if-then-else statements to make the output more palatable. The revised program is shown in listing 9.4.

Listing 9.4. Revised traveltime program.

# traveltime - a program to calculate how long it will
# take to travel a fixed distance
# syntax: traveltime miles mph
X60=´´expr $1 \* 60´´
TOTMINUTES=´´expr $X60 / $2´´
HOURS=´´expr $TOTMINUTES / 60´´
MINUTES=´´expr $TOTMINUTES % 60´´
if [ $HOURS -gt 1 ]
then
   DISPHRS=hours
else
   DISPHRS=hour
fi
if [ $MINUTES -gt 1 ]
then
   DISPMIN=minutes
else
   DISPMIN=minute
fi
echo "The trip will take $HOURS $DISPHRS \c"
if [ $MINUTES -gt 0 ]
then
   echo "and $MINUTES $DISPMIN"
else
   echo
fi

Now traveltime supplies the appropriate singular or plural noun depending on the amount of time:

$ traveltime 50 40
The trip will take 1 hour and 15 minutes
$ traveltime 121 60
The trip will take 2 hours and 1 minute
$ traveltime 120 60
The trip will take 2 hours
$

The Null Command You have now enhanced the unload program to accept the name of a directory from the command line, to check for a valid directory name, and to give the user of the program more information on any errors that may occur. The only real difference between the unload function and the backup function is that unload removes the files from the directory after it has been archived. It would seem that a simple modification to unload--taking out the rm statement--would transform unload to an enhanced version of backup. The only problem is that the rm command is the only command following a then statement, and there must be at least one command following every then statement. The Bourne shell provides a solution with the null command. The null command, represented by a colon (:), is a place holder whose purpose is to fulfill a requirement where a command must appear. To change unload to backup, you replace the rm command with the null command and change some of the messages.


# backup - program to backup all files in a directory
# syntax - backup directory
# check arguments
if [ $# -ne 1 ]
then
   echo "usage: backup directory"
   exit 1
fi
# check for valid directory name
if [ ! -d "$1" ]
then
   echo "$1 is not a directory"
   exit 2
fi
cd $1
ls -a | cpio -o >/dev/rmt0
if [ $? -eq 0 ]
then
   :
else
   echo "A problem has occurred in creating the backup."
   echo "Please check the backup device and try again." 

Displaying the Program Name In the previous two examples, a helpful message was displayed for the user who failed to enter any command-line arguments.

In this message, the name of the program is displayed as part of a literal string. However, if you renamed this program, this message would no longer be valid. In the Bourne shell, the variable $0 always contains the name of the program, as entered on the command line. You can now make the program more general, as in the following example:


if [ $# -ne 1 ]
then
   echo "usage: $0 directory"
   exit 1
fi

Nested if Statements and the elif Construct Often you may want your program to do the following:

  1. 1. Check for a primary condition, and
    A. If the primary condition is true, perform an operation.
    B. If the primary condition is false, check a secondary condition.
    (1) If the secondary condition is true, perform another operation, but
    (2) If the secondary condition is false, check a third condition.
    (a) If the third condition is true, perform another operation.

You can do so by nesting if-else statements, as in the following syntax:

if command
then
   command
else
   if command
   then
      command
   else
      if command
      then
         command
      fi
   fi
fi

Nesting can be useful but can also be confusing, especially knowing where to place the fi statements. Because this kind of programming occurs frequently, the Bourne shell provides a special construct called elif, which stands for else-if and indicates a continuation of the main if statement. You could restate the sequence described above with elif statements, as follows:

if command
then
   command
elif command
then
   command
elif command
then
   command
fi

Either method produces the same results. You should use the one that makes the most sense to you.

Reading Data into a Program Interactively Up to this point, all the input to your programs has been supplied by users in the form of command-line arguments. You can also obtain input for a program by using the read statement. The general syntax of the read statement is as follows:

read var1 var2 ... varn

When the Bourne shell encounters a read statement, the standard input file is read until the shell reads a newline character. When the shell interprets the line of input, it does not make filename and variable substitutions, but it does remove excess white space. After it removes white space, the shell puts the value of the first word into the first variable, and the second word into the second variable, and so on until either the list of variables or the input line is exhausted. If there are more words in the input line than in the variable list, the last variable in the list is assigned the remaining words in the input line. If there are more variables in the list than words in the line, the leftover variables are null. A word is a group of alphanumeric characters surrounded by whitespace.

In the following example, the read statement is looking for three variables. Since the line of input contains three words, each word is assigned to a variable.

$ read var1 var2 var3
Hello      my         friend
$ echo $var1 $var2 $var3
Hello my friend
$ echo $var1
Hello
$ echo $var2
my
$ echo $var3
friend
$

In the next example, the read statement is looking for three variables, but the input line consists of four words. In this case, the last two words are assigned to the third variable.

$ read var1 var2 var3
Hello my dear friend
$ echo $var1
Hello
$ echo $var2
my
$ echo $var3
dear friend
$

Finally, in this example, the input line contains fewer words than the number of variables in the read statement, so the last variable remains null.

$ read var1 var2 var3
Hello friend
$ echo $var1
Hello
$ echo $var2
friend
$ echo $var3

$

Suppose that you want to give the user of the unload program in Listing 9.3 the option to abort. You might insert these lines of code:

...
echo "The following files will be unloaded"
ls -x $1
echo "Do you want to continue: Y or N \c"
read ANSWER
if [ $ANSWER = N -o $ANSWER = n ]
then
   exit 0
fi
...

In the preceding example, you use the \c character in the user prompt so that the user's response appears on the same line as the prompt. The read statement will cause the program to pause until the operator responds with a line of input. The operator's response will be stored in the variable ANSWER. When you're testing the user's response, you use the -o operator so that the appropriate action is taken, regardless of whether the user's response is in upper- or lowercase.

The case Statement Earlier in this section, you saw that the Bourne shell provided a special construct for a common occurrence by providing the elif statement to be used in place of nested if-then-else constructs. Another fairly common occurrence is a series of elif statements where the same variable is tested for many possible conditions, as in the following:

if [ variable1 = value1 ]
then
   command
   command
elif [ variable1 = value2 ]
then
   command
   command
elif [ variable1 = value3 ]
then
   command
   command
fi

The Bourne shell provides a cleaner and more powerful method of handling this situation with the case statement. The case statement is cleaner because it does away with the elifs and the thens. It is more powerful because it allows pattern matching, much as the command-line interpreter does. The case statement allows a value to be named, which is almost always a variable, and a series of patterns to be used to match against the value, and a series of commands to executed if the value matches the pattern. The general syntax of case is as follows:

case value in
   pattern1)
      command
      command;;
   pattern2)
      command
      command;;
   ...
   patternn)
      command;
esac

The case statement executes only one set of commands. If the value matches more than one of the patterns, only the first set of commands specified is executed. The double semicolons (;;) after a command act as the delimiter of the commands to be executed for a particular pattern match.

In the program in listing 9.5, the case statement combines the three sample programs--backup, restore, and unload--into a single interactive program, enabling the user to select the function from a menu.

Listing 9.5. An interactive archive program.

# Interactive program to restore, backup, or unload
# a directory
echo "Welcome to the menu driven Archive program"
echo _
# Read and validate the name of the directory
echo "What directory do you want? \c"
read WORKDIR
if [ ! -d $WORKDIR ]
then
   echo "Sorry, $WORKDIR is not a directory"
   exit 1
fi
# Make the directory the current working directory
cd $WORKDIR
# Display a Menu
echo "Make a Choice from the Menu below"
echo _
echo "1  Restore Archive to $WORKDIR"
echo "2  Backup $WORKDIR "
echo "3  Unload $WORKDIR"
echo
# Read and execute the user's selection
echo "Enter Choice: \c"
read CHOICE
case "$CHOICE" in
   1) echo "Restoring..."
      cpio -i </dev/rmt0;;
   2) echo "Archiving..."
      ls | cpio -o >/dev/rmt0;;
   3) echo "Unloading..."
      ls | cpio -o >/dev/rmt0;;
   *) echo "Sorry, $CHOICE is not a valid choice"
      exit 1
esac
#Check for cpio errors
if [ $? -ne 0 ]
then
   echo "A problem has occurred during the process"
   if [ $CHOICE = 3 ]
   then
      echo "The directory will not be erased"
   fi
   echo "Please check the device and try again"
   exit 2
else
   if [ $CHOICE = 3 ]
   then
      rm *
   fi
fi

In the program in listing 9.5, notice the use of the asterisk (*) to define a default action if all the other patterns in the case statement fail to match. Also notice that the check for errors in the archive process occurs only once in the program. This check can be done in this program because the exit status of the case statement is always the exit status of the last command executed. Because all three cases end with the execution of cpio, and the default case ends with an exit statement, the exit status variable at this point in this program is always the exit status of cpio.

Another powerful capability of the case statement is to allow multiple patterns to be related to the same set of commands. You use a vertical bar (|) as an or symbol in the following form:

pattern1 | pattern2 ) command
                      command;;

You can further modify the interactive archive program to allow the user to make a choice by entering either the menu number or the first letter of the function, by changing the case statement:

read CHOICE
case "$CHOICE" in
   1 | R ) echo "Restoring..."
           cpio -i </dev/rmt0;;
   2 | B ) echo "Archiving..."
           ls | cpio -o >/dev/rmt0;;
   3 | U ) echo "Unloading..."
           ls | cpio -o >/dev/rmt0;;
   *) echo "Sorry, $CHOICE is not a valid choice"
      exit 1
esac

Building Repetitions into a Program

Up to now, the programs you have looked at have had a top-to-bottom, linear progression. The program statements are executed from top to bottom. One of the most beneficial things about computer programs is their capability to process data in volume. For this to occur, the programming language must have some construct to cause portions of the program to be repetitive. In computer terminology, this construct is often called looping.

For example, suppose you had a computer file containing records with mailing addresses and ZIP codes and you wanted to print only records matching a specific ZIP code. You would want to write a program which reads a record, performs a matching test on the ZIP code, prints those that match, and then repeat the process until the data is exhausted. You could do this within a loop.

The Bourne shell has three different looping constructs built into the language. One of the key concepts in program looping is the termination of the loop. Many hours of computer time are wasted by programs that inadvertently go into infinite loops. The main difference between the shell's three looping constructs is the method by which the loop is terminated. The three types of loops are the while loop, the until loop, and the for loop; each is discussed separately in the following sections.

Repeating Within a while Loop The while construct enables you to specify commands that will be executed while some condition is true.

The general format of the while construct is as follows:

while command
do
   command
   command
   ...
   command
done

Consider the following example in a program called squares in listing 9.6.

Listing 9.6. Example of a while loop.

# squares - prints the square of integers in succession
int=1
while [ $int -lt 5 ]
do
   sq='expr $int \* $int'
   echo $sq
   int='expr $int + 1'
done
echo "Job Complete"

$ squares
1
4
9
16
Job Complete
$

In the program in listing 9.6, as long as the value of int is less than five, the commands inside the loop are executed. On the fifth repetition, the test condition associated with the while statement returns a nonzero value, and the command following the done statement is executed.

In the interactive archive program in Listing 9.5, the user is allowed to make a single request and the program terminates. Using while, you can change the program to allow the user to enter multiple requests. The revised program is shown in listing 9.7.

Listing 9.7. Revised interactive archive program.

# Interactive program to restore, backup, or unload
# a directory
echo "Welcome to the menu driven Archive program"
ANSWER=Y
while [ $ANSWER = Y -o $ANSWER = y ]
do
   echo _
# Read and validate the name of the directory
   echo "What directory do you want? \c"
   read WORKDIR
   if [ ! -d $WORKDIR ]
   then
      echo "Sorry, $WORKDIR is not a directory"
      exit 1
   fi
# Make the directory the current working directory
   cd $WORKDIR
# Display a Menu
   echo "Make a Choice from the Menu below"
   echo _
   echo "1  Restore Archive to $WORKDIR"
   echo "2  Backup $WORKDIR "
   echo "3  Unload $WORKDIR"
   echo
# Read and execute the user's selection
   echo "Enter Choice: \c"
   read CHOICE
   case "$CHOICE" in
      1) echo "Restoring..."
         cpio -i </dev/rmt0;;
      2) echo "Archiving..."
         ls | cpio -o >/dev/rmt0;;
      3) echo "Unloading..."
         ls | cpio -o >/dev/rmt0;;
      *) echo "Sorry, $CHOICE is not a valid choice"
   esac
#Check for cpio errors
   if [ $? -ne 0 ]
   then
      echo "A problem has occurred during the process"
      if [ $CHOICE = 3 ]
      then
         echo "The directory will not be erased"
      fi
      echo "Please check the device and try again"
      exit 2
   else
      if [ $CHOICE = 3 ]
      then
         rm *
      fi
   fi
   echo "Do you want to make another choice? \c"
   read ANSWER
done

By initializing the ANSWER variable to Y, enclosing the main part of the program within a while loop, and getting a new ANSWER at then end of the loop in the program in listing 9.7, the user is able to stay in this program until he or she answers N to the question.

Repeating Within an until Loop The while construct causes the program to loop as long as some condition is true. The until construct is the complement to while; it causes the program to loop until a condition is true. These two constructs are so similar, you can usually use either one. Use the one that makes the most sense in the context of the program you are writing.

The general format of the until construct is as follows:

until command
do
   command
   command
   ...
   command
done

You could have made the modification to the interactive archive program just as easily with an until loop by replacing the while with until:

until [ $ANSWER = N -o $ANSWER = n ]

Processing an Arbitrary Number of Parameters with shift Before considering the for loop, it would be helpful to look at the shift command, since the for loop is really a shorthand use of shift.

In the examples presented so far, the number of positional parameters, or command-line arguments, is either presumed to be solitary or is passed on to a command as a whole using the $* variable. If a program needs to process each of the command-line arguments individually, and the number of arguments is not known, you can process the arguments one by one by using the shift command in your program. The shift command shifts the position of positional parameters by one; $2 becomes $1, $3 becomes $2, and so on. The parameter that was $1 before the shift command is not available after shift. The following simple program illustrates this concept:

# shifter
until [ $# -eq 0 ]
do
   echo "Argument is $1 and 'expr $# - 1' argument(s) remain"
   shift
done

$ shifter 1 2 3 4
Argument is 1 and 3 argument(s) remain
Argument is 2 and 2 argument(s) remain
Argument is 3 and 1 argument(s) remain
Argument is 4 and 0 argument(s) remain
$

You may have noticed that the $# variable decremented each time the shift command was executed in the preceding example. Using this knowledge, you can use an until loop to process all the variables. Consider the example in listing 9.8, a program to sum an integer list supplied as command-line arguments.

Listing 9.8. An integer summing program.

# sumints - a program to sum a series of integers
#
if [ $# -eq 0 ]
then
   echo "Usage: sumints integer list"
   exit 1
fi
sum=0
until [ $# -eq 0 ]
do
   sum=´´expr $sum + $1´´
   shift
done
echo $sum

Following is the execution of sumints:

$ sumints 12 18 6 21
57
$

You also can use the shift command for another purpose. The Bourne shell predefines nine positional parameters, $1 through $9. This does not mean that only nine positional parameters can be entered on the command line, but to access positional parameters beyond the first nine, you must use the shift command.

The shift command can take an integer argument that causes it to shift more than one position at a time. If you know that you have processed the first three positional parameters, for example, and you want to begin a loop to process the remaining arguments, you can make $4 shift to $1 with the following command:

shift 3.

Repeating Within a for Loop The third type of looping construct in the Bourne shell is the for loop. The for loop differs from the other constructs in that it is not based on a condition being true or false. Instead the for loop executes one time for each word in the argument list it has been supplied. For each iteration of the loop, a variable name supplied on the for command line assumes the value of the next word in the argument list. The general syntax of the for loop is as follows:

for variable in arg1 arg2  ... argn
do
   command
   ...
   command
done

The following simple example illustrates the construct:

$ for LETTER in a b c d; do echo $LETTER; done
a
b
c
d
$

Because the argument list contained four words, the loop is executed exactly four times. The argument list in the for command does not have to be a literal constant; it can be from a variable substitution.

You can also write the sumints program in listing 9.8 using a for loop, by passing the command-line arguments to the for loop. The modified program appears in listing 9.9.

Listing 9.9. Modified integer summing program.

# sumints - a program to sum a series of integers
#
if [ $# -eq 0 ]
then
   echo "Usage: sumints integer list"
   exit 1
fi
sum=0
for INT in $*
do
   sum=´´expr $sum + $INT´´
done
echo $sum

Getting Out of a Loop from the Middle Normally, a looping construct executes all the commands between the do statement and the done statement. Two commands enable you to get around this limitation: the break command causes the program to exit the loop immediately, and the continue command causes the program to skip the remaining commands in the loop but remain in the loop.

A technique that is sometimes used in shell programming is to start an infinite loop, that is, a loop that will not end until either a break or continue command is executed. An infinite loop is usually started with either a true or false command. The true command always returns an exit status of zero, whereas the false command always returns a nonzero exit status. The loop

while true
do
   command
   ...
   command
done

executes until either your program does a break or the user initiates an interrupt. You can also write an infinite loop as follows:

until false
do
   command
   ...
   command
done

We could use this technique to make the interactive archive program of Listing 9.7 a little easier to use. The revised program is shown in listing 9.10.

Listing 9.10. Another version of the interactive archiver.

# Interactive program to restore, backup, or unload
# a directory
echo "Welcome to the menu driven Archive program"
while true
do
# Display a Menu
   echo
   echo "Make a Choice from the Menu below"
   echo _
   echo "1  Restore Archive"
   echo "2  Backup directory"
   echo "3  Unload directory"
   echo "4  Quit"
   echo
# Read the user's selection
   echo "Enter Choice: \c"
   read CHOICE
   case $CHOICE in
      [1-3] ) echo _
              # Read and validate the name of the directory
              echo "What directory do you want? \c"
              read WORKDIR
              if [ ! -d "$WORKDIR" ]
              then
                 echo "Sorry, $WORKDIR is not a directory"
              continue
              fi
              # Make the directory the current working directory
              cd $WORKDIR;;
           4) :;;
           *) echo "Sorry, $CHOICE is not a valid choice"
              continue _
   esac
   case "$CHOICE" in
      1) echo "Restoring..."
         cpio -i </dev/rmt0;;
      2) echo "Archiving..."
         ls | cpio -o >/dev/rmt0;;
      3) echo "Unloading..."
         ls | cpio -o >/dev/rmt0;;
      4) echo "Quitting"
         break;;
   esac
#Check for cpio errors
   if [ $? -ne 0 ]
   then
      echo "A problem has occurred during the process"
      if [ $CHOICE = 3 ]
      then
         echo "The directory will not be erased"
      fi
      echo "Please check the device and try again"
      continue
   else
      if [ $CHOICE = 3 ]
      then
         rm *
      fi
   fi
done

In the program in listing 9.1, the loop continues as long as true returns a zero exit status, which is always, or until the user makes selection four, which executes the break command and terminates the loop. Notice also, that if the user makes an error in choosing the selection or in entering the directory name, the continue statement is executed rather than the exit statement. This way, the user can stay in the program even if he or she makes a mistake in entering data, but the mistaken data cannot be acted on.

Notice also the use of two case statements. The first case statement requests that the operator enter a directory name only if option 1, 2, or 3 is selected. This example illustrates how pattern matching in a case statement is similar to that on a command line. In the first case statement, if the user selects option 4, the null command (:) is executed. Because the first case statement checks for invalid selections and executes a continue if an invalid selection is made, the second case statement need not check for any but valid selections.

Structured Shell Programming Using Functions A common feature among higher level programming languages is the ability to group computer instructions together into functions that can be called from anywhere within the program. These functions are sometimes called subroutines. The Bourne shell also provides you this ability.

The general syntax of a function definition is as follows:

funcname ()
{
   command
   ...   _
   command;
}

Once it is defined, a function can be called from anywhere within the shell by using funcname as a command. There are two reasons you might want to group commands into a function. One good reason is to break a complex program into more manageable segments, creating a structured program. A structured program might take the following form:

# start program
setup ()
{  command list ; }_

do_data ()
{  command list ; }_

cleanup ()
{  command list ; }_

errors ()
{  command list ; }_

setup
do_data
cleanup
# end program

In the above example, setup, do_data, and cleanup are functions. When you look at a well-structured program, the names of the functions give you a fair idea of what the functions might do. If you were trying to analyze this, you might assume what the setup and cleanup functions do and concentrate on the do_data section.


TIP: Always give variables and functions meaningful names. It may seem at the time you are writing a program that you will remember what the variables and functions are used for, but experience has proven that after the passage of time things are not always so clear. You should also remember that there will probably come a time when someone else will look at your programs, and that person will appreciate descriptive names.

Another legitimate reason for grouping commands into functions is that you may want to execute the same sequence of commands from several points within a program. At several points in the interactive archive program in listing 9.10, a non-fatal error occurs and the continue command is executed. You can give the user the option of continuing at each of these points with an interactive continue function named icontinue.

icontinue ()
{
while true
do
   echo "Continue? (y/n) \c"
   read ANSWER
   case $ANSWER in
      [Yy] ) return 0;;
      [Nn] ) return 1;;
      * ) echo "Answer y or n";;
   esac
done
}

Now you can replace the continue statements in the program with the icontinue function.

if icontinue then continue else break fi

All of the prompting, reading, and error checking are carried out by the icontinue function, instead of repeating these commands at every continue point. This example also illustrates the function's capability to return an exit status with return. If no return command is available within the function, the exit status of the function is the exit status of the last command in the function.

Shell functions are very much like shell programs--with one very important difference. Shell programs are executed by subshells, whereas shell functions are executed as part of the current shell. Therefore, functions can change variables that are seen in the current shell. Functions can be defined in any shell, including the interactive shell.

$ dir () { ls -l; }_
$ dir
-rw-rw-r--   1 marsha   adept      1024 Jan 20 14:14 LINES.dat
-rw-rw-r--   1 marsha   adept      3072 Jan 20 14:14 LINES.idx
-rw-rw-r--   1 marsha   adept       256 Jan 20 14:14 PAGES.dat
-rw-rw-r--   1 marsha   adept      3072 Jan 20 14:14 PAGES.idx
-rw-rw-r--   1 marsha   acct        240 May  5  1992 acct.pds
$

You have now defined dir as a function within your interactive shell. It remains defined until you log off or unset the function, as follows:

$ unset dir

Functions can also receive positional parameters, as in the following example:

$ dir () {_
>  echo "Permission  Ln Owner    Group   File Sz Last Access"
>  echo "----------  -- ----    ----   ------ ----------"
>  ls -l $*;
>}
$ dir L*
Permission  Ln Owner    Group   File Sz Last Access
----------  -- ----    ----   ------ ----------_
-rw-rw-r--   1 marsha   adept      1024 Jan 20 14:14 LINES.dat
-rw-rw-r--   1 marsha   adept      3072 Jan 20 14:14 LINES.idx

In this example, the argument L* was passed to the dir function and replaced in the ls command for $*.

Normally, a shell script is executed in a subshell. Any changes made to variables in the subshell are not made in the parent shell. The dot (.) command causes the shell to read and execute a shell script within the current shell. You make any function definitions or variable assignments in the current shell. A common use of the dot command is to reinitialize login values by rereading the .profile file. For information about .profile, see "Customizing the Shell" later in this chapter.

$ . .profile

Handling the Unexpected with trap When you're writing programs, one thing to keep in mind is that programs do not run in a vacuum. Many things can happen during a program that are not under the control of the program. The user of the program may press the interrupt key or send a kill command to the process, or the controlling terminal may become disconnected from the system. In UNIX, any of these events can cause a signal to be sent to the process. The default action when a process receives a signal is to terminate.

Sometimes, however, you may want to take some special action when a signal is received. If a program is creating temporary data files, and it is terminated by a signal, the temporary data files remain. In the Bourne shell, you can change the default action of your program when a signal is received by using the trap command.

The general format of the trap command is as follows:

trap command_string signals

On most systems, you can trap 15 signals. The default action for most is to terminate the program, but this action can vary, so check your system documentation to see what signals can occur on your system (Part IV, "Process Control" discusses signals in more detail). Any signal except 9 (known as the sure kill signal) can be trapped, but usually you are concerned only with the signals that can occur because of the user's actions. Following are the three most common signals you'll want to trap:
Signal Description
1 Hangup
2 Operator Interrupt
15 Software Termination (kill signal)

If the command string contains more than one command, which it most certainly should, you must enclose the string in either single or double quotation marks. The type of quotation marks you use determines when variable substitution is made.

Suppose you have a program that creates some temporary files. When the program ends normally, the temporary files are removed, but receiving a signal causes the program to terminate immediately, which may leave the temporary files on the disk. By using the trap command in the following example, you can cause the temporary files to be removed even if the program does not terminate normally due to receiving a hangup, interrupt, or kill signal:

trap "rm $TEMPDIR/*$$; exit" 1 2 15

When the trap command is executed, the command string is stored as an entry in a table. From that point on, unless the trap is reset or changed, if the signal is detected, the command string is interpreted and executed. If the signal occurs in the program before the trap command is executed, the default action occurs. It is important to remember that the shell reads the command string twice--once when the trap is set and again when the signal is detected. This determines the distinction between the single and double quotation marks. In the preceding example, when the trap command line is read by the interpreter, variable substitution takes place for $TEMPDIR and $$. After the substitution, the resultant command string is stored in the trap table. If the trap command is changed to use single quotation marks

trap 'rm $TEMPDIR/*$$; exit' 1 2 15

when trap is executed, no variable substitution take place, and the command string

rm $TEMPDIR/*$$; exit

is placed in the trap table. When the signal is detected, the command string in the table is interpreted, and then the variable substitution takes place. In the first instance, $TEMPDIR and $$ have the value that they had at the time the trap was executed. In the second instance, $TEMPDIR and $$ assume the value that they have at the time the signal is detected. Make sure that you know which you want.

The command string for the trap command almost always contains an exit statement. If you don't include an exit statement, then the rm command is executed when the signal is detected, and the program picks right up where it left off when the signal occurred. Sometimes you might want the program to pick up where it left off instead of exiting. For example, if you don't want your program to stop when the terminal is disconnected, you can trap the hangup signal, specifying the null command, as shown in the following example:

trap : 1

You can set a trap back to the default by executing the trap command with no command string, like this:

trap 1

The following command has the effect of making the user press the interrupt key twice to terminate a program:

trap 'trap 2' 2

Conditional Command Execution with the And/Or Constructs

As you have already seen, often you can write a shell program more than one way without changing the results of the program. The until statement, for example, is simply a reverse way of using a while statement. You can cause commands to be conditionally executed using the if-then-else construct, but you also can accomplish conditional execution using the && and || operators. In the C programming language, these symbols represent the logical and and the logical or operations respectively. In the Bourne shell, the && connects two commands in such a way that the second command is executed only if the first command is successful.

The general format of && is as follows:

command && command

For example, in the statement

rm $TEMPDIR/* && echo "Files successfully removed"

the echo command is executed only if the rm command is successful. You also can do this programming in an if-then statement like this one:

if rm $TEMPDIR/*
then
   echo "Files successfully removed"
fi

Conversely, the || connects to commands in such a way that the second command is executed only if the first command is not successful, as in this command:

rm $TEMPDIR/* || echo "Files were not removed"

The preceding is the programming equivalent of

if rm $TEMPDIR/*
then
   :
else
   echo "Files were not removed"
fi

You also can concatenate these operators. In the following command line, command3 is executed only if both command1 and command2 are successful:

command1 && command2 && command3

You can also concatenate operators of different types. In the following command line, command3 is executed only if command1 is successful and command2 is unsuccessful:

command1 && command2 || command3

The && and || are simple forms of conditional command execution and are usually used only in cases where single commands are to be executed. Although the commands can be compound, if too many commands appear in this format, the program can be difficult to read. Generally, if-then constructs seem to be more clear if you use more than one or two commands.

Reading UNIX-Style Options One of the nicer things about UNIX is that most of the standard commands have a similar command-line format:

command -options parameters

If you are writing shell programs for use by other people, it is nice if you use the same conventions. To help you do so, a special command is available in the Bourne shell for reading and processing options in this format: the getopts command, which has the following form:

getopts option_string variable

where option_string contains the valid single-character options. If getopts sees the hyphen (-) in the command input stream, it compares the character following the hyphen with the characters in option_string. If a match occurs, getopts sets variable to the option; if the character following the hyphen does not match one of the characters in option_string, variable is set to a question mark (?). If getopts sees no more characters following a hyphen, it returns a nonzero exit status. This capability enables you to use getopts in a loop.

The program in listing 9.11 illustrates how you use getups to handle options for the date command. The program creates a version of date, which conforms to standard UNIX style, and it adds some options.

Listing 9.11. A standardized date function newdate.

#newdate
if [ $# -lt 1 ]
then
   date
else
   while getopts mdyDHMSTjJwahr OPTION
   do
      case $OPTION
      in
         m) date '+%m ';;  # Month of Year
         d) date '+%d ';;  # Day of Month
         y) date '+%y ';;  # Year
         D) date '+%D ';;  # MM/DD/YY
         H) date '+%H ';;  # Hour
         M) date '+%M ';;  # Minute
         S) date '+%S ';;  # Second
         T) date '+%T ';;  # HH:MM:SS
         j) date '+%j ';;  # day of year
         J) date '+%y%j ';;# 5 digit Julian date
         w) date '+%w ';;  # Day of the Week
         a) date '+%a ';;  # Day abbreviation
         h) date '+%h ';;  # Month abbreviation
         r) date '+%r ';;  # AM-PM time
         \?) echo "Invalid option $OPTION";;
      esac
   done
fi

In the program in listing 9.11, each option is processed in turn. When getopts has processed all the options, it returns a nonzero exit status, and the while loop terminates. Notice that getopts allows options to be stacked behind a single hyphen, which is also a common UNIX form.

The following examples illustrate how newdate works:

$ newdate -J
94031
$ newdate -a -h -d
Mon
Jan
31
$ newdate -ahd
Mon
Jan
31
$

Sometimes an option requires an argument, which getopts also parses if you follow the option letter in option_string with a colon. When getopts sees the colon, it looks for a value following a space following the option flag. If the value is present, getopts stores the value in a special variable OPTARG. If it can find no value where one is expected, getopts stores a question mark in OPTARG and writes a message to standard error.

The program in listing 9.12 makes copies of a file and gives the copies a new name. The -c option takes an argument specifying the number of copies to make, and the -v option instructs the program to be verbose, that is to display the names of the new files as they are created.

Listing 9.12. duplicate program.

# Syntax: duplicate [-c integer] [-v] filename
#    where integer is the number of duplicate copies
#    and -v is the verbose option
COPIES=1
VERBOSE=N

while getopts vc: OPTION
do
   case $OPTION
   in
      c) COPIES=$OPTARG;;
      v) VERBOSE=Y;;
      \?) echo "Illegal Option"
          exit 1;;
   esac
done

if [ $OPTIND -gt $# ]
then
   echo "No file name specified"
   exit 2
fi

shift ´´expr $OPTIND -1´´'

FILE=$1
COPY=0

while [ $COPIES -gt $COPY ]
do
   COPY=´´expr $COPY + 1´´
   cp $FILE ${FILE}${COPY}
   if [ VERBOSE = Y ]
   then
      echo ${FILE}${COPY}
   fi
done

In the program in listing 9.12, allowing the user to enter options presents a unique problem; when you write the program, you don't know which of the positional parameters will contain the name of the file that is to be copied. The getopts command helps out by storing the number of the next positional parameter in the variable OPTIND. In the duplicate program, after getopts has located all the options, OPTIND is checked to make sure that a filename is specified and then the shift command makes the filename the first positional parameter.

$ duplicate -v fileA
fileA1
$ duplicate -c 3 -v fileB
fileB1
fileB2
fileB3

Customizing the Shell

The shell performs some very specific tasks and expects its input to follow some specific guidelines--command names first, for instance. But the Bourne shell does allow the user some control over his or her own environment. You can change the look of your shell and even add your own commands.

Customizing the Shell with Environment Variables

In the section "Variables" earlier in this chapter, you learned that one type of variable is called an environment variable. The shell refers to these variables when processing information. Changing the value of an environment variable changes how the shell operates. You can change your command-line prompt, get mail forwarded to you, and even change the way the shell looks at your input.

Adding Command-Line Separators with IFS When a command line is entered in an interactive shell, each word on the command line is interpreted by the shell to see what action needs to be taken. By default, words are separated by spaces, tabs, and newline characters. You can add your own separators by changing the IFS environment variable, as in the following example:

$ IFS=':'
$ echo:Hello:My:Friend
Hello My Friend
$

Setting additional field separators does not void the default field separators; space, tab, and newline are always seen as field separators.

Checking Multiple Mailboxes with MAILPATH Most users have only one mailbox for their electronic mail. Some users, however, may require multiple mailboxes (see Chapter 7, "Communicating with Others" for a discussion of electronic mail). For example, Dave wants to read mail addressed to him personally (which arrives to his personal user account), mail addressed to sysadm (which arrives to his system administrator account), and mail addressed to root (which arrives to his main account), but Dave can be logged in as only one of these accounts at any one time. Dave therefore can cause his current shell to check all three mailboxes by setting the environment variable MAILPATH, as follows:

$ MAILPATH="/usr/spool/mail/Dave:/usr/spool/mail/sysadm\
:/usr/spool/mail/root"

Now when mail is sent to any of these names, Dave receives the following message:

you have mail.

The only problem is that Dave does not know which mailbox to check when he receives this message. You can help solve Dave's problem by changing the mail message associated with each mailbox. You terminate the mailbox name in MAILPATH with a percent sign (%) and supply a message like this:

$ MAILPATH="/usr/spool/mail/Dave%Dave has mail\
:/usr/spool/mail/sysadm%sysadm has mail\
:/usr/spool/mail/root%root has mail

Adding Your Own Commands and Functions

This chapter has shown how you can group UNIX commands together in files and create your own programs or shell scripts. Sometimes though, you don't achieve the desired results. The program in listing 9.13 changes the working directory, and at the same time changes the environment variable PS1, which contains the command-line prompt.

Listing 9.13. Change directory program chdir.

# Directory and Prompt Change Program
# Syntax: chdir directory

if [ ! -d "$1" ]
then
  echo "$1 is not a directory"
  exit 1
fi

cd $1
PS1="´´pwd´´> "
export PS1

When you execute the following chdir command from listing 9.13, nothing happens.

$ chdir /usr/home/teresa
$

There is no error message, yet the command-line prompt is not changed. The problem is that chdir is executed in a subshell, and the variable PS1 that was exported is made available only to lower shells. To make chdir work like you want, it must be executed within the current shell. The best way to do that is to make it a function. You can write the function in your .profile file, but there is a better solution. Group your personal functions into a single file and load them into your current shell using the transfer command (.). Rewrite chdir as a function, changing the exit to return. The function definition file persfuncs is shown in listing 9.14.

Listing 9.14. Personal function file with chdir written as a function.

#Personal function file persfuncs
chdir ()
{
# Directory and Prompt Change Program
# Syntax: chdir directory

if [ ! -d "$1" ]
then
  echo "$1 is not a directory"
  return
fi

cd $1
PS1="´´pwd´´> "
export PS1;
}
$ . persfuncs
$ chdir /usr/home/teresa
/usr/home/teresa> chdir /usr/home/john
/usr/home/john> _

Keeping personal functions in a separate file makes them easier to maintain and debug than keeping them in your .profile.

You can make your personal functions a permanent part of your environment by putting the command

.persfuncs

in your .profile.

Specialized Topics

There are many topics from a programming standpoint that pertain to the shell. Of particular importance are debugging, command grouping, and program layering. These topics are discussed in the following sections.

Debugging Shell Programs

When you begin to write shell programs, you will realize something that computer users have known for years: programmers make mistakes! Sometimes what seems to be a perfectly reasonable use of computer language produces results that are unexpected. At those times, it is helpful to have some method of tracking down your errors.

The Bourne shell contains a trace option, which causes each command to be printed as it is executed, along with the actual value of the parameters it receives. You initiate the trace option by using set to turn on the -x option or execute a shell with the -x option. The sumints program is reproduced in listing 9.15.

Listing 9.15. An integer summing program.

# sumints - a program to sum a series of integers
#
if [ $# -eq 0 ]
then
   echo "Usage: sumints integer list"
   exit 1
fi
sum=0
until [ $# -eq 0 ]
do
   sum='expr $sum + $1'
   shift
done
echo $sum

Running sumints with the trace option looks like this:


$ sh -x sumints 2 3 4
+ [ 3 -eq 0 ]
+ sum=0
+ [ 3 -eq 0 ]
+ expr 0 + 2
+ sum= 2
+ shift
+ [ 2 -eq 0 ]
+ expr 2 + 3
+ sum= 5
+ shift
+ [ 1 -eq 0 ]
+ expr 5 + 4
+ sum= 9
+ [ 0 -eq 0 ]
+ echo 9
9
$

The trace shows you each command that executes and the value of any substitutions that were made before the command was executed. Notice that the control words if, then, and until were not printed.

Grouping Commands

Commands to a shell can be grouped to be executed as a unit. If you enclose the commands in parentheses, the commands are run in a subshell; if you group them in curly braces ({}), they are run in the current shell. The difference in the two has to do with the effect on shell variables. Commands run in a subshell do not affect the variables in the current shell, but if commands are grouped and run in the current shell, any changes made to variables in the group are made to variables in the current shell.

$ NUMBER=2
$ (A=2; B=2; NUMBER='expr $A + $B'; echo $NUMBER)
4
$ echo $NUMBER
2

In the previous example, note that the variable NUMBER had a value of 2 before the command group was executed. When the command group was run inside of parentheses, NUMBER was assigned a value of 4, but after execution of the command group was complete, NUMBER had returned to its original value. In this next example, when the commands are grouped inside of curly braces, NUMBER will keep the value it was assigned during execution of the command group.

$ {A=2; B=2; NUMBER='expr $A + $B'; echo $NUMBER}
4
$ echo $NUMBER
4
$

Note that the second example looks somewhat like a function definition. A function is a named group of commands, which executes in the current shell.

Using the Shell Layer Manager shl

UNIX is a multi-programming operating system. Some UNIX systems take advantage of this feature, allowing the user to open several shells at one time, which they can accomplish using the shell layer manager shl. Only the active layer can get terminal input, but output from all layers is displayed on the terminal, no matter which layer is active, unless layer output is blocked.

A layer is created and named with shl. While the user is working in a layer, he or she can activate the shell manager by using a special character (Ctrl+Z on some systems). The shell layer manager has a special command-line prompt (>>>) to distinguish it from the layers. While in the shell layer manager, the user can create, activate, and remove layers. Following are the shl commands:
create name Creates a layer called name
delete name Removes the layer called name
block name Blocks output from name
unblock name Removes the output block for name
resume name Makes name the active layer
toggle Resumes the most recent layer
name Makes name the active layer
layers [-l] name ... For each name in the list, displays the process ID. The -l option produces more detail.
help Displays help on the shl commands
quit Exits shl and all active layers

Summary

In this chapter you have learned the fundamental aspects of the Bourne shell, using shell variables, and the basics of shell scripting. What you have learned here will be useful in the other shell chapters, such as Chapter 11, "The Korn Shell" and Chapter 12, "The C Shell," as well as other scripting languages. Most operating systems have a variety of available languages and some sort of system command scripting capability. However, few have the system command script language abilities of the UNIX shell. Writing shell programs can save countless hours of coding using tradition languages like C, COBOL, BASIC, etc. Becoming experienced with UNIX shell scripts is a valuable asset for any systems administrator or programmer. Pursuing additional learning and experience in shell scripting is highly recommended.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.