10.2 The Bourne Shell

The Bourne shell serves a twofold purpose, as most shells do. It can be used as a command line interpreter, just like bash (Bourne-again shell) and tcsh , and it can be used to write simple programs. Of course, bash and tcsh can also be used for simple programming, but the Bourne shell is the preferred language. Though csh and tcsh were designed with a syntax similar to that in the C programming language, certain idiosyncrasies can lead to problems with even moderately interesting scripts. The Bourne shell and bash are closely related , and most programs written for one will work with the other. On many Linux systems, the Bourne shell program is really just a symlink to the bash binary anyway. Regardless, if you write a program with the Bourne shell in mind, it will work even on systems that do not have the bash program.

10.2.1 Basics of the Bourne Shell

Fundamentally, a Bourne shell script is a list of commands just as you would type on the command line. Unless the command is a reserved keyword, the shell uses the PATH environment variable to locate the command and then it is executed. For example the script:

 
 #!/bin/sh    hostname    date    uptime    who 

would run the commands hostname , date , uptime , and who , printing the output of each to the screen. If a command is not in your path, you can either specify an explicit path or modify the PATH environment variable. Thus:

 
 #!/bin/sh    /usr/local/bin/myprogram 

and

 
 #!/bin/sh    PATH=${PATH}:/usr/local/bin; export PATH    myprogram 

are both acceptable solutions.

Multiple commands can be listed on a single line if they are separated by semicolons. The preceding example could have been written as:

 
 #!/bin/sh    hostname; date; uptime; who 

10.2.2 Using Variables

Here is an example of setting and using a variable in the Bourne shell:

 
 #!/bin/sh    a=foobar    echo $a 

Notice that when a variable is set, no dollar sign is used, but when the variable is referenced, the dollar sign is used.

Because of the way quoting works in the Bourne shell, you will need to use quotation marks if you want to set a variable to a string with spaces in it:

 
 #!/bin/sh    a="This is a test"    echo $a 

But note that even though the variable a contains all four words and the spaces between them, when the variable is given to the echo command, each word is given as a separate argument. That is, the echo command is called here with four arguments. If instead you need to pass an argument containing spaces as a single argument to a command, you must use quotation marks when you reference the variable as well as when you set it. For example, if you want to use the grep command to search for the text "network administration," including the space, in the file book.tex it would look like:

 
 #!/bin/sh    a="network administration"    grep "$a" book.tex 

Here, only two arguments are passed to grep : first the search text and then the file name .

10.2.3 Local and Environment Variables

The Bourne shell has a slightly odd way of dealing with environment variables. The variable a , set and referenced above, is a local variable. Environment variables are accessed exactly the same way local variables are, except that they have the property of already being set for you. So:

 
 #!/bin/sh    echo $EDITOR 

prints the value of the EDITOR environment variable. Notice the convention that environment variable names are in uppercase while local variable names are in lowercase. This is not enforced but it is highly recommended so that you do not confuse others who read the program later. How did you know the EDITOR variable already had a value by virtue of its being in the environment? Only by convention. It is possible to figure out what variables have values set from the environment, but it is not usually necessary or worthwhile.

So does setting the value of an environment variable work just as above? The answer is no. The EDITOR variable as you accessed it is not really an environment variable at all. If you set its value with

 
 EDITOR=vi 

it will change the value of $EDITOR, but it is really only a local variable. This can be demonstrated with this script:

 
 #!/bin/sh    echo $EDITOR    EDITOR=vi    /usr/bin/env  grep EDITOR 

When run, it produces:

 
 Solaris% ./testscript.sh    emacs    EDITOR=emacs 

The value of the EDITOR environment variable is emacs even though you changed the value of the local variable to "vi." The reason for this is that when the Bourne shell starts up, every variable is a local variable, and it places a copy of all environment variable values into a local variable of the same name.

You can declare a variable to be an environment variable with the export command. Once you do that, that variable is bound to the environment, meaning that any value you set for it will automatically be reflected in the environment and thus passed on to other programs. If we modify the preceding example by adding an export command:

 
 #!/bin/sh    echo $EDITOR    EDITOR=vi    export EDITOR    /usr/bin/env  grep EDITOR 

the output now becomes:

 
 Solaris% ./testscript.sh    emacs    EDITOR=vi 

Often in scripts you will see the export command placed on the same line as the variable is set, either with a semicolon:

 
 EDITOR=vi; export EDITOR 

or without:

 
 EDITOR=vi export EDITOR 

Both are valid Bourne shell syntax.

10.2.4 Exit Status

After a program is finished running, it returns an integer as its exit status . Often a program returns an exit status of zero if it exits normally and an exit status of one if it does not. This is not always the case, though; check the documentation for the program in question to find out its behavior.

