Introduction to SH shell programming

Note: Like the /bin/csh shell page, here is a good introduction to /bin/sh programming.

Credit: I found this here at North Carolina University...

Introduction

A shell is a command line interpreter. It takes commands and executes them. As such, it implements a programming language. The Bourne shell can be used to create shell scripts, i.e., 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 like this:

   % 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:

Fundamentals

#!/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 interpreters -- 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 specification:

   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 supplied (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

for - loop iteration

Substitute values for variable and perform task:

   for variable in word ...

   do

        command

   done

For example:

   for i in `cat $LOGS`

   do

           mv $i $i.$TODAY

           cp /dev/null $i

           chmod 664 $i

   done

Alternatively you may see:

   for variable in word ...; do command; done

case - multiway branch

Switch to statements depending on pattern match:

   case word in

   [ pattern [ | pattern ... ] )

        command ;; ] ...

   esac

For example:

   case "$year" in

   [0-9][0-9])

           year=19${year}

           years=`expr $year - 1901`

           ;;

   [0-9][0-9][0-9][0-9])

           years=`expr $year - 1901`

           ;;

   *)

           echo 1>&2 Year \"$year\" out of range ...

           exit 127

           ;;

   esac

if - conditional execution

Test exit status of command and branch:

   if command

   then

        command

   [ else

        command ]

   fi

