Appendix A. Contributed Scripts

These scripts, while not fitting into the text of this document, do illustrate some interesting shell programming techniques. They are useful, too. Have fun analyzing and running them.


Example A-1. manview: Viewing formatted manpages

   1 #!/bin/bash
   2 # manview.sh: Formats the source of a man page for viewing.
   3 
   4 # This is useful when writing man page source and you want to
   5 # look at the intermediate results on the fly while working on it.
   6 
   7 E_WRONGARGS=65
   8 
   9 if [ -z "$1" ]
  10 then
  11   echo "Usage: `basename $0` [filename]"
  12   exit $E_WRONGARGS
  13 fi
  14 
  15 groff -Tascii -man $1 | less
  16 # From the man page for groff.
  17 
  18 # If the man page includes tables and/or equations,
  19 # then the above code will barf.
  20 # The following line can handle such cases.
  21 #
  22 #   gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
  23 #
  24 #   Thanks, S.C.
  25 
  26 exit 0


Example A-2. mailformat: Formatting an e-mail message

   1 #!/bin/bash
   2 # mail-format.sh: Format e-mail messages.
   3 
   4 # Gets rid of carets, tabs, also fold excessively long lines.
   5 
   6 ARGS=1
   7 E_BADARGS=65
   8 E_NOFILE=66
   9 
  10 if [ $# -ne $ARGS ]  # Correct number of arguments passed to script?
  11 then
  12   echo "Usage: `basename $0` filename"
  13   exit $E_BADARGS
  14 fi
  15 
  16 if [ -f "$1" ]       # Check if file exists.
  17 then
  18     file_name=$1
  19 else
  20     echo "File \"$1\" does not exist."
  21     exit $E_NOFILE
  22 fi
  23 
  24 MAXWIDTH=70          # Width to fold long lines to.
  25 
  26 sed '
  27 s/^>//
  28 s/^  *>//
  29 s/^  *//
  30 s/		*//
  31 ' $1 | fold -s --width=$MAXWIDTH
  32           # -s option to "fold" breaks lines at whitespace, if possible.
  33 
  34 # This script was inspired by an article in a well-known trade journal
  35 # extolling a 164K Windows utility with similar functionality.
  36 
  37 exit 0


Example A-3. rn: A simple-minded file rename utility

This script is a modification of Example 12-14.

   1 #! /bin/bash
   2 #
   3 # Very simpleminded filename "rename" utility (based on "lowercase.sh").
   4 #
   5 # The "ren" utility, by Vladimir Lanin (lanin@csd2.nyu.edu),
   6 # does a much better job of this.
   7 
   8 
   9 ARGS=2
  10 E_BADARGS=65
  11 ONE=1                     # For getting singular/plural right (see below).
  12 
  13 if [ $# -ne "$ARGS" ]
  14 then
  15   echo "Usage: `basename $0` old-pattern new-pattern"
  16   # As in "rn gif jpg", which renames all gif files in working directory to jpg.
  17   exit $E_BADARGS
  18 fi
  19 
  20 number=0                  # Keeps track of how many files actually renamed.
  21 
  22 
  23 for filename in *$1*      #Traverse all matching files in directory.
  24 do
  25    if [ -f "$filename" ]  # If finds match...
  26    then
  27      fname=`basename $filename`            # Strip off path.
  28      n=`echo $fname | sed -e "s/$1/$2/"`   # Substitute new for old in filename.
  29      mv $fname $n                          # Rename.
  30      let "number += 1"
  31    fi
  32 done   
  33 
  34 if [ "$number" -eq "$ONE" ]                # For correct grammar.
  35 then
  36  echo "$number file renamed."
  37 else 
  38  echo "$number files renamed."
  39 fi 
  40 
  41 exit 0
  42 
  43 
  44 # Exercise for reader:
  45 # What type of files will this not work on?
  46 # How to fix this?


Example A-4. encryptedpw: Uploading to an ftp site, using a locally encrypted password

   1 #!/bin/bash
   2 
   3 # Example "ex72.sh" modified to use encrypted password.
   4 
   5 #  Note that this is still somewhat insecure,
   6 #+ since the decrypted password is sent in the clear.
   7 # Use something like "ssh" if this is a concern.
   8 
   9 E_BADARGS=65
  10 
  11 if [ -z "$1" ]
  12 then
  13   echo "Usage: `basename $0` filename"
  14   exit $E_BADARGS
  15 fi  
  16 
  17 Username=bozo           # Change to suit.
  18 pword=/home/bozo/secret/password_encrypted.file
  19 # File containing encrypted password.
  20 
  21 Filename=`basename $1`  # Strips pathname out of file name
  22 
  23 Server="XXX"
  24 Directory="YYY"         # Change above to actual server name & directory.
  25 
  26 
  27 Password=`cruft <$pword`          # Decrypt password.
  28 #  Uses the author's own "cruft" file encryption package,
  29 #+ based on the classic "onetime pad" algorithm,
  30 #+ and obtainable from:
  31 #+ Primary-site:   ftp://metalab.unc.edu /pub/Linux/utils/file
  32 #+                 cruft-0.2.tar.gz [16k]
  33 
  34 
  35 ftp -n $Server <<End-Of-Session
  36 user $Username $Password
  37 binary
  38 bell
  39 cd $Directory
  40 put $Filename
  41 bye
  42 End-Of-Session
  43 # -n option to "ftp" disables auto-logon.
  44 # "bell" rings 'bell' after each file transfer.
  45 
  46 exit 0


Example A-5. copy-cd: Copying a data CD

   1 #!/bin/bash
   2 # copy-cd.sh: copying a data CD
   3 
   4 CDROM=/dev/cdrom                           # CD ROM device
   5 OF=/home/bozo/projects/cdimage.iso         # output file
   6 #       /xxxx/xxxxxxx/                     Change to suit your system.
   7 BLOCKSIZE=2048
   8 SPEED=2                                    # May use higher speed if supported.
   9 
  10 echo; echo "Insert source CD, but do *not* mount it."
  11 echo "Press ENTER when ready. "
  12 read ready                                 # Wait for input, $ready not used.
  13 
  14 echo; echo "Copying the source CD to $OF."
  15 echo "This may take a while. Please be patient."
  16 
  17 dd if=$CDROM of=$OF bs=$BLOCKSIZE          # Raw device copy.
  18 
  19 
  20 echo; echo "Remove data CD."
  21 echo "Insert blank CDR."
  22 echo "Press ENTER when ready. "
  23 read ready                                 # Wait for input, $ready not used.
  24 
  25 echo "Copying $OF to CDR."
  26 
  27 cdrecord -v -isosize speed=$SPEED dev=0,0 $OF
  28 # Uses Joerg Schilling's "cdrecord" package (see its docs).
  29 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html
  30 
  31 
  32 echo; echo "Done copying $OF to CDR on device $CDROM."
  33 
  34 echo "Do you want to erase the image file (y/n)? "  # Probably a huge file.
  35 read answer
  36 
  37 case "$answer" in
  38 [yY]) rm -f $OF
  39       echo "$OF erased."
  40       ;;
  41 *)    echo "$OF not erased.";;
  42 esac
  43 
  44 echo
  45 
  46 # Exercise for the reader:
  47 # Change the above "case" statement to also accept "yes" and "Yes" as input.
  48 
  49 exit 0