In the Bourne shell, the return value of the last program run is stored in the variable $? . For example, this short script uses the $? variable to print the exit status from the touch program. The touch program tries to create a file if it does not exist; if it does already exist, it updates the modification time:

 
 #!/bin/sh    touch /tmp/mytestfile    echo $?    touch /foobar    echo $? 

When you run it, the result is:

 
 Solaris% ./testscript.sh    0    touch: /foobar cannot create    1 

The first use of the touch program exits normally with exit status zero. In the second instance, touch is not able to create the file /foobar , so a warning is printed and the exit status is set to one.

10.2.5 Conditionals

This script demonstrates the use of conditionals in the Bourne shell:

 
 #!/bin/sh    a="green"    if [ "$a" = "red" ]; then      echo "Found red"    elif [ "$a" = "green" ]; then      echo "Found green"    else      echo "Found a color other than red or green"    fi 

Note that some of the spacing is very important. There must be a space after the open bracket, before the closed bracket , and both before and after the equal signs. Placing quotes around $a , while not strictly necessary in this example, is a good habit to develop. If the value of $a had been empty ( "" ), the quotes would be required.

The if statement begins the set of conditionals. The elif command stands for "else if." The else statement catches any conditions not already met, and finally the fi ("if" backwards ) command closes the set of conditionals.

