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. mailformat: Formatting an e-mail message

   1 #!/bin/bash
   2 # mail-format.sh (ver. 1.1): Format e-mail messages.
   3 
   4 # Gets rid of carets, tabs, and also folds excessively long lines.
   5 
   6 # =================================================================
   7 #                 Standard Check for Script Argument(s)
   8 ARGS=1
   9 E_BADARGS=65
  10 E_NOFILE=66
  11 
  12 if [ $# -ne $ARGS ]  # Correct number of arguments passed to script?
  13 then
  14   echo "Usage: `basename $0` filename"
  15   exit $E_BADARGS
  16 fi
  17 
  18 if [ -f "$1" ]       # Check if file exists.
  19 then
  20     file_name=$1
  21 else
  22     echo "File \"$1\" does not exist."
  23     exit $E_NOFILE
  24 fi
  25 # =================================================================
  26 
  27 MAXWIDTH=70          # Width to fold excessively long lines to.
  28 
  29 # ---------------------------------
  30 # A variable can hold a sed script.
  31 sedscript='s/^>//
  32 s/^  *>//
  33 s/^  *//
  34 s/		*//'
  35 # ---------------------------------
  36 
  37 #  Delete carets and tabs at beginning of lines,
  38 #+ then fold lines to $MAXWIDTH characters.
  39 sed "$sedscript" $1 | fold -s --width=$MAXWIDTH
  40                         #  -s option to "fold"
  41                         #+ breaks lines at whitespace, if possible.
  42 
  43 
  44 #  This script was inspired by an article in a well-known trade journal
  45 #+ extolling a 164K MS Windows utility with similar functionality.
  46 #
  47 #  An nice set of text processing utilities and an efficient
  48 #+ scripting language provide an alternative to bloated executables.
  49 
  50 exit 0


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

This script is a modification of Example 12-19.

   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 # Exercises:
  45 # ---------
  46 # What type of files will this not work on?
  47 # How can this be fixed?
  48 #
  49 #  Rewrite this script to process all the files in a directory
  50 #+ containing spaces in their names, and to rename them,
  51 #+ substituting an underscore for each space.


Example A-3. blank-rename: renames filenames containing blanks

This is an even simpler-minded version of previous script.

   1 #! /bin/bash
   2 # blank-rename.sh
   3 #
   4 # Substitutes underscores for blanks in all the filenames in a directory.
   5 
   6 ONE=1                     # For getting singular/plural right (see below).
   7 number=0                  # Keeps track of how many files actually renamed.
   8 FOUND=0                   # Successful return value.
   9 
  10 for filename in *         #Traverse all files in directory.
  11 do
  12      echo "$filename" | grep -q " "         #  Check whether filename
  13      if [ $? -eq $FOUND ]                   #+ contains space(s).
  14      then
  15        fname=$filename                      # Strip off path.
  16        n=`echo $fname | sed -e "s/ /_/g"`   # Substitute underscore for blank.
  17        mv "$fname" "$n"                     # Do the actual renaming.
  18        let "number += 1"
  19      fi
  20 done   
  21 
  22 if [ "$number" -eq "$ONE" ]                 # For correct grammar.
  23 then
  24  echo "$number file renamed."
  25 else 
  26  echo "$number files renamed."
  27 fi 
  28 
  29 exit 0


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 rather 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://ibiblio.org/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 # Note that "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 DEVICE=cdrom
  10 # DEVICE="0,0"    on older versions of cdrecord.
  11 
  12 echo; echo "Insert source CD, but do *not* mount it."
  13 echo "Press ENTER when ready. "
  14 read ready                                 # Wait for input, $ready not used.
  15 
  16 echo; echo "Copying the source CD to $OF."
  17 echo "This may take a while. Please be patient."
  18 
  19 dd if=$CDROM of=$OF bs=$BLOCKSIZE          # Raw device copy.
  20 
  21 
  22 echo; echo "Remove data CD."
  23 echo "Insert blank CDR."
  24 echo "Press ENTER when ready. "
  25 read ready                                 # Wait for input, $ready not used.
  26 
  27 echo "Copying $OF to CDR."
  28 
  29 cdrecord -v -isosize speed=$SPEED dev=$DEVICE $OF
  30 # Uses Joerg Schilling's "cdrecord" package (see its docs).
  31 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html
  32 
  33 
  34 echo; echo "Done copying $OF to CDR on device $CDROM."
  35 
  36 echo "Do you want to erase the image file (y/n)? "  # Probably a huge file.
  37 read answer
  38 
  39 case "$answer" in
  40 [yY]) rm -f $OF
  41       echo "$OF erased."
  42       ;;
  43 *)    echo "$OF not erased.";;
  44 esac
  45 
  46 echo
  47 
  48 # Exercise:
  49 # Change the above "case" statement to also accept "yes" and "Yes" as input.
  50 
  51 exit 0