Example A-6. days-between: Calculate number of days between two dates

   1 #!/bin/bash
   2 # days-between.sh:    Number of days between two dates.
   3 # Usage: ./days-between.sh [M]M/[D]D/YYYY [M]M/[D]D/YYYY
   4 
   5 ARGS=2                # Two command line parameters expected.
   6 E_PARAM_ERR=65        # Param error.
   7 
   8 REFYR=1600            # Reference year.
   9 CENTURY=100
  10 DIY=365
  11 ADJ_DIY=367           # Adjusted for leap year + fraction.
  12 MIY=12
  13 DIM=31
  14 LEAPCYCLE=4
  15 
  16 MAXRETVAL=256         # Largest permissable
  17                       # positive return value from a function.
  18 
  19 diff=		      # Declare global variable for date difference.
  20 value=                # Declare global variable for absolute value.
  21 day=                  # Declare globals for day, month, year.
  22 month=
  23 year=
  24 
  25 
  26 Param_Error ()        # Command line parameters wrong.
  27 {
  28   echo "Usage: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY"
  29   echo "       (date must be after 1/3/1600)"
  30   exit $E_PARAM_ERR
  31 }  
  32 
  33 
  34 Parse_Date ()                 # Parse date from command line params.
  35 {
  36   month=${1%%/**}
  37   dm=${1%/**}                 # Day and month.
  38   day=${dm#*/}
  39   let "year = `basename $1`"  # Not a filename, but works just the same.
  40 }  
  41 
  42 
  43 check_date ()                 # Checks for invalid date(s) passed.
  44 {
  45   [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error
  46   # Exit script on bad value(s).
  47   # Uses "or-list / and-list".
  48   # Exercise for the reader: Implement more rigorous date checking.
  49 }
  50 
  51 
  52 strip_leading_zero () # Better to strip possible leading zero(s)
  53 {                     # from day and/or month
  54   val=${1#0}          # since otherwise Bash will interpret them
  55   return $val         # as octal values (POSIX.2, sect 2.9.2.1).
  56 }
  57 
  58 
  59 day_index ()          # Gauss' Formula:
  60 {                     # Days from Jan. 3, 1600 to date passed as param.
  61 
  62   day=$1
  63   month=$2
  64   year=$3
  65 
  66   let "month = $month - 2"
  67   if [ "$month" -le 0 ]
  68   then
  69     let "month += 12"
  70     let "year -= 1"
  71   fi  
  72 
  73   let "year -= $REFYR"
  74   let "indexyr = $year / $CENTURY"
  75 
  76 
  77   let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM"
  78   # For an in-depth explanation of this algorithm, see
  79   # http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm
  80 
  81 
  82   if [ "$Days" -gt "$MAXRETVAL" ]  # If greater than 256,
  83   then                             # then change to negative value
  84     let "dindex = 0 - $Days"       # which can be returned from function.
  85   else let "dindex = $Days"
  86   fi
  87 
  88   return $dindex
  89 
  90 }  
  91 
  92 
  93 calculate_difference ()            # Difference between to day indices.
  94 {
  95   let "diff = $1 - $2"             # Global variable.
  96 }  
  97 
  98 
  99 abs ()                             # Absolute value
 100 {                                  # Uses global "value" variable.
 101   if [ "$1" -lt 0 ]                # If negative
 102   then                             # then
 103     let "value = 0 - $1"           # change sign,
 104   else                             # else
 105     let "value = $1"               # leave it alone.
 106   fi
 107 }
 108 
 109 
 110 
 111 if [ $# -ne "$ARGS" ]              # Require two command line params.
 112 then
 113   Param_Error
 114 fi  
 115 
 116 Parse_Date $1
 117 check_date $day $month $year      # See if valid date.
 118 
 119 strip_leading_zero $day           # Remove any leading zeroes
 120 day=$?                            # on day and/or month.
 121 strip_leading_zero $month
 122 month=$?
 123 
 124 day_index $day $month $year
 125 date1=$?
 126 
 127 abs $date1                         # Make sure it's positive
 128 date1=$value                       # by getting absolute value.
 129 
 130 Parse_Date $2
 131 check_date $day $month $year
 132 
 133 strip_leading_zero $day
 134 day=$?
 135 strip_leading_zero $month
 136 month=$?
 137 
 138 day_index $day $month $year
 139 date2=$?
 140 
 141 abs $date2                         # Make sure it's positive.
 142 date2=$value
 143 
 144 calculate_difference $date1 $date2
 145 
 146 abs $diff                          # Make sure it's positive.
 147 diff=$value
 148 
 149 echo $diff
 150 
 151 exit 0
 152 # Compare this script with the implementation of Gauss' Formula in C at
 153 # http://buschencrew.hypermart.net/software/datedif

+

The following two scripts are by Mark Moraes of the University of Toronto. See the enclosed file "Moraes-COPYRIGHT" for permissions and restrictions.


Example A-7. behead: Removing mail and news message headers

   1 #! /bin/sh
   2 # Strips off the header from a mail/News message i.e. till the first
   3 # empty line
   4 # Mark Moraes, University of Toronto
   5 
   6 # ==> These comments added by author of this document.
   7 
   8 if [ $# -eq 0 ]; then
   9 # ==> If no command line args present, then works on file redirected to stdin.
  10 	sed -e '1,/^$/d' -e '/^[ 	]*$/d'
  11 	# --> Delete empty lines and all lines until 
  12 	# --> first one beginning with white space.
  13 else
  14 # ==> If command line args present, then work on files named.
  15 	for i do
  16 		sed -e '1,/^$/d' -e '/^[ 	]*$/d' $i
  17 		# --> Ditto, as above.
  18 	done
  19 fi
  20 
  21 # ==> Exercise for the reader: Add error checking and other options.
  22 # ==>
  23 # ==> Note that the small sed script repeats, except for the arg passed.
  24 # ==> Does it make sense to embed it in a function? Why or why not?


Example A-8. ftpget: Downloading files via ftp

   1 #! /bin/sh 
   2 # $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $ 
   3 # Script to perform batch anonymous ftp. Essentially converts a list of
   4 # of command line arguments into input to ftp.
   5 # Simple, and quick - written as a companion to ftplist 
   6 # -h specifies the remote host (default prep.ai.mit.edu) 
   7 # -d specifies the remote directory to cd to - you can provide a sequence 
   8 # of -d options - they will be cd'ed to in turn. If the paths are relative, 
   9 # make sure you get the sequence right. Be careful with relative paths - 
  10 # there are far too many symlinks nowadays.  
  11 # (default is the ftp login directory)
  12 # -v turns on the verbose option of ftp, and shows all responses from the 
  13 # ftp server.  
  14 # -f remotefile[:localfile] gets the remote file into localfile 
  15 # -m pattern does an mget with the specified pattern. Remember to quote 
  16 # shell characters.  
  17 # -c does a local cd to the specified directory
  18 # For example, 
  19 # 	ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \
  20 #		-d ../pub/R3/fixes -c ~/fixes -m 'fix*' 
  21 # will get xplaces.shar from ~ftp/contrib on expo.lcs.mit.edu, and put it in
  22 # xplaces.sh in the current working directory, and get all fixes from
  23 # ~ftp/pub/R3/fixes and put them in the ~/fixes directory. 
  24 # Obviously, the sequence of the options is important, since the equivalent
  25 # commands are executed by ftp in corresponding order
  26 #
  27 # Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 
  28 # ==> Angle brackets changed to parens, so Docbook won't get indigestion.
  29 #
  30 
  31 
  32 # ==> These comments added by author of this document.
  33 
  34 # PATH=/local/bin:/usr/ucb:/usr/bin:/bin
  35 # export PATH
  36 # ==> Above 2 lines from original script probably superfluous.
  37 
  38 TMPFILE=/tmp/ftp.$$
  39 # ==> Creates temp file, using process id of script ($$)
  40 # ==> to construct filename.
  41 
  42 SITE=`domainname`.toronto.edu
  43 # ==> 'domainname' similar to 'hostname'
  44 # ==> May rewrite this to parameterize this for general use.
  45 
  46 usage="Usage: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \
  47 		[-c localdirectory] [-m filepattern] [-v]"
  48 ftpflags="-i -n"
  49 verbflag=
  50 set -f 		# So we can use globbing in -m
  51 set x `getopt vh:d:c:m:f: $*`
  52 if [ $? != 0 ]; then
  53 	echo $usage
  54 	exit 65
  55 fi
  56 shift
  57 trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15
  58 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}"
  59 # ==> Added quotes (recommended in complex echoes).
  60 echo binary >> ${TMPFILE}
  61 for i in $*   # ==> Parse command line args.
  62 do
  63 	case $i in
  64 	-v) verbflag=-v; echo hash >> ${TMPFILE}; shift;;
  65 	-h) remhost=$2; shift 2;;
  66 	-d) echo cd $2 >> ${TMPFILE}; 
  67 	    if [ x${verbflag} != x ]; then
  68 	        echo pwd >> ${TMPFILE};
  69 	    fi;
  70 	    shift 2;;
  71 	-c) echo lcd $2 >> ${TMPFILE}; shift 2;;
  72 	-m) echo mget "$2" >> ${TMPFILE}; shift 2;;
  73 	-f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
  74 	    echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;;
  75 	--) shift; break;;
  76 	esac
  77 done
  78 if [ $# -ne 0 ]; then
  79 	echo $usage
  80 	exit 65   # ==> Changed from "exit 2" to conform with standard.
  81 fi
  82 if [ x${verbflag} != x ]; then
  83 	ftpflags="${ftpflags} -v"
  84 fi
  85 if [ x${remhost} = x ]; then
  86 	remhost=prep.ai.mit.edu
  87 	# ==> Rewrite to match your favorite ftp site.
  88 fi
  89 echo quit >> ${TMPFILE}
  90 # ==> All commands saved in tempfile.
  91 
  92 ftp ${ftpflags} ${remhost} < ${TMPFILE}
  93 # ==> Now, tempfile batch processed by ftp.
  94 
  95 rm -f ${TMPFILE}
  96 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile).
  97 
  98 
  99 # ==> Exercises for reader:
 100 # ==> 1) Add error checking.
 101 # ==> 2) Add bells & whistles.