There are a number of interesting details to be studied in how these statements are constructed and in shortcuts that can be taken. But the format above is clear and simple and will work wherever you need a conditional. However, it is worth mentioning that the brackets are not really Bourne shell syntax. The open bracket ( [ ) is actually a separate program, usually a symlink to the test program:

 
 Linux% ls -l /usr/bin/[    lrwxrwxrwx    1 root     root   4 Jul  1  2002 /usr/bin/[ -> test* 

In the previous example, you compared strings to see if they were equal. Other tests are also possible, such as comparing numeric values:

 
 #!/bin/sh    a=75    if [ $a -eq 0 ]; then      echo "Zero"    elif [ $a -le 50 ]; then      echo "Less than or equal to 50"    elif [ $a -lt 80 ]; then      echo "Less than 80"    elif [ $a -ge 80 ]; then      echo "Greater than or equal to 80"    fi 

Also useful is the ability to test files. For example, you can use -r to test if a file is readable:

 
 #!/bin/sh    a=/var/tmp/myfile    if [ ! -r "$a" ]; then      echo "Warning: $a is not readable"    fi 

The exclamation mark negates the test, which is to say that it will succeed when the opposite condition is true. Using -r alone will succeed when the file is readable; using ! -r will succeed when the file is not readable.

The and and or operators are -a and -o , respectively:

 
 #!/bin/sh    a=1    b=1    if [ $a -eq 1 -a $b -eq 1 ]; then      echo "Both"    elif [ $a -eq 1 -o $b -eq 1 ]; then      echo "One"    else      echo "Neither"    fi 

The full list of tests is available on the test man page (type man test at the prompt). On some systems, this man page lists tests for a number of different shells, so make sure to look at the ones for /bin/sh , or if that is not present, the tests for the bash shell.

Do note that conditionals are particularly useful in conjunction with the status variable:

 
 #!/bin/sh    touch /foobar    if [ $? -ne 0 ]; then      echo "Warning: touch failed"    fi 

10.2.6 Arguments

Often you will want use arguments from the command line as variables in your script. Say you have a script that tests a server for a security vulnerability. You would like to be able to invoke it as:

 
 Solaris% ./scanhost myserver.example.com 

In the Bourne shell, the arguments are stored in order as the variables $1 , $2 , $3 , and so on. The variable $0 stores the name of the script itself, as invoked from the command line, and the variables $* and $@ store all of the command line arguments separated by spaces. [2] Our vulnerability testing program above might begin with:

[2] The difference between these two variables is in how they behave when quoted.

 
 #!/bin/sh    if [ "$1" = "" ]; then       echo "You must supply a hostname"       exit 1    fi    host="$1"    echo "Scanning $host" 

Arguments can also be manipulated with the shift command. It will throw out the value of $1 , store $2 in $1 , store $3 in $2 , and so on. A common example of argument processing using the shift command is presented in the next section, on Bourne shell loops .

10.2.7 Loops

The Bourne shell supports for loops as follows :

 
 #!/bin/sh    servers="mail1.example.com mail2.example.com time.example.com"    for i in $servers; do       echo $i    done 

Support for while loops is also included. In this example, we use a while loop to perform argument processing:

 
 #!/bin/sh    quiet=0    verbose=0    while [ "$1" != "" ]; do      if [ "$1" = "-q" ]; then         quiet=1         shift      elif [ "$1" = "-v" ]; then         verbose=1         shift      else         echo "Invalid argument"         exit      fi    done 

10.2.8 Using Command Output

Many times you will want to use the output of a particular program in your script. Perhaps you wish to store the result of the date command in a variable so that it can be used several times later on in the script. You can do so by placing the command in single back quotes:

 
 #!/bin/sh    date='date'    echo The date is $date 

Many scripts use this technique to perform simple changes to text using sed or awk :

 
 gecos='grep "$1" /etc/passwd  awk -F: '{print $5}'' 

Note that $1 refers to the first argument on the command line, but the $5 is something else entirely. When single quotes are used in the Bourne shell, as they are in 'print $5' , variables values are not substituted. Instead, the text string $5 is passed on to the awk program, which uses that syntax to represent the fifth field of text (in this case, the fifth field of the password file).

10.2.9 Working with Input and Output

Input and output redirection is straightforward in the Bourne shell. The standard output is redirected to a file with > filename :

 
 #!/bin/sh    echo "Starting script at 'date'" > /var/tmp/log 

This will overwrite an existing file; if you wish to append to the file instead, use >> filename :

 
 echo "Ending script at 'date'" >> /var/tmp/log 

You can redirect standard error by placing the number 2 in front of the greater-than sign. [3] The following will redirect both the standard out and standard error to the same file:

[3] It is 2 because that is the file descriptor number of standard error.

 
 #!/bin/sh    make > /var/tmp/buildlog 2> /var/tmp/buildlog 

The standard input can be redirected from a file with a less-than sign:

 
 grep $ user < /etc/passwd 

It is also possible to read lines from the standard input and use them in your script. The following script, for example, can be used much like the cat program. Run it as testscript.sh filename :

 
 #!/bin/sh    while read a; do       echo $a    done < $1 

The read command reads one line from the standard input and stores it in the named variable, here a . When the end of the file is reached, read returns a non-zero value. This allows the loop above to continue reading lines until no more are present.

10.2.10 Functions

Modern versions of the Bourne shell support user-created functions, though it is still possible to find some old versions that do not. Make sure to test out functions on your system before relying on them.

This program defines and uses a function called logit :

 
 #!/bin/sh    logit () {      echo "$*" >> /var/tmp/scriptlog    }    logit "Starting script at 'date'"    # do some work here    logit "Finished script at 'date'" 

Notice that within a function, the variables $1 , $* , etc., now refer to the arguments passed to the function instead of the arguments passed to the script command line. Functions can also return a value just as a program can. This allows you to check the success or failure of the function. As an example:

 
 #!/bin/sh    myfunction () {      return 1    }    myfunction    echo $? 

This produces:

 
 Solaris% ./testscript.sh    1 

10.2.11 Other Miscellaneous Items

There are many other features available to you in the Bourne shell, all of which are documented in the manual page. The following sections describe a few odds and ends that you may find useful.

Interpreting Another File

You can cause a Bourne shell script in another file to be interpreted by placing the filename after a period and a space:

 
 #!/bin/sh    echo This is my script    . /var/tmp/otherscript    echo Back to my script 

All the commands in /var/tmp/otherscript will then be run just as if they were present our script. Note that this means you do not want /var/tmp/otherscript to start with a #!/bin/sh line.

Exiting

You can exit a Bourne shell script with the exit command. If you give it an integer argument, that will be the exit status of the program:

 
 #!/bin/sh    if [ "$1" = "" ]; then       echo "You must supply an argument"       exit 1    fi 
Traps

Unlike many other simple scripting languages, the Bourne shell lets you catch signals with the trap command:

 
 #!/bin/sh    trap "echo Interrupted ; exit 1" 2    sleep 30 

This script, if run normally, will simply wait for 30 seconds and then exit. Instead, run the script, but before it finishes, type <ctrl>-C to send it an interrupt signal. The result is this:

 
 Solaris% ./testscript.sh    ^CInturrupted 

The trap command takes two or more arguments. The first is the command to be run when the signal is caught. In this case, we wish to print "Interrupted" and then exit the script, so these two commands are placed in quotation marks so as to be one argument to the trap command. The second and following arguments to trap list the signals to be caught. Each signal is listed by number; the number assigned to the interrupt signal (SIGINT) is 2. The full list of signals available, in sequential order starting with signal 1, can be printed from the command line with:

 
 Linux% kill -l    HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALA...    CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH P...    RTMIN RTMIN+1 RTMIN+2 RTMIN+3 RTMAX-3 RTMAX-2 RTMAX-1 RTMAX 

This list may be different on different systems.

In a more practical setting, you might use the trap command to call a function that cleans up temporary files and the like, in case a script is interrupted unexpectedly.

The Process ID

The process ID of the script itself is stored in the variable $$ . This can be handy in creating temporary files so that they do not conflict with other instances of the script running at the same time.

 
 #!/bin/sh tmpfile =/tmp/scripttmp.$$ 
Comments

Any line beginning with a pound sign ( # ) is a comment line and is ignored by the interpreter. The #!/bin/sh line at the beginning is an exception, as described earlier.



Open Source Network Administration
Linux Kernel in a Nutshell (In a Nutshell (OReilly))
ISBN: 130462101
EAN: 2147483647
Year: 2002
Pages: 85

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net