For example:

   if [ $# -ne 3 ]; then

           echo 1>&2 Usage: $0 19 Oct 91

           exit 127

   fi

Alternatively you may see:

   if command; then command; [ else command; ] fi

while/until - tested iterations

Repeat task while command returns good exit status.

   {while | until} command

   do

        command

   done

For example:

   # for each argument mentioned, purge that directory

   while [ $# -ge 1 ]; do

           _purge $1

           shift

   done

Alternatively you may see:

   while command; do command; done

Variables

Variables are sequences of letters, digits, or underscores beginning with a letter or underscore. To get the contents of a variable you must prepend the name with a $.

Numeric variables (eg. like $1, etc.) are positional vari- ables for argument communication.

Variable Assignment

Assign a value to a variable by variable=value. For example:

   PATH=/usr/ucb:/usr/bin:/bin; export PATH

or

   TODAY=`(set \`date\`; echo $1)`

Exporting Variables

Variables are not exported to children unless explicitly marked.

   # We MUST have a DISPLAY environment variable

   if [ "$DISPLAY" = "" ]; then

           if tty -s ; then

    echo "DISPLAY (`hostname`:0.0)? \c";

    read DISPLAY

           fi

           if [ "$DISPLAY" = "" ]; then

    DISPLAY=`hostname`:0.0

           fi

           export DISPLAY

   fi

Likewise, for variables like the PRINTER which you want hon- ored by lpr(1). From a user's .profile:

   PRINTER=PostScript; export PRINTER

Note that the Cshell automatically exports all environment variables.

Referencing Variables

Use $variable (or, if necessary, ${variable}) to reference the value.

   # Most user's have a /bin of their own

   if [ "$USER" != "root" ]; then

           PATH=$HOME/bin:$PATH

   else

           PATH=/etc:/usr/etc:$PATH

   fi

The braces are required for concatenation constructs.

$p_01

is the value of the variable "p_01".

${p}_01

is the value of the variable "p" with "_01" pasted onto the end.

Conditional Reference

${variable-word}

If the variable has been set, use it's value, else use word.

POSTSCRIPT=${POSTSCRIPT-PostScript};

export POSTSCRIPT

${variable:-word}

If the variable has been set and is not null, use it's value, else use word.

These are useful constructions for honoring the user environment, i.e., the user of the script can override variable assignments. Compare to programs like lpr(1), which honors the PRINTER environment variable. You can do the same trick with your shell scripts.

${variable:?word}

If variable is set use it's value, else print out word and exit. Useful for bailing out.

Arguments

Command line arguments to shell scripts are positional variables:

$0, $1, ...

The command and arguments, with $0 the command and the rest the arguments.

$#

The number of arguments.

$*, $@

All the arguments as a blank separated string. Watch out for "$*" vs. "$@".

Commands for Variables

shift

Shift the postional variables down one and decrement number of arguments.

set arg arg ...

Set the positional variables to the argument list.

Command line parsing uses shift:

   # parse argument list

   while [ $# -ge 1 ]; do

           case $1 in

        process arguments...

           esac

           shift

   done

A use of the set command:

   # figure out what day it is

   TODAY=`(set \`date\`; echo $1)`

   cd $SPOOL

   for i in `cat $LOGS`

   do

           mv $i $i.$TODAY

           cp /dev/null $i

           chmod 664 $i

   done

Special Variables

$$

Current process id. This is very useful for constructing temporary files.

        tmp=/tmp/cal0$$

        trap "rm -f $tmp /tmp/cal1$$ /tmp/cal2$$"

        trap exit 1 2 13 15

        /usr/lib/calprog >$tmp

$?

The exit status of the last command.

        $command

        # Run target file if no errors and ...

        if [ $? -eq 0 ]

        then

 etc...

        fi

Quotes/Special Characters

Special characters to terminate words:

     ; & ( ) | ^ < > new-line space tab

These are for command sequences, background jobs, etc. To quote any of these use a backslash (\) or bracket with quote marks ("" or '').

Single Quotes

Within single quotes all characters are quoted, including the backslash. The result is one word.

        grep :${gid}: /etc/group | awk -F: '{print $1}'

Double Quotes

Within double quotes you have variable substitution (i.e., the dollar sign is interpreted) but no file name generation (i.e., * and ? are quoted). The result is one word.

        if [ ! "${parent}" ]; then

          parent=${people}/${group}/${user}

        fi

Back Quotes

Back quotes mean run the command and substitute the output.

        if [ "`echo -n`" = "-n" ]; then

  n=""

  c="\c"

        else

  n="-n"

  c=""

        fi

and

        TODAY=`(set \`date\`; echo $1)`

Functions

Functions are a powerful feature that aren't used often enough. Syntax is

   name ()

   {

        commands

   }

For example:

   # Purge a directory

   _purge()

   {

           # there had better be a directory

           if [ ! -d $1 ]; then

    echo $1: No such directory 1>&2

    return

           fi

        etc...

   }

Within a function the positional parmeters $0, $1, etc. are the arguments to the function (not the arguments to the script).

Within a function use return instead of exit.

Functions are good for encapsulations. You can pipe, redi- rect input, etc. to functions. For example:

   # deal with a file, add people one at a time

   do_file()

   {

           while parse_one

           etc...

   }

   etc...

   # take standard input (or a specified file) and do it.

   if [ "$1" != "" ]; then

           cat $1 | do_file

   else

           do_file

   fi

Sourcing Commands

You can execute shell scripts from within shell scripts. A couple of choices:

sh command

This runs the shell script as a separate shell. For example, on Sun machines in /etc/rc:

        sh /etc/rc.local

. command

This runs the shell script from within the current shell script. For example:

        # Read in configuration information

        .  /etc/hostconfig

What are the virtues of each? What's the difference? The second form is useful for configuration files where environment variables are set for the script. For example:

   for HOST in $HOSTS; do

     # is there a config file for this host?

     if [ -r ${BACKUPHOME}/${HOST} ]; then

.  ${BACKUPHOME}/${HOST}

     fi

   etc...

Using configuration files in this manner makes it possible to write scripts that are automatically tailored for differ- ent situations.

Some Tricks

Test

The most powerful command is test(1).

   if test expression; then

        etc...

and (note the matching bracket argument)

   if [ expression ]; then

        etc...

On System V machines this is a builtin (check out the com- mand /bin/test).

On BSD systems (like the Suns) compare the command /usr/bin/test with /usr/bin/[.

Useful expressions are:

test { -w, -r, -x, -s, ... } filename

is file writeable, readable, executeable, empty, etc?

test n1 { -eq, -ne, -gt, ... } n2

are numbers equal, not equal, greater than, etc.?

test s1 { =, != } s2

Are strings the same or different?

test cond1 { -o, -a } cond2

Binary or; binary and; use ! for unary negation.

For example

   if [ $year -lt 1901  -o  $year -gt 2099 ]; then

        echo 1>&2 Year \"$year\" out of range

        exit 127

   fi

Learn this command inside out! It does a lot for you.

String matching

The test command provides limited string matching tests. A more powerful trick is to match strings with the case switch.

   # parse argument list

   while [ $# -ge 1 ]; do

           case $1 in

           -c*)    rate=`echo $1 | cut -c3-`;;

           -c)     shift;  rate=$1 ;;

           -p*)    prefix=`echo $1 | cut -c3-`;;

           -p)     shift;  prefix=$1 ;;

           -*)     echo $Usage; exit 1 ;;

           *)      disks=$*;       break   ;;

           esac

           shift

   done