+

Antek Sawicki contributed the following script, which makes very clever use of the parameter substitution operators discussed in Section 9.3.


Example A-9. password: Generating random 8-character passwords

   1 #!/bin/bash
   2 # May need to be invoked with  #!/bin/bash2  on older machines.
   3 #
   4 # Random password generator for bash 2.x by Antek Sawicki <tenox@tenox.tc>,
   5 # who generously gave permission to the document author to use it here.
   6 #
   7 # ==> Comments added by document author ==>
   8 
   9 
  10 MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  11 LENGTH="8"
  12 # ==> May change 'LENGTH' for longer password, of course.
  13 
  14 
  15 while [ "${n:=1}" -le "$LENGTH" ]
  16 # ==> Recall that := is "default substitution" operator.
  17 # ==> So, if 'n' has not been initialized, set it to 1.
  18 do
  19 	PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
  20 	# ==> Very clever, but tricky.
  21 
  22 	# ==> Starting from the innermost nesting...
  23 	# ==> ${#MATRIX} returns length of array MATRIX.
  24 
  25 	# ==> $RANDOM%${#MATRIX} returns random number between 1
  26 	# ==> and length of MATRIX - 1.
  27 
  28 	# ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1}
  29 	# ==> returns expansion of MATRIX at random position, by length 1. 
  30 	# ==> See {var:pos:len} parameter substitution in Section 3.3.1
  31 	# ==> and following examples.
  32 
  33 	# ==> PASS=... simply pastes this result onto previous PASS (concatenation).
  34 
  35 	# ==> To visualize this more clearly, uncomment the following line
  36 	# ==>             echo "$PASS"
  37 	# ==> to see PASS being built up,
  38 	# ==> one character at a time, each iteration of the loop.
  39 
  40 	let n+=1
  41 	# ==> Increment 'n' for next pass.
  42 done
  43 
  44 echo "$PASS"      # ==> Or, redirect to file, as desired.
  45 
  46 exit 0