Example A-6. Collatz series

   1 #!/bin/bash
   2 # collatz.sh
   3 
   4 #  The notorious "hailstone" or Collatz series.
   5 #  -------------------------------------------
   6 #  1) Get the integer "seed" from the command line.
   7 #  2) NUMBER <--- seed
   8 #  3) Print NUMBER.
   9 #  4)  If NUMBER is even, divide by 2, or
  10 #  5)+ if odd, multiply by 3 and add 1.
  11 #  6) NUMBER <--- result 
  12 #  7) Loop back to step 3 (for specified number of iterations).
  13 #
  14 #  The theory is that every sequence,
  15 #+ no matter how large the initial value,
  16 #+ eventually settles down to repeating "4,2,1..." cycles,
  17 #+ even after fluctuating through a wide range of values.
  18 #
  19 #  This is an instance of an "iterate",
  20 #+ an operation that feeds its output back into the input.
  21 #  Sometimes the result is a "chaotic" series.
  22 
  23 
  24 MAX_ITERATIONS=200
  25 # For large seed numbers (>32000), increase MAX_ITERATIONS.
  26 
  27 h=${1:-$$}                      #  Seed
  28                                 #  Use $PID as seed,
  29                                 #+ if not specified as command-line arg.
  30 
  31 echo
  32 echo "C($h) --- $MAX_ITERATIONS Iterations"
  33 echo
  34 
  35 for ((i=1; i<=MAX_ITERATIONS; i++))
  36 do
  37 
  38 echo -n "$h	"
  39 #          ^^^^^
  40 #           tab
  41 
  42   let "remainder = h % 2"
  43   if [ "$remainder" -eq 0 ]   # Even?
  44   then
  45     let "h /= 2"              # Divide by 2.
  46   else
  47     let "h = h*3 + 1"         # Multiply by 3 and add 1.
  48   fi
  49 
  50 
  51 COLUMNS=10                    # Output 10 values per line.
  52 let "line_break = i % $COLUMNS"
  53 if [ "$line_break" -eq 0 ]
  54 then
  55   echo
  56 fi  
  57 
  58 done
  59 
  60 echo
  61 
  62 #  For more information on this mathematical function,
  63 #+ see "Computers, Pattern, Chaos, and Beauty", by Pickover, p. 185 ff.,
  64 #+ as listed in the bibliography.
  65 
  66 exit 0


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


Example A-8. Make a "dictionary"

   1 #!/bin/bash
   2 # makedict.sh  [make dictionary]
   3 
   4 # Modification of /usr/sbin/mkdict script.
   5 # Original script copyright 1993, by Alec Muffett.
   6 #
   7 #  This modified script included in this document in a manner
   8 #+ consistent with the "LICENSE" document of the "Crack" package
   9 #+ that the original script is a part of.
  10 
  11 #  This script processes text files to produce a sorted list
  12 #+ of words found in the files.
  13 #  This may be useful for compiling dictionaries
  14 #+ and for lexicographic research.
  15 
  16 
  17 E_BADARGS=65
  18 
  19 if [ ! -r "$1" ]                     #  Need at least one
  20 then                                 #+ valid file argument.
  21   echo "Usage: $0 files-to-process"
  22   exit $E_BADARGS
  23 fi  
  24 
  25 
  26 # SORT="sort"                        #  No longer necessary to define options
  27                                      #+ to sort. Changed from original script.
  28 
  29 cat $* |                             # Contents of specified files to stdout.
  30         tr A-Z a-z |                 # Convert to lowercase.
  31         tr ' ' '\012' |              # New: change spaces to newlines.
  32 #       tr -cd '\012[a-z][0-9]' |    #  Get rid of everything non-alphanumeric
  33                                      #+ (original script).
  34         tr -c '\012a-z'  '\012' |    #  Rather than deleting
  35                                      #+ now change non-alpha to newlines.
  36         sort |                       # $SORT options unnecessary now.
  37         uniq |                       # Remove duplicates.
  38         grep -v '^#' |               # Delete lines beginning with a hashmark.
  39         grep -v '^$'                 # Delete blank lines.
  40 
  41 exit 0	


