find . -name file -printand you'd rather type a simple command, say
sfind fileCreate 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
%chmod a+x sfind
#!/bin/shFrom 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.
PATH=/usr/ucb:/usr/bin:/bin; export PATHA 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.
if [ $# -ne 3 ]; then echo 1>&2 Usage: $0 19 Oct 91 exit 127 fiThis script requires three arguments and gripes 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 range exit 127 fi etc... # All done, exit ok exit 0A 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.
The conditional construct is:
if command; then command fiFor example,
if tty -s; then echo Enter text end with \^D fiYour code should be written with the expectation that others will use it. Making sure you return a meaningful exit status will help.
# 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" fiNote: 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)).
Substitute values for variable and perform task:
for variable in word ... do command doneFor example:
for i in `cat $LOGS` do mv $i $i.$TODAY cp /dev/null $i chmod 664 $i doneAlternatively you may see:
for variable in word ...; do command; done
Switch to statements depending on pattern match
case word in [ pattern [ | pattern ... ] ) command ;; ] ... esacFor 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
Test exit status of command and branch
if command then command [ else command ] fiFor example:
if [ $# -ne 3 ]; then echo 1>&2 Usage: $0 19 Oct 91 exit 127 fiAlternatively you may see:
if command; then command; [ else command; ] fi
Repeat task while command returns good exit status.
{while | until} command do command doneFor example:
# for each argument mentioned, purge that directory while [ $# -ge 1 ]; do _purge $1 shift doneAlternatively you may see:
while command; do command; done
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.
Assign a value to a variable by variable=value. For example:
PATH=/usr/ucb:/usr/bin:/bin; export PATHor
TODAY=`(set \`date\`; echo $1)`
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 fiLikewise, for variables like the PRINTER which you want hon- ored by lpr(1). From a user's .profile:
PRINTER=PostScript; export PRINTERNote: that the Cshell exports all environment 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 fiThe braces are required for concatenation constructs.
$p_01The value of the variable "p_01".
${p}_01The value of the variable "p" with "_01" pasted onto the end.
${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 envi- ronment. 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.
Command line arguments to shell scripts are positional vari- ables:
$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. "$@".
shiftShift 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 doneA 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
$$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
Special characters to terminate words:
; & ( ) | ^ < > new-line space tabThese 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} fiBack Quotes
Back quotes mean run the command and substitute the output.
if [ "`echo -n`" = "-n" ]; then n="" c="\c" else n="-n" c="" fiand
TODAY=`(set \`date\`; echo $1)`
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
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/hostconfigWhat 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:
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.
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, ... } filenameis file writeable, readable, executeable, empty, etc?
test n1 { -eq, -ne, -gt, ... } n2are numbers equal, not equal, greater than, etc.?
test s1 { =, != } s2Are strings the same or different?
test cond1 { -o, -a } cond2Binary 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 fiLearn this command inside out! It does a lot for you.
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 doneOf course getopt would work much better.
On BSD systems to get a prompt you'd say:
echo -n Ok to procede?; read ansOn SysV systems you'd say:
echo Ok to procede? \c; read ansIn 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
The Unix tradition is that programs should execute as qui- etly 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 fiThe tradition also extends to output.
# If the output is to a terminal, be verbose if tty -s <&1; then verbose=true else verbose=false fiBeware: 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 fiHave you ever had a program stop waiting for keyboard input when the output is directed elsewhere?
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 fialternatively, redirection from a file:
# take standard input (or a specified file) and do it. if [ "$1" != "" ]; then do_file < $1 else do_file fiYou can also construct files on the fly.
rmail bsmtp <Note: that variables are expanded in the input.rcpt to: data from: <$1@newshost.uwo.ca> to: Subject: Signon $2 subscribe $2 Usenet Feeder at UWO . quit EOF
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
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
Based on An Introduction to Shell Programing by:
Reg Quinton