+

James R. Van Zandt contributed this script, which uses named pipes and, in his words, "really exercises quoting and escaping".


Example A-10. fifo: Making daily backups, using named pipes

   1 #!/bin/bash
   2 # ==> Script by James R. Van Zandt, and used here with his permission.
   3 
   4 # ==> Comments added by author of this document.
   5 
   6   
   7   HERE=`uname -n`    # ==> hostname
   8   THERE=bilbo
   9   echo "starting remote backup to $THERE at `date +%r`"
  10   # ==> `date +%r` returns time in 12-hour format, i.e. "08:08:34 PM".
  11   
  12   # make sure /pipe really is a pipe and not a plain file
  13   rm -rf /pipe
  14   mkfifo /pipe       # ==> Create a "named pipe", named "/pipe".
  15   
  16   # ==> 'su xyz' runs commands as user "xyz".
  17   # ==> 'ssh' invokes secure shell (remote login client).
  18   su xyz -c "ssh $THERE \"cat >/home/xyz/backup/${HERE}-daily.tar.gz\" < /pipe"&
  19   cd /
  20   tar -czf - bin boot dev etc home info lib man root sbin share usr var >/pipe
  21   # ==> Uses named pipe, /pipe, to communicate between processes:
  22   # ==> 'tar/gzip' writes to /pipe and 'ssh' reads from /pipe.
  23 
  24   # ==> The end result is this backs up the main directories, from / on down.
  25 
  26   # ==> What are the advantages of a "named pipe" in this situation,
  27   # ==> as opposed to an "anonymous pipe", with |?
  28   # ==> Will an anonymous pipe even work here?
  29 
  30 
  31   exit 0