Example A-9. Soundex conversion

   1 #!/bin/bash
   2 # soundex.sh: Calculate "soundex" code for names
   3 
   4 # =======================================================
   5 #        Soundex script
   6 #              by
   7 #         Mendel Cooper
   8 #     thegrendel@theriver.com
   9 #       23 January, 2002
  10 #
  11 #   Placed in the Public Domain.
  12 #
  13 # A slightly different version of this script appeared in
  14 #+ Ed Schaefer's July, 2002 "Shell Corner" column
  15 #+ in "Unix Review" on-line,
  16 #+ http://www.unixreview.com/documents/uni1026336632258/
  17 # =======================================================
  18 
  19 
  20 ARGCOUNT=1                     # Need name as argument.
  21 E_WRONGARGS=70
  22 
  23 if [ $# -ne "$ARGCOUNT" ]
  24 then
  25   echo "Usage: `basename $0` name"
  26   exit $E_WRONGARGS
  27 fi  
  28 
  29 
  30 assign_value ()                #  Assigns numerical value
  31 {                              #+ to letters of name.
  32 
  33   val1=bfpv                    # 'b,f,p,v' = 1
  34   val2=cgjkqsxz                # 'c,g,j,k,q,s,x,z' = 2
  35   val3=dt                      #  etc.
  36   val4=l
  37   val5=mn
  38   val6=r
  39 
  40 # Exceptionally clever use of 'tr' follows.
  41 # Try to figure out what is going on here.
  42 
  43 value=$( echo "$1" \
  44 | tr -d wh \
  45 | tr $val1 1 | tr $val2 2 | tr $val3 3 \
  46 | tr $val4 4 | tr $val5 5 | tr $val6 6 \
  47 | tr -s 123456 \
  48 | tr -d aeiouy )
  49 
  50 # Assign letter values.
  51 # Remove duplicate numbers, except when separated by vowels.
  52 # Ignore vowels, except as separators, so delete them last.
  53 # Ignore 'w' and 'h', even as separators, so delete them first.
  54 #
  55 # The above command substitution lays more pipe than a plumber <g>.
  56 
  57 }  
  58 
  59 
  60 input_name="$1"
  61 echo
  62 echo "Name = $input_name"
  63 
  64 
  65 # Change all characters of name input to lowercase.
  66 # ------------------------------------------------
  67 name=$( echo $input_name | tr A-Z a-z )
  68 # ------------------------------------------------
  69 # Just in case argument to script is mixed case.
  70 
  71 
  72 # Prefix of soundex code: first letter of name.
  73 # --------------------------------------------
  74 
  75 
  76 char_pos=0                     # Initialize character position. 
  77 prefix0=${name:$char_pos:1}
  78 prefix=`echo $prefix0 | tr a-z A-Z`
  79                                # Uppercase 1st letter of soundex.
  80 
  81 let "char_pos += 1"            # Bump character position to 2nd letter of name.
  82 name1=${name:$char_pos}
  83 
  84 
  85 # ++++++++++++++++++++++++++ Exception Patch +++++++++++++++++++++++++++++++++
  86 #  Now, we run both the input name and the name shifted one char to the right
  87 #+ through the value-assigning function.
  88 #  If we get the same value out, that means that the first two characters
  89 #+ of the name have the same value assigned, and that one should cancel.
  90 #  However, we also need to test whether the first letter of the name
  91 #+ is a vowel or 'w' or 'h', because otherwise this would bollix things up.
  92 
  93 char1=`echo $prefix | tr A-Z a-z`    # First letter of name, lowercased.
  94 
  95 assign_value $name
  96 s1=$value
  97 assign_value $name1
  98 s2=$value
  99 assign_value $char1
 100 s3=$value
 101 s3=9$s3                              #  If first letter of name is a vowel
 102                                      #+ or 'w' or 'h',
 103                                      #+ then its "value" will be null (unset).
 104 				     #+ Therefore, set it to 9, an otherwise
 105 				     #+ unused value, which can be tested for.
 106 
 107 
 108 if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]]
 109 then
 110   suffix=$s2
 111 else  
 112   suffix=${s2:$char_pos}
 113 fi  
 114 # ++++++++++++++++++++++ end Exception Patch +++++++++++++++++++++++++++++++++
 115 
 116 
 117 padding=000                    # Use at most 3 zeroes to pad.
 118 
 119 
 120 soun=$prefix$suffix$padding    # Pad with zeroes.
 121 
 122 MAXLEN=4                       # Truncate to maximum of 4 chars.
 123 soundex=${soun:0:$MAXLEN}
 124 
 125 echo "Soundex = $soundex"
 126 
 127 echo
 128 
 129 #  The soundex code is a method of indexing and classifying names
 130 #+ by grouping together the ones that sound alike.
 131 #  The soundex code for a given name is the first letter of the name,
 132 #+ followed by a calculated three-number code.
 133 #  Similar sounding names should have almost the same soundex codes.
 134 
 135 #   Examples:
 136 #   Smith and Smythe both have a "S-530" soundex.
 137 #   Harrison = H-625
 138 #   Hargison = H-622
 139 #   Harriman = H-655
 140 
 141 #  This works out fairly well in practice, but there are numerous anomalies.
 142 #
 143 #
 144 #  The U.S. Census and certain other governmental agencies use soundex,
 145 #  as do genealogical researchers.
 146 #
 147 #  For more information,
 148 #+ see the "National Archives and Records Administration home page",
 149 #+ http://www.nara.gov/genealogy/soundex/soundex.html
 150 
 151 
 152 
 153 # Exercise:
 154 # --------
 155 # Simplify the "Exception Patch" section of this script.
 156 
 157 exit 0


