How to write a shell script

Introduction

A shell is a command line interpretor. It takes commands and executes them. As such, it implements a programming language. The Bourne shell is used to create shell scripts -- ie. programs that are interpreted/executed by the shell. You can write shell scripts with the C-shell; however, this is not covered here.

Creating a Script

Suppose you often type the command
    find . -name file -print
and you'd rather type a simple command, say
    sfind file
Create a shell script
    % cd ~/bin
    % emacs sfind
    % page sfind
    find . -name $1 -print
    % chmod a+x sfind
    % rehash
    % cd /usr/local/bin
    % sfind tcsh
    ./shells/tcsh

Observations

This quick example is far from adequate but some observations:
  1. Shell scripts are simple text files created with an editor.
  2. Shell scripts are marked as executeable
        %chmod a+x sfind
    
  3. Should be located in your search path and ~/bin should be in your search path.
  4. You likely need to rehash if you're a Csh (tcsh) user (but not again when you login).
  5. Arguments are passed from the command line and referenced. For example, as $1.

#!/bin/sh

All Bourne Shell scripts should begin with the sequence
    #!/bin/sh
From the man page for exec(2):

"On the first line of an interpreter script, following the "#!", is the name of a program which should be used to interpret the contents of the file. For instance, if the first line contains "#! /bin/sh", then the con- tents of the file are executed as a shell script."

You can get away without this, but you shouldn't. All good scripts state the interpretor explicitly. Long ago there was just one (the Bourne Shell) but these days there are many interpretors -- Csh, Ksh, Bash, and others.

Comments

Comments are any text beginning with the pound (#) sign. A comment can start anywhere on a line and continue until the end of the line.

Search Path

All shell scripts should include a search path specifica- tion:
    PATH=/usr/ucb:/usr/bin:/bin; export PATH
A PATH specification is recommended -- often times a script will fail for some people because they have a different or incomplete search path.

The Bourne Shell does not export environment variables to children unless explicitly instructed to do so by using the export command.

Argument Checking

A good shell script should verify that the arguments sup- plied (if any) are correct.

    if [ $# -ne 3 ]; then
         echo 1>&2 Usage: $0 19 Oct 91
         exit 127
    fi
This script requires three arguments and gripes accordingly.

Exit status

All Unix utilities should return an exit status.
    # is the year out of range for me?

    if [ $year -lt 1901  -o  $year -gt 2099 ]; then
         echo 1>&2 Year \"$year\" out of range
         exit 127
    fi

    etc...

    # All done, exit ok

    exit 0
A non-zero exit status indicates an error condition of some sort while a zero exit status indicates things worked as expected.

On BSD systems there's been an attempt to categorize some of the more common exit status codes. See /usr/include/sysexits.h.

Using exit status

Exit codes are important for those who use your code. Many constructs test on the exit status of a command.

The conditional construct is:

    if command; then
         command
    fi
For example,
    if tty -s; then
         echo Enter text end with \^D
    fi
Your code should be written with the expectation that others will use it. Making sure you return a meaningful exit status will help.

Stdin, Stdout, Stderr

Standard input, output, and error are file descriptors 0, 1, and 2. Each has a particular role and should be used accordingly:
    # is the year out of range for me?

    if [ $year -lt 1901  -o  $year -gt 2099 ]; then
         echo 1>&2 Year \"$year\" out of my range
         exit 127
    fi

    etc...

    # ok, you have the number of days since Jan 1, ...

    case `expr $days % 7` in
    0)
         echo Mon;;
    1)
         echo Tue;;

    etc...
Error messages should appear on stderr not on stdout! Output should appear on stdout. As for input/output dialogue:
    # give the fellow a chance to quit

    if tty -s ; then
         echo This will remove all files in $* since ...
         echo $n Ok to procede? $c;      read ans
         case "$ans" in
              n*|N*)
    echo File purge abandoned;
    exit 0   ;;
         esac
         RM="rm -rfi"
    else
         RM="rm -rf"
    fi
Note: this code behaves differently if there's a user to communicate with (ie. if the standard input is a tty rather than a pipe, or file, or etc. See tty(1)).

Language Constructs

Some Tricks

Based on An Introduction to Shell Programing by:

Reg Quinton
Computing and Communications Services
The University of Western Ontario
London, Ontario N6A 5B7
Canada


Press here to return to the General Unix Software Menu.