+

Stephane Chazelas contributed the following script to demonstrate that generating prime numbers does not require arrays.


Example A-11. Generating prime numbers using the modulo operator

   1 #!/bin/bash
   2 # primes.sh: Generate prime numbers, without using arrays.
   3 
   4 #  This does *not* use the classic "Sieve of Erastosthenes" algorithm,
   5 #+ but instead uses the more intuitive method of testing each candidate number
   6 #+ for factors (divisors), using the "%" modulo operator.
   7 #
   8 # Script contributed by Stephane Chazelas,
   9 
  10 
  11 LIMIT=1000                    # Primes 2 - 1000
  12 
  13 Primes()
  14 {
  15  (( n = $1 + 1 ))             # Bump to next integer.
  16  shift                        # Next parameter in list.
  17 #  echo "_n=$n i=$i_"
  18  
  19  if (( n == LIMIT ))
  20  then echo $*
  21  return
  22  fi
  23 
  24  for i; do                    # "i" gets set to "@", previous values of $n.
  25 #   echo "-n=$n i=$i-"
  26    (( i * i > n )) && break   # Optimization.
  27    (( n % i )) && continue    # Sift out non-primes using modulo operator.
  28    Primes $n $@               # Recursion inside loop.
  29    return
  30    done
  31 
  32    Primes $n $@ $n            # Recursion outside loop.
  33                               # Successively accumulate positional parameters.
  34 			      # "$@" is the accumulating list of primes.
  35 }
  36 
  37 Primes 1
  38 
  39 exit 0
  40 
  41 # Uncomment lines 17 and 25 to help figure out what is going on.
  42 
  43 # Compare the speed of this algorithm for generating primes
  44 # with the Sieve of Erastosthenes (ex68.sh).
  45 
  46 # Exercise: Rewrite this script without recursion, for faster execution.

+

Jordi Sanfeliu gave permission to use his elegant tree script.