Of course getopt would work much better.

SysV vs BSD echo

On BSD systems to get a prompt you'd say:

   echo -n Ok to proceed?;  read ans

On SysV systems you'd say:

   echo Ok to procede? \c; read ans

In an effort to produce portable code we've been using:

   # figure out what kind of echo to use

   if [ "`echo -n`" = "-n" ]; then

           n="";  c="\c"

   else

           n="-n";     c=""

   fi

   etc...

   echo $n Ok to procede? $c; read ans

Is there a person?

The Unix tradition is that programs should execute as quietly as possible, especially for pipelines, cron jobs, etc.

User prompts aren't required if there's no user.

   # If there's a person out there, prod him a bit.

   if tty -s; then

        echo Enter text end with \^D

   fi

The tradition also extends to output.

   # If the output is to a terminal, be verbose

   if tty -s <&1; then

        verbose=true

   else

        verbose=false

   fi

Beware: just because stdin is a tty that doesn't mean that stdout is too. User prompts should be directed to the user terminal.

   # If there's a person out there, prod him a bit.

   if tty -s; then

        echo Enter text end with \^D >&0

   fi

Have you ever had a program stop waiting for keyboard input when the output is directed elsewhere?

Creating Input

We're familiar with redirecting input. For example:

   # take standard input (or a specified file) and do it.

   if [ "$1" != "" ]; then

           cat $1 | do_file

   else

           do_file

   fi

alternatively, redirection from a file:

   # take standard input (or a specified file) and do it.

   if [ "$1" != "" ]; then

           do_file < $1

   else

           do_file

   fi

You can also construct files on the fly.

   rmail bsmtp << EOF

   helo news

   mail from:<[email protected]>

   rcpt to:<listserv@$3>

   data

   from: <[email protected]>

   to: <listserv@$3>

   Subject: Signon $2

   subscribe $2 Usenet Feeder at UWO

   .

   quit

   EOF

Note that variables are expanded in the input.

String Manipulations

One of the more common things you'll need to do is parse strings. Some tricks:

   TIME=`date | cut -c12-19`

   TIME=`date | sed 's/.* .* .* \(.*\) .* .*/\1/'`

   TIME=`date | awk '{print $4}'`

   TIME=`set \`date\`; echo $4`

   TIME=`date | (read u v w x y z; echo $x)`

With some care, redefining the input field separators can help.

   #!/bin/sh

   # convert IP number to in-addr.arpa name

   name()

   {    set `IFS=".";echo $1`

        echo $4.$3.$2.$1.in-addr.arpa

   }

   if [ $# -ne 1 ]; then

        echo 1>&2 Usage: bynum IP-address

        exit 127

   fi

   add=`name $1`

   nslookup << EOF | grep "$add" | sed 's/.*= //'

   set type=any

   $add

   EOF

Debugging

The shell has a number of flags that make debugging easier:

sh -n command

Read the shell script but don't execute the commands, i.e., check syntax.

sh -x command

Display commands and arguments as they're executed. In a lot of my shell scripts you'll see

   # Uncomment the next line for testing

   # set -x

Based on An Introduction to Shell Programing by:

Reg Quinton <[email protected]>

Computing and Communications Services

The University of Western Ontario

London, Ontario N6A 5B7

Canada

The original version of this document is here.