Example A-10. "Game of Life"

   1 #!/bin/bash
   2 # life.sh: "Life in the Slow Lane"
   3 # Version 2: Patched by Daniel Albers
   4 #+           to allow non-square grids as input.
   5 
   6 # ##################################################################### #
   7 # This is the Bash script version of John Conway's "Game of Life".      #
   8 # "Life" is a simple implementation of cellular automata.               #
   9 # --------------------------------------------------------------------- #
  10 # On a rectangular grid, let each "cell" be either "living" or "dead".  #
  11 # Designate a living cell with a dot, and a dead one with a blank space.#
  12 #  Begin with an arbitrarily drawn dot-and-blank grid,                  #
  13 #+ and let this be the starting generation, "generation 0".             #
  14 # Determine each successive generation by the following rules:          #
  15 # 1) Each cell has 8 neighbors, the adjoining cells                     #
  16 #+   left, right, top, bottom, and the 4 diagonals.                     #
  17 #                       123                                             #
  18 #                       4*5                                             #
  19 #                       678                                             #
  20 #                                                                       #
  21 # 2) A living cell with either 2 or 3 living neighbors remains alive.   #
  22 # 3) A dead cell with 3 living neighbors becomes alive (a "birth").     #
  23 SURVIVE=2                                                               #
  24 BIRTH=3                                                                 #
  25 # 4) All other cases result in a dead cell for the next generation.     #
  26 # ##################################################################### #
  27 
  28 
  29 startfile=gen0   # Read the starting generation from the file "gen0".
  30                  # Default, if no other file specified when invoking script.
  31                  #
  32 if [ -n "$1" ]   # Specify another "generation 0" file.
  33 then
  34   if [ -e "$1" ] # Check for existence.
  35   then
  36     startfile="$1"
  37   fi  
  38 fi  
  39 
  40 
  41 ALIVE1=.
  42 DEAD1=_
  43                  # Represent living and "dead" cells in the start-up file.
  44 
  45 #  ---------------------------------------------------------- #
  46 #  This script uses a 10 x 10 grid (may be increased,
  47 #+ but a large grid will will cause very slow execution).
  48 ROWS=10
  49 COLS=10
  50 #  Change above two variables to match grid size, if necessary.
  51 #  ---------------------------------------------------------- #
  52 
  53 GENERATIONS=10          #  How many generations to cycle through.
  54                         #  Adjust this upwards,
  55                         #+ if you have time on your hands.
  56 
  57 NONE_ALIVE=80           #  Exit status on premature bailout,
  58                         #+ if no cells left alive.
  59 TRUE=0
  60 FALSE=1
  61 ALIVE=0
  62 DEAD=1
  63 
  64 avar=                   # Global; holds current generation.
  65 generation=0            # Initialize generation count.
  66 
  67 # =================================================================
  68 
  69 
  70 let "cells = $ROWS * $COLS"
  71                         # How many cells.
  72 
  73 declare -a initial      # Arrays containing "cells".
  74 declare -a current
  75 
  76 display ()
  77 {
  78 
  79 alive=0                 # How many cells "alive" at any given time.
  80                         # Initially zero.
  81 
  82 declare -a arr
  83 arr=( `echo "$1"` )     # Convert passed arg to array.
  84 
  85 element_count=${#arr[*]}
  86 
  87 local i
  88 local rowcheck
  89 
  90 for ((i=0; i<$element_count; i++))
  91 do
  92 
  93   # Insert newline at end of each row.
  94   let "rowcheck = $i % COLS"
  95   if [ "$rowcheck" -eq 0 ]
  96   then
  97     echo                # Newline.
  98     echo -n "      "    # Indent.
  99   fi  
 100 
 101   cell=${arr[i]}
 102 
 103   if [ "$cell" = . ]
 104   then
 105     let "alive += 1"
 106   fi  
 107 
 108   echo -n "$cell" | sed -e 's/_/ /g'
 109   # Print out array and change underscores to spaces.
 110 done  
 111 
 112 return
 113 
 114 }
 115 
 116 IsValid ()                            # Test whether cell coordinate valid.
 117 {
 118 
 119   if [ -z "$1"  -o -z "$2" ]          # Mandatory arguments missing?
 120   then
 121     return $FALSE
 122   fi
 123 
 124 local row
 125 local lower_limit=0                   # Disallow negative coordinate.
 126 local upper_limit
 127 local left
 128 local right
 129 
 130 let "upper_limit = $ROWS * $COLS - 1" # Total number of cells.
 131 
 132 
 133 if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ]
 134 then
 135   return $FALSE                       # Out of array bounds.
 136 fi  
 137 
 138 row=$2
 139 let "left = $row * $COLS"             # Left limit.
 140 let "right = $left + $COLS - 1"       # Right limit.
 141 
 142 if [ "$1" -lt "$left" -o "$1" -gt "$right" ]
 143 then
 144   return $FALSE                       # Beyond row boundary.
 145 fi  
 146 
 147 return $TRUE                          # Valid coordinate.
 148 
 149 }  
 150 
 151 
 152 IsAlive ()              # Test whether cell is alive.
 153                         # Takes array, cell number, state of cell as arguments.
 154 {
 155   GetCount "$1" $2      # Get alive cell count in neighborhood.
 156   local nhbd=$?
 157 
 158 
 159   if [ "$nhbd" -eq "$BIRTH" ]  # Alive in any case.
 160   then
 161     return $ALIVE
 162   fi
 163 
 164   if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ]
 165   then                  # Alive only if previously alive.
 166     return $ALIVE
 167   fi  
 168 
 169   return $DEAD          # Default.
 170 
 171 }  
 172 
 173 
 174 GetCount ()             # Count live cells in passed cell's neighborhood.
 175                         # Two arguments needed:
 176 			# $1) variable holding array
 177 			# $2) cell number
 178 {
 179   local cell_number=$2
 180   local array
 181   local top
 182   local center
 183   local bottom
 184   local r
 185   local row
 186   local i
 187   local t_top
 188   local t_cen
 189   local t_bot
 190   local count=0
 191   local ROW_NHBD=3
 192 
 193   array=( `echo "$1"` )
 194 
 195   let "top = $cell_number - $COLS - 1"    # Set up cell neighborhood.
 196   let "center = $cell_number - 1"
 197   let "bottom = $cell_number + $COLS - 1"
 198   let "r = $cell_number / $COLS"
 199 
 200   for ((i=0; i<$ROW_NHBD; i++))           # Traverse from left to right. 
 201   do
 202     let "t_top = $top + $i"
 203     let "t_cen = $center + $i"
 204     let "t_bot = $bottom + $i"
 205 
 206 
 207     let "row = $r"                        # Count center row of neighborhood.
 208     IsValid $t_cen $row                   # Valid cell position?
 209     if [ $? -eq "$TRUE" ]
 210     then
 211       if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive?
 212       then                                # Yes?
 213         let "count += 1"                  # Increment count.
 214       fi	
 215     fi  
 216 
 217     let "row = $r - 1"                    # Count top row.          
 218     IsValid $t_top $row
 219     if [ $? -eq "$TRUE" ]
 220     then
 221       if [ ${array[$t_top]} = "$ALIVE1" ] 
 222       then
 223         let "count += 1"
 224       fi	
 225     fi  
 226 
 227     let "row = $r + 1"                    # Count bottom row.
 228     IsValid $t_bot $row
 229     if [ $? -eq "$TRUE" ]
 230     then
 231       if [ ${array[$t_bot]} = "$ALIVE1" ] 
 232       then
 233         let "count += 1"
 234       fi	
 235     fi  
 236 
 237   done  
 238 
 239 
 240   if [ ${array[$cell_number]} = "$ALIVE1" ]
 241   then
 242     let "count -= 1"        #  Make sure value of tested cell itself
 243   fi                        #+ is not counted.
 244 
 245 
 246   return $count
 247   
 248 }
 249 
 250 next_gen ()               # Update generation array.
 251 {
 252 
 253 local array
 254 local i=0
 255 
 256 array=( `echo "$1"` )     # Convert passed arg to array.
 257 
 258 while [ "$i" -lt "$cells" ]
 259 do
 260   IsAlive "$1" $i ${array[$i]}   # Is cell alive?
 261   if [ $? -eq "$ALIVE" ]
 262   then                           #  If alive, then
 263     array[$i]=.                  #+ represent the cell as a period.
 264   else  
 265     array[$i]="_"                #  Otherwise underscore
 266    fi                            #+ (which will later be converted to space).  
 267   let "i += 1" 
 268 done   
 269 
 270 
 271 # let "generation += 1"   # Increment generation count.
 272 # Why was the above line commented out?
 273 
 274 
 275 # Set variable to pass as parameter to "display" function.
 276 avar=`echo ${array[@]}`   # Convert array back to string variable.
 277 display "$avar"           # Display it.
 278 echo; echo
 279 echo "Generation $generation  -  $alive alive"
 280 
 281 if [ "$alive" -eq 0 ]
 282 then
 283   echo
 284   echo "Premature exit: no more cells alive!"
 285   exit $NONE_ALIVE        #  No point in continuing
 286 fi                        #+ if no live cells.
 287 
 288 }
 289 
 290 
 291 # =========================================================
 292 
 293 # main ()
 294 
 295 # Load initial array with contents of startup file.
 296 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\
 297 sed -e 's/\./\. /g' -e 's/_/_ /g'` )
 298 # Delete lines containing '#' comment character.
 299 # Remove linefeeds and insert space between elements.
 300 
 301 clear          # Clear screen.
 302 
 303 echo #         Title
 304 echo "======================="
 305 echo "    $GENERATIONS generations"
 306 echo "           of"
 307 echo "\"Life in the Slow Lane\""
 308 echo "======================="
 309 
 310 
 311 # -------- Display first generation. --------
 312 Gen0=`echo ${initial[@]}`
 313 display "$Gen0"           # Display only.
 314 echo; echo
 315 echo "Generation $generation  -  $alive alive"
 316 # -------------------------------------------
 317 
 318 
 319 let "generation += 1"     # Increment generation count.
 320 echo
 321 
 322 # ------- Display second generation. -------
 323 Cur=`echo ${initial[@]}`
 324 next_gen "$Cur"          # Update & display.
 325 # ------------------------------------------
 326 
 327 let "generation += 1"     # Increment generation count.
 328 
 329 # ------ Main loop for displaying subsequent generations ------
 330 while [ "$generation" -le "$GENERATIONS" ]
 331 do
 332   Cur="$avar"
 333   next_gen "$Cur"
 334   let "generation += 1"
 335 done
 336 # ==============================================================
 337 
 338 echo
 339 
 340 exit 0
 341 
 342 # --------------------------------------------------------------
 343 
 344 # The grid in this script has a "boundary problem."
 345 # The the top, bottom, and sides border on a void of dead cells.
 346 # Exercise: Change the script to have the grid wrap around,
 347 # +         so that the left and right sides will "touch,"      
 348 # +         as will the top and bottom.
 349 #
 350 # Exercise: Create a new "gen0" file to seed this script.
 351 #           Use a 12 x 16 grid, instead of the original 10 x 10 one.
 352 #           Make the necessary changes to the script,
 353 #+          so it will run with the altered file.
 354 #
 355 # Exercise: Modify this script so that it can determine the grid size
 356 #+          from the "gen0" file, and set any variables necessary
 357 #+          for the script to run.
 358 #           This would make unnecessary any changes to variables
 359 #+          in the script for an altered grid size.


Example A-11. Data file for "Game of Life"

   1 # This is an example "generation 0" start-up file for "life.sh".
   2 # --------------------------------------------------------------
   3 #  The "gen0" file is a 10 x 10 grid using a period (.) for live cells,
   4 #+ and an underscore (_) for dead ones. We cannot simply use spaces
   5 #+ for dead cells in this file because of a peculiarity in Bash arrays.
   6 #  [Exercise for the reader: explain this.]
   7 #
   8 # Lines beginning with a '#' are comments, and the script ignores them.
   9 __.__..___
  10 ___._.____
  11 ____.___..
  12 _._______.
  13 ____._____
  14 ..__...___
  15 ____._____
  16 ___...____
  17 __.._..___
  18 _..___..__

+++

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-12. 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: 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-13. 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 # ==> This script is nothing but a shell wrapper around "ftp" . . .
   6 # Simple, and quick - written as a companion to ftplist 
   7 # -h specifies the remote host (default prep.ai.mit.edu) 
   8 # -d specifies the remote directory to cd to - you can provide a sequence 
   9 # of -d options - they will be cd'ed to in turn. If the paths are relative, 
  10 # make sure you get the sequence right. Be careful with relative paths - 
  11 # there are far too many symlinks nowadays.  
  12 # (default is the ftp login directory)
  13 # -v turns on the verbose option of ftp, and shows all responses from the 
  14 # ftp server.  
  15 # -f remotefile[:localfile] gets the remote file into localfile 
  16 # -m pattern does an mget with the specified pattern. Remember to quote 
  17 # shell characters.  
  18 # -c does a local cd to the specified directory
  19 # For example, 
  20 # 	ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \
  21 #		-d ../pub/R3/fixes -c ~/fixes -m 'fix*' 
  22 # will get xplaces.shar from ~ftp/contrib on expo.lcs.mit.edu, and put it in
  23 # xplaces.sh in the current working directory, and get all fixes from
  24 # ~ftp/pub/R3/fixes and put them in the ~/fixes directory. 
  25 # Obviously, the sequence of the options is important, since the equivalent
  26 # commands are executed by ftp in corresponding order
  27 #
  28 # Mark Moraes <moraes@csri.toronto.edu>, Feb 1, 1989 
  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 E_BADARGS=65
  39 
  40 TMPFILE=/tmp/ftp.$$
  41 # ==> Creates temp file, using process id of script ($$)
  42 # ==> to construct filename.
  43 
  44 SITE=`domainname`.toronto.edu
  45 # ==> 'domainname' similar to 'hostname'
  46 # ==> May rewrite this to parameterize this for general use.
  47 
  48 usage="Usage: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \
  49 		[-c localdirectory] [-m filepattern] [-v]"
  50 ftpflags="-i -n"
  51 verbflag=
  52 set -f 		# So we can use globbing in -m
  53 set x `getopt vh:d:c:m:f: $*`
  54 if [ $? != 0 ]; then
  55 	echo $usage
  56 	exit $E_BADARGS
  57 fi
  58 shift
  59 trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15
  60 # ==> Delete tempfile in case of abnormal exit from script.
  61 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}"
  62 # ==> Added quotes (recommended in complex echoes).
  63 echo binary >> ${TMPFILE}
  64 for i in $*   # ==> Parse command line args.
  65 do
  66 	case $i in
  67 	-v) verbflag=-v; echo hash >> ${TMPFILE}; shift;;
  68 	-h) remhost=$2; shift 2;;
  69 	-d) echo cd $2 >> ${TMPFILE}; 
  70 	    if [ x${verbflag} != x ]; then
  71 	        echo pwd >> ${TMPFILE};
  72 	    fi;
  73 	    shift 2;;
  74 	-c) echo lcd $2 >> ${TMPFILE}; shift 2;;
  75 	-m) echo mget "$2" >> ${TMPFILE}; shift 2;;
  76 	-f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
  77 	    echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;;
  78 	--) shift; break;;
  79 	esac
  80         # ==> 'lcd' and 'mget' are ftp commands. See "man ftp" . . .
  81 done
  82 if [ $# -ne 0 ]; then
  83 	echo $usage
  84 	exit $E_BADARGS
  85         # ==> Changed from "exit 2" to conform with style standard.
  86 fi
  87 if [ x${verbflag} != x ]; then
  88 	ftpflags="${ftpflags} -v"
  89 fi
  90 if [ x${remhost} = x ]; then
  91 	remhost=prep.ai.mit.edu
  92 	# ==> Change to match appropriate ftp site.
  93 fi
  94 echo quit >> ${TMPFILE}
  95 # ==> All commands saved in tempfile.
  96 
  97 ftp ${ftpflags} ${remhost} < ${TMPFILE}
  98 # ==> Now, tempfile batch processed by ftp.
  99 
 100 rm -f ${TMPFILE}
 101 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile).
 102 
 103 
 104 # ==> Exercises:
 105 # ==> ---------
 106 # ==> 1) Add error checking.
 107 # ==> 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-14. 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 # ==> Password will consist of alphanumeric characters.
  12 LENGTH="8"
  13 # ==> May change 'LENGTH' for longer password.
  14 
  15 
  16 while [ "${n:=1}" -le "$LENGTH" ]
  17 # ==> Recall that := is "default substitution" operator.
  18 # ==> So, if 'n' has not been initialized, set it to 1.
  19 do
  20 	PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
  21 	# ==> Very clever, but tricky.
  22 
  23 	# ==> Starting from the innermost nesting...
  24 	# ==> ${#MATRIX} returns length of array MATRIX.
  25 
  26 	# ==> $RANDOM%${#MATRIX} returns random number between 1
  27 	# ==> and [length of MATRIX] - 1.
  28 
  29 	# ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1}
  30 	# ==> returns expansion of MATRIX at random position, by length 1. 
  31 	# ==> See {var:pos:len} parameter substitution in Chapter 9.
  32 	# ==> and the associated examples.
  33 
  34 	# ==> PASS=... simply pastes this result onto previous PASS (concatenation).
  35 
  36 	# ==> To visualize this more clearly, uncomment the following line
  37 	#                 echo "$PASS"
  38 	# ==> to see PASS being built up,
  39 	# ==> one character at a time, each iteration of the loop.
  40 
  41 	let n+=1
  42 	# ==> Increment 'n' for next pass.
  43 done
  44 
  45 echo "$PASS"      # ==> Or, redirect to a file, as desired.
  46 
  47 exit 0

+

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


Example A-15. 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

+

St