Example A-12. tree: Displaying a directory tree

   1 #!/bin/sh
   2 #         @(#) tree      1.1  30/11/95       by Jordi Sanfeliu
   3 #                                         email: mikaku@arrakis.es
   4 #
   5 #         Initial version:  1.0  30/11/95
   6 #         Next version   :  1.1  24/02/97   Now, with symbolic links
   7 #         Patch by       :  Ian Kjos, to support unsearchable dirs
   8 #                           email: beth13@mail.utexas.edu
   9 #
  10 #         Tree is a tool for view the directory tree (obvious :-) )
  11 #
  12 
  13 # ==> 'Tree' script used here with the permission of its author, Jordi Sanfeliu.
  14 # ==> Comments added by the author of this document.
  15 # ==> Argument quoting added.
  16 
  17 
  18 search () {
  19    for dir in `echo *`
  20    # ==> `echo *` lists all the files in current working directory, without line breaks.
  21    # ==> Similar effect to     for dir in *
  22    # ==> but "dir in `echo *`" will not handle filenames with blanks.
  23    do
  24       if [ -d "$dir" ] ; then   # ==> If it is a directory (-d)...
  25          zz=0   # ==> Temp variable, keeping track of directory level.
  26          while [ $zz != $deep ]    # Keep track of inner nested loop.
  27          do
  28             echo -n "|   "    # ==> Display vertical connector symbol,
  29 	                      # ==> with 2 spaces & no line feed in order to indent.
  30             zz=`expr $zz + 1` # ==> Increment zz.
  31          done
  32          if [ -L "$dir" ] ; then   # ==> If directory is a symbolic link...
  33             echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'`
  34 	    # ==> Display horiz. connector and list directory name, but...
  35 	    # ==> delete date/time part of long listing.
  36          else
  37             echo "+---$dir"      # ==> Display horizontal connector symbol...
  38                                  # ==> and print directory name.
  39             if cd "$dir" ; then  # ==> If can move to subdirectory...
  40                deep=`expr $deep + 1`   # ==> Increment depth.
  41                search     # with recursivity ;-)
  42 	                  # ==> Function calls itself.
  43                numdirs=`expr $numdirs + 1`   # ==> Increment directory count.
  44             fi
  45          fi
  46       fi
  47    done
  48    cd ..   # ==> Up one directory level.
  49    if [ "$deep" ] ; then  # ==> If depth = 0 (returns TRUE)...
  50       swfi=1              # ==> set flag showing that search is done.
  51    fi
  52    deep=`expr $deep - 1`  # ==> Decrement depth.
  53 }
  54 
  55 # - Main -
  56 if [ $# = 0 ] ; then
  57    cd `pwd`    # ==> No args to script, then use current working directory.
  58 else
  59    cd $1       # ==> Otherwise, move to indicated directory.
  60 fi
  61 echo "Initial directory = `pwd`"
  62 swfi=0      # ==> Search finished flag.
  63 deep=0      # ==> Depth of listing.
  64 numdirs=0
  65 zz=0
  66 
  67 while [ "$swfi" != 1 ]   # While flag not set...
  68 do
  69    search   # ==> Call function after initializing variables.
  70 done
  71 echo "Total directories = $numdirs"
  72 
  73 exit 0
  74 # ==> Challenge to reader: try to figure out exactly how this script works.

Noah Friedman gave permission to use his string function script, which essentially reproduces some of the C-library string manipulation functions.


Example A-13. string functions: C-like string functions

   1 #!/bin/bash
   2 
   3 # string.bash --- bash emulation of string(3) library routines
   4 # Author: Noah Friedman <friedman@prep.ai.mit.edu>
   5 # ==>     Used with his kind permission in this document.
   6 # Created: 1992-07-01
   7 # Last modified: 1993-09-29
   8 # Public domain
   9 
  10 # Conversion to bash v2 syntax done by Chet Ramey
  11 
  12 # Commentary:
  13 # Code:
  14 
  15 #:docstring strcat:
  16 # Usage: strcat s1 s2
  17 #
  18 # Strcat appends the value of variable s2 to variable s1. 
  19 #
  20 # Example:
  21 #    a="foo"
  22 #    b="bar"
  23 #    strcat a b
  24 #    echo $a
  25 #    => foobar
  26 #
  27 #:end docstring:
  28 
  29 ###;;;autoload   ==> Autoloading of function commented out.
  30 function strcat ()
  31 {
  32     local s1_val s2_val
  33 
  34     s1_val=${!1}                        # indirect variable expansion
  35     s2_val=${!2}
  36     eval "$1"=\'"${s1_val}${s2_val}"\'
  37     # ==> eval $1='${s1_val}${s2_val}' avoids problems,
  38     # ==> if one of the variables contains a single quote.
  39 }
  40 
  41 #:docstring strncat:
  42 # Usage: strncat s1 s2 $n
  43 # 
  44 # Line strcat, but strncat appends a maximum of n characters from the value
  45 # of variable s2.  It copies fewer if the value of variabl s2 is shorter
  46 # than n characters.  Echoes result on stdout.
  47 #
  48 # Example:
  49 #    a=foo
  50 #    b=barbaz
  51 #    strncat a b 3
  52 #    echo $a
  53 #    => foobar
  54 #
  55 #:end docstring:
  56 
  57 ###;;;autoload
  58 function strncat ()
  59 {
  60     local s1="$1"
  61     local s2="$2"
  62     local -i n="$3"
  63     local s1_val s2_val
  64 
  65     s1_val=${!s1}                       # ==> indirect variable expansion
  66     s2_val=${!s2}
  67 
  68     if [ ${#s2_val} -gt ${n} ]; then
  69        s2_val=${s2_val:0:$n}            # ==> substring extraction
  70     fi
  71 
  72     eval "$s1"=\'"${s1_val}${s2_val}"\'
  73     # ==> eval $1='${s1_val}${s2_val}' avoids problems,
  74     # ==> if one of the variables contains a single quote.
  75 }
  76 
  77 #:docstring strcmp:
  78 # Usage: strcmp $s1 $s2
  79 #
  80 # Strcmp compares its arguments and returns an integer less than, equal to,
  81 # or greater than zero, depending on whether string s1 is lexicographically
  82 # less than, equal to, or greater than string s2.
  83 #:end docstring:
  84 
  85 ###;;;autoload
  86 function strcmp ()
  87 {
  88     [ "$1" = "$2" ] && return 0
  89 
  90     [ "${1}" '<' "${2}" ] > /dev/null && return -1
  91 
  92     return 1
  93 }
  94 
  95 #:docstring strncmp:
  96 # Usage: strncmp $s1 $s2 $n
  97 # 
  98 # Like strcmp, but makes the comparison by examining a maximum of n
  99 # characters (n less than or equal to zero yields equality).
 100 #:end docstring:
 101 
 102 ###;;;autoload
 103 function strncmp ()
 104 {
 105     if [ -z "${3}" -o "${3}" -le "0" ]; then
 106        return 0
 107     fi
 108    
 109     if [ ${3} -ge ${#1} -a ${3} -ge ${#2} ]; then
 110        strcmp "$1" "$2"
 111        return $?
 112     else
 113        s1=${1:0:$3}
 114        s2=${2:0:$3}
 115        strcmp $s1 $s2
 116        return $?
 117     fi
 118 }
 119 
 120 #:docstring strlen:
 121 # Usage: strlen s
 122 #
 123 # Strlen returns the number of characters in string literal s.
 124 #:end docstring:
 125 
 126 ###;;;autoload
 127 function strlen ()
 128 {
 129     eval echo "\${#${1}}"
 130     # ==> Returns the length of the value of the variable
 131     # ==> whose name is passed as an argument.
 132 }
 133 
 134 #:docstring strspn:
 135 # Usage: strspn $s1 $s2
 136 # 
 137 # Strspn returns the length of the maximum initial segment of string s1,
 138 # which consists entirely of characters from string s2.
 139 #:end docstring:
 140 
 141 ###;;;autoload
 142 function strspn ()
 143 {
 144     # Unsetting IFS allows whitespace to be handled as normal chars. 
 145     local IFS=
 146     local result="${1%%[!${2}]*}"
 147  
 148     echo ${#result}
 149 }
 150 
 151 #:docstring strcspn:
 152 # Usage: strcspn $s1 $s2
 153 #
 154 # Strcspn returns the length of the maximum initial segment of string s1,
 155 # which consists entirely of characters not from string s2.
 156 #:end docstring:
 157 
 158 ###;;;autoload
 159 function strcspn ()
 160 {
 161     # Unsetting IFS allows whitspace to be handled as normal chars. 
 162     local IFS=
 163     local result="${1%%[${2}]*}"
 164  
 165     echo ${#result}
 166 }
 167 
 168 #:docstring strstr:
 169 # Usage: strstr s1 s2
 170 # 
 171 # Strstr echoes a substring starting at the first occurrence of string s2 in
 172 # string s1, or nothing if s2 does not occur in the string.  If s2 points to
 173 # a string of zero length, strstr echoes s1.
 174 #:end docstring:
 175 
 176 ###;;;autoload
 177 function strstr ()
 178 {
 179     # if s2 points to a string of zero length, strstr echoes s1
 180     [ ${#2} -eq 0 ] && { echo "$1" ; return 0; }
 181 
 182     # strstr echoes nothing if s2 does not occur in s1
 183     case "$1" in
 184     *$2*) ;;
 185     *) return 1;;
 186     esac
 187 
 188     # use the pattern matching code to strip off the match and everything
 189     # following it
 190     first=${1/$2*/}
 191 
 192     # then strip off the first unmatched portion of the string
 193     echo "${1##$first}"
 194 }
 195 
 196 #:docstring strtok:
 197 # Usage: strtok s1 s2
 198 #
 199 # Strtok considers the string s1 to consist of a sequence of zero or more
 200 # text tokens separated by spans of one or more characters from the
 201 # separator string s2.  The first call (with a non-empty string s1
 202 # specified) echoes a string consisting of the first token on stdout. The
 203 # function keeps track of its position in the string s1 between separate
 204 # calls, so that subsequent calls made with the first argument an empty
 205 # string will work through the string immediately following that token.  In
 206 # this way subsequent calls will work through the string s1 until no tokens
 207 # remain.  The separator string s2 may be different from call to call.
 208 # When no token remains in s1, an empty value is echoed on stdout.
 209 #:end docstring:
 210 
 211 ###;;;autoload
 212 function strtok ()
 213 {
 214  :
 215 }
 216 
 217 #:docstring strtrunc:
 218 # Usage: strtrunc $n $s1 {$s2} {$...}
 219 #
 220 # Used by many functions like strncmp to truncate arguments for comparison.
 221 # Echoes the first n characters of each string s1 s2 ... on stdout. 
 222 #:end docstring:
 223 
 224 ###;;;autoload
 225 function strtrunc ()
 226 {
 227     n=$1 ; shift
 228     for z; do
 229         echo "${z:0:$n}"
 230     done
 231 }
 232 
 233 # provide string
 234 
 235 # string.bash ends here
 236 
 237 
 238 # ========================================================================== #
 239 # ==> Everything below here added by the document author.
 240 
 241 # ==> Suggested use of this script is to delete everything below here,
 242 # ==> and "source" this file into your own scripts.
 243 
 244 # strcat
 245 string0=one
 246 string1=two
 247 echo
 248 echo "Testing \"strcat\" function:"
 249 echo "Original \"string0\" = $string0"
 250 echo "\"string1\" = $string1"
 251 strcat string0 string1
 252 echo "New \"string0\" = $string0"
 253 echo
 254 
 255 # strlen
 256 echo
 257 echo "Testing \"strlen\" function:"
 258 str=123456789
 259 echo "\"str\" = $str"
 260 echo -n "Length of \"str\" = "
 261 strlen str
 262 echo
 263 
 264 
 265 
 266 # Exercise for reader:
 267 # Add code to test all the other string functions above.
 268 
 269 
 270 exit 0

Stephane Chazelas demonstrates object-oriented programming a Bash script.


Example A-14. Object-oriented database

   1 #!/bin/bash
   2 # obj-oriented.sh: Object-oriented programming in a shell script.
   3 # Script by Stephane Chazelas.
   4 
   5 
   6 person.new()        # Looks almost like a class declaration in C++.
   7 {
   8   local obj_name=$1 name=$2 firstname=$3 birthdate=$4
   9 
  10   eval "$obj_name.set_name() {
  11           eval \"$obj_name.get_name() {
  12                    echo \$1
  13                  }\"
  14         }"
  15 
  16   eval "$obj_name.set_firstname() {
  17           eval \"$obj_name.get_firstname() {
  18                    echo \$1
  19                  }\"
  20         }"
  21 
  22   eval "$obj_name.set_birthdate() {
  23           eval \"$obj_name.get_birthdate() {
  24             echo \$1
  25           }\"
  26           eval \"$obj_name.show_birthdate() {
  27             echo \$(date -d \"1/1/1970 0:0:\$1 GMT\")
  28           }\"
  29           eval \"$obj_name.get_age() {
  30             echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 ))
  31           }\"
  32         }"
  33 
  34   $obj_name.set_name $name
  35   $obj_name.set_firstname $firstname
  36   $obj_name.set_birthdate $birthdate
  37 }
  38 
  39 echo
  40 
  41 person.new self Bozeman Bozo 101272413
  42 # Create an instance of "person.new" (actually passing args to the function).
  43 
  44 self.get_firstname       #   Bozo
  45 self.get_name            #   Bozeman
  46 self.get_age             #   28
  47 self.get_birthdate       #   101272413
  48 self.show_birthdate      #   Sat Mar 17 20:13:33 MST 1973
  49 
  50 echo
  51 
  52 # typeset -f
  53 # to see the created functions (careful, it scrolls off the page).
  54 
  55 exit 0