levselector.com New York
home > sh  Email

sh (Bourne Shell)
intro
simple sh tutorial - part 1
simple sh tutorial - part 2
simple sh tutorial - part 3
simple sh tutorial - part 4
simple sh tutorial - part 5
examples 1

 
intro home - top of the page - email

sh - Bourne Shell

www.columbia.edu/acis/tutor/Unixhelp/scrpt_.html - simple shell tutorial
theory.uwinnipeg.ca/UNIXhelp/scrpt/ - simple shell tutorial
steve-parker.org/sh/sh.shtml - tutorial
www.ooblick.com/text/sh/ - tutorial (by Andrew Arensburger)
www.geocities.com/~gregl/htm/bourne_shell_tutorial.htm - shell programming tutorial
www.ling.helsinki.fi/users/reriksso/unix/shell.html - An Introduction to the Unix Shell - by S. R. Bourne
nim.cit.cornell.edu/usr/share/man/info/en_US/a_doc_lib/aixuser/usrosdev/bourne_shell.htm - yet another one
www.torget.se/users/d/Devlin/shell/ - UNIX Bourne Shell Programming
www.i-cram.com/cgi-bin/page.cgi?g=Unix%2FShell%2FScripting%2Findex.html - many tutorials

Books:
"Portable Shell Scripting"  by Bruce Blinn

 
simple sh tutorial - part 1 home - top of the page - email

How to write a shell script
Introduction

A shell is a command line interpretor. It takes commands and executes them. You can put those commands in a file - thus you get a program (shell script) written in shell as a programming language.

Creating a Script
Suppose you often type the command
    find . -name file -print
and you'd rather type a simple command, say
    sfind file
Let's create a shell script file "sfind' in your ~/bin directory (make sure that it is in your path), put there 2 lines:
#!/bin/sh

# this is a comment

find . -name $1 -print    # this is a comment

# this is a comment

Save this file and make it executable:
chmod a+x  sfind
rehash

That's it.

Path Specification
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. Note the export PATH command - 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.
#!/bin/sh

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.
#!/bin/sh

# 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.

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:
#!/bin/sh

if command; then
       command
fi

# For example:

if tty -s; then
     echo Enter text end with \^D
fi

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:
#!/bin/sh

# 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)).


 
simple sh tutorial - part 2 home - top of the page - email

For loop iteration:
#!/bin/sh

    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:
#!/bin/sh

# 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

Conditional Execution:
#!/bin/sh

# 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 Iteration:
#!/bin/sh

# 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


 
simple sh tutorial - part 3 home - top of the page - email

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.
#!/bin/sh

# 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 honored by lpr(1). 
# From a user's .profile:

    PRINTER=PostScript; export PRINTER

# Note: that the Cshell exports all environment variables.

Referencing Variables:
Use $variable (or, if necessary, ${variable}) to reference the value.
#!/bin/sh

# 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         # returns the value of the variable called "p_01".
${p}_01     # returns the value of the variable "p" concatenated with "_01"

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. Ie. the user of the script can override variable assignments. Cf. programs like lpr(1) honor 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, ...

Where $0 is the command, and the rest the arguments.

$# - The number of arguments.

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

And, some commands:
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:
 
#!/bin/sh

# 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

 
simple sh tutorial - part 4 home - top of the page - email

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 subsitution (ie. the dollar sign is interpreted) but no file name generation (ie. * 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:
#!/bin/sh

# 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, redirect 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 variable are set for the script. For example:
#!/bin/sh

    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 different 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 command /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.

 
simple sh tutorial - part 5 home - top of the page - email

SysV vs BSD echo:
On BSD systems to get a prompt you'd say:
    echo -n Ok to procede?;  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:
#!/bin/sh

# 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:
#!/bin/sh

# Exampel of redirecting input: 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:<$1@newshost.uwo.ca>
rcpt to:<listserv@$3>
data
from: <$1@newshost.uwo.ca>
to: <listserv@$3>
Subject: Signon $2

subscribe $2 Usenet Feeder at UWO
.
quit
EOF

# Note: that variables are expanded in the input.

String Manipulations:
#!/bin/sh

# Some tricks parsing strings

    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. IE. 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

----------------------------------------------------
This tutorial is based on An Introduction to Shell Programing by Reg Quinton

 
examples 1 home - top of the page - email

You can use here-doc constructs:
#!/bin/sh

sort <<EOF
line
abc
ABC
line2
EOF

You can use here-doc constructs:
#!/bin/sh

while [ 1 ]; do
  echo "enter : \c";
  read aword
  echo "Entered -> $aword"
done