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 script is useful when writing man page source.
   5 #  It lets you look at the intermediate results on the fly
   6 #+ while working on it.
   7 
   8 E_WRONGARGS=65
   9 
  10 if [ -z "$1" ]
  11 then
  12   echo "Usage: `basename $0` filename"
  13   exit $E_WRONGARGS
  14 fi
  15 
  16 # ---------------------------
  17 groff -Tascii -man $1 | less
  18 # From the man page for groff.
  19 # ---------------------------
  20 
  21 #  If the man page includes tables and/or equations,
  22 #+ then the above code will barf.
  23 #  The following line can handle such cases.
  24 #
  25 #   gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
  26 #
  27 #   Thanks, S.C.
  28 
  29 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 # =================================================================
   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 long lines to.
  28 
  29 #  Delete carets and tabs at beginning of lines,
  30 #+ then fold lines to $MAXWIDTH characters.
  31 sed '
  32 s/^>//
  33 s/^  *>//
  34 s/^  *//
  35 s/		*//
  36 ' $1 | fold -s --width=$MAXWIDTH
  37           # -s option to "fold" breaks lines at whitespace, if possible.
  38 
  39 #  This script was inspired by an article in a well-known trade journal
  40 #+ extolling a 164K Windows utility with similar functionality.
  41 #
  42 #  An nice set of text processing utilities and an efficient
  43 #+ scripting language provide an alternative to bloated executables.
  44 
  45 exit 0


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

This script is a modification of Example 12-18.

   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-4. 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-5. 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 # "bell" rings 'bell' after each file transfer.
  45 
  46 exit 0


Example A-6. 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-7. 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-8. 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-9. 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-10. 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-11. "Game of Life"

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


Example A-12. 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-13. 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-14. 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:
 100 # ==> ---------
 101 # ==> 1) Add error checking.
 102 # ==> 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-15. 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-16. 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-17. Generating prime numbers using the modulo operator

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

+

This is Rick Boivie's revision of Jordi Sanfeliu's tree script.


Example A-18. tree: Displaying a directory tree

   1 #!/bin/bash
   2 # tree.sh
   3 
   4 #  Written by Rick Boivie.
   5 #  Used with permission.
   6 #  This is a revised and simplified version of a script
   7 #  by Jordi Sanfeliu (and patched by Ian Kjos).
   8 #  This script replaces the earlier version used in
   9 #+ previous releases of the Advanced Bash Scripting Guide.
  10 
  11 # ==> Comments added by the author of this document.
  12 
  13 
  14 search () {
  15 for dir in `echo *`
  16 #  ==> `echo *` lists all the files in current working directory,
  17 #+ ==> without line breaks.
  18 #  ==> Similar effect to for dir in *
  19 #  ==> but "dir in `echo *`" will not handle filenames with blanks.
  20 do
  21   if [ -d "$dir" ] ; then # ==> If it is a directory (-d)...
  22   zz=0                    # ==> Temp variable, keeping track of directory level.
  23   while [ $zz != $1 ]     # Keep track of inner nested loop.
  24     do
  25       echo -n "| "        # ==> Display vertical connector symbol,
  26                           # ==> with 2 spaces & no line feed in order to indent.
  27       zz=`expr $zz + 1`   # ==> Increment zz.
  28     done
  29 
  30     if [ -L "$dir" ] ; then # ==> If directory is a symbolic link...
  31       echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'`
  32       # ==> Display horiz. connector and list directory name, but...
  33       # ==> delete date/time part of long listing.
  34     else
  35       echo "+---$dir"       # ==> Display horizontal connector symbol...
  36       # ==> and print directory name.
  37       numdirs=`expr $numdirs + 1` # ==> Increment directory count.
  38       if cd "$dir" ; then # ==> If can move to subdirectory...
  39         search `expr $1 + 1` # with recursion ;-)
  40         # ==> Function calls itself.
  41         cd ..
  42       fi
  43     fi
  44   fi
  45 done
  46 }
  47 
  48 if [ $# != 0 ] ; then
  49   cd $1 # move to indicated directory.
  50   #else # stay in current directory
  51 fi
  52 
  53 echo "Initial directory = `pwd`"
  54 numdirs=0
  55 
  56 search 0
  57 echo "Total directories = $numdirs"
  58 
  59 exit 0

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


Example A-19. 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:
 267 # --------
 268 # Add code to test all the other string functions above.
 269 
 270 
 271 exit 0

Michael Zick's complex array example uses the md5sum check sum command to encode directory information.


Example A-20. Directory information

   1 #! /bin/bash
   2 # directory-info.sh
   3 # Parses and lists directory information.
   4 
   5 # NOTE: Change lines 273 and 353 per "README" file.
   6 
   7 # Michael Zick is the author of this script.
   8 # Used here with his permission.
   9 
  10 # Controls
  11 # If overridden by command arguments, they must be in the order:
  12 #   Arg1: "Descriptor Directory"
  13 #   Arg2: "Exclude Paths"
  14 #   Arg3: "Exclude Directories"
  15 #
  16 # Environment Settings override Defaults.
  17 # Command arguments override Environment Settings.
  18 
  19 # Default location for content addressed file descriptors.
  20 MD5UCFS=${1:-${MD5UCFS:-'/tmpfs/ucfs'}}
  21 
  22 # Directory paths never to list or enter
  23 declare -a \
  24   EXCLUDE_PATHS=${2:-${EXCLUDE_PATHS:-'(/proc /dev /devfs /tmpfs)'}}
  25 
  26 # Directories never to list or enter
  27 declare -a \
  28   EXCLUDE_DIRS=${3:-${EXCLUDE_DIRS:-'(ucfs lost+found tmp wtmp)'}}
  29 
  30 # Files never to list or enter
  31 declare -a \
  32   EXCLUDE_FILES=${3:-${EXCLUDE_FILES:-'(core "Name with Spaces")'}}
  33 
  34 
  35 # Here document used as a comment block.
  36 : << LSfieldsDoc
  37 # # # # # List Filesystem Directory Information # # # # #
  38 #
  39 #	ListDirectory "FileGlob" "Field-Array-Name"
  40 # or
  41 #	ListDirectory -of "FileGlob" "Field-Array-Filename"
  42 #	'-of' meaning 'output to filename'
  43 # # # # #
  44 
  45 String format description based on: ls (GNU fileutils) version 4.0.36
  46 
  47 Produces a line (or more) formatted:
  48 inode permissions hard-links owner group ...
  49 32736 -rw-------    1 mszick   mszick
  50 
  51 size    day month date hh:mm:ss year path
  52 2756608 Sun Apr 20 08:53:06 2003 /home/mszick/core
  53 
  54 Unless it is formatted:
  55 inode permissions hard-links owner group ...
  56 266705 crw-rw----    1    root  uucp
  57 
  58 major minor day month date hh:mm:ss year path
  59 4,  68 Sun Apr 20 09:27:33 2003 /dev/ttyS4
  60 NOTE: that pesky comma after the major number
  61 
  62 NOTE: the 'path' may be multiple fields:
  63 /home/mszick/core
  64 /proc/982/fd/0 -> /dev/null
  65 /proc/982/fd/1 -> /home/mszick/.xsession-errors
  66 /proc/982/fd/13 -> /tmp/tmpfZVVOCs (deleted)
  67 /proc/982/fd/7 -> /tmp/kde-mszick/ksycoca
  68 /proc/982/fd/8 -> socket:[11586]
  69 /proc/982/fd/9 -> pipe:[11588]
  70 
  71 If that isn't enough to keep your parser guessing,
  72 either or both of the path components may be relative:
  73 ../Built-Shared -> Built-Static
  74 ../linux-2.4.20.tar.bz2 -> ../../../SRCS/linux-2.4.20.tar.bz2
  75 
  76 The first character of the 11 (10?) character permissions field:
  77 's' Socket
  78 'd' Directory
  79 'b' Block device
  80 'c' Character device
  81 'l' Symbolic link
  82 NOTE: Hard links not marked - test for identical inode numbers
  83 on identical filesystems.
  84 All information about hard linked files are shared, except
  85 for the names and the name's location in the directory system.
  86 NOTE: A "Hard link" is known as a "File Alias" on some systems.
  87 '-' An undistingushed file
  88 
  89 Followed by three groups of letters for: User, Group, Others
  90 Character 1: '-' Not readable; 'r' Readable
  91 Character 2: '-' Not writable; 'w' Writable
  92 Character 3, User and Group: Combined execute and special
  93 '-' Not Executable, Not Special
  94 'x' Executable, Not Special
  95 's' Executable, Special
  96 'S' Not Executable, Special
  97 Character 3, Others: Combined execute and sticky (tacky?)
  98 '-' Not Executable, Not Tacky
  99 'x' Executable, Not Tacky
 100 't' Executable, Tacky
 101 'T' Not Executable, Tacky
 102 
 103 Followed by an access indicator
 104 Haven't tested this one, it may be the eleventh character
 105 or it may generate another field
 106 ' ' No alternate access
 107 '+' Alternate access
 108 LSfieldsDoc
 109 
 110 
 111 ListDirectory()
 112 {
 113 	local -a T
 114 	local -i of=0		# Default return in variable
 115 #	OLD_IFS=$IFS		# Using BASH default ' \t\n'
 116 
 117 	case "$#" in
 118 	3)	case "$1" in
 119 		-of)	of=1 ; shift ;;
 120 		 * )	return 1 ;;
 121 		esac ;;
 122 	2)	: ;;		# Poor man's "continue"
 123 	*)	return 1 ;;
 124 	esac
 125 
 126 	# NOTE: the (ls) command is NOT quoted (")
 127 	T=( $(ls --inode --ignore-backups --almost-all --directory \
 128 	--full-time --color=none --time=status --sort=none \
 129 	--format=long $1) )
 130 
 131 	case $of in
 132 	# Assign T back to the array whose name was passed as $2
 133 		0) eval $2=\( \"\$\{T\[@\]\}\" \) ;;
 134 	# Write T into filename passed as $2
 135 		1) echo "${T[@]}" > "$2" ;;
 136 	esac
 137 	return 0
 138    }
 139 
 140 # # # # # Is that string a legal number? # # # # #
 141 #
 142 #	IsNumber "Var"
 143 # # # # # There has to be a better way, sigh...
 144 
 145 IsNumber()
 146 {
 147 	local -i int
 148 	if [ $# -eq 0 ]
 149 	then
 150 		return 1
 151 	else
 152 		(let int=$1)  2>/dev/null
 153 		return $?	# Exit status of the let thread
 154 	fi
 155 }
 156 
 157 # # # # # Index Filesystem Directory Information # # # # #
 158 #
 159 #	IndexList "Field-Array-Name" "Index-Array-Name"
 160 # or
 161 #	IndexList -if Field-Array-Filename Index-Array-Name
 162 #	IndexList -of Field-Array-Name Index-Array-Filename
 163 #	IndexList -if -of Field-Array-Filename Index-Array-Filename
 164 # # # # #
 165 
 166 : << IndexListDoc
 167 Walk an array of directory fields produced by ListDirectory
 168 
 169 Having suppressed the line breaks in an otherwise line oriented
 170 report, build an index to the array element which starts each line.
 171 
 172 Each line gets two index entries, the first element of each line
 173 (inode) and the element that holds the pathname of the file.
 174 
 175 The first index entry pair (Line-Number==0) are informational:
 176 Index-Array-Name[0] : Number of "Lines" indexed
 177 Index-Array-Name[1] : "Current Line" pointer into Index-Array-Name
 178 
 179 The following index pairs (if any) hold element indexes into
 180 the Field-Array-Name per:
 181 Index-Array-Name[Line-Number * 2] : The "inode" field element.
 182 NOTE: This distance may be either +11 or +12 elements.
 183 Index-Array-Name[(Line-Number * 2) + 1] : The "pathname" element.
 184 NOTE: This distance may be a variable number of elements.
 185 Next line index pair for Line-Number+1.
 186 IndexListDoc
 187 
 188 
 189 
 190 IndexList()
 191 {
 192 	local -a LIST			# Local of listname passed
 193 	local -a -i INDEX=( 0 0 )	# Local of index to return
 194 	local -i Lidx Lcnt
 195 	local -i if=0 of=0		# Default to variable names
 196 
 197 	case "$#" in			# Simplistic option testing
 198 		0) return 1 ;;
 199 		1) return 1 ;;
 200 		2) : ;;			# Poor man's continue
 201 		3) case "$1" in
 202 			-if) if=1 ;;
 203 			-of) of=1 ;;
 204 			 * ) return 1 ;;
 205 		   esac ; shift ;;
 206 		4) if=1 ; of=1 ; shift ; shift ;;
 207 		*) return 1
 208 	esac
 209 
 210 	# Make local copy of list
 211 	case "$if" in
 212 		0) eval LIST=\( \"\$\{$1\[@\]\}\" \) ;;
 213 		1) LIST=( $(cat $1) ) ;;
 214 	esac
 215 
 216 	# Grok (grope?) the array
 217 	Lcnt=${#LIST[@]}
 218 	Lidx=0
 219 	until (( Lidx >= Lcnt ))
 220 	do
 221 	if IsNumber ${LIST[$Lidx]}
 222 	then
 223 		local -i inode name
 224 		local ft
 225 		inode=Lidx
 226 		local m=${LIST[$Lidx+2]}	# Hard Links field
 227 		ft=${LIST[$Lidx+1]:0:1} 	# Fast-Stat
 228 		case $ft in
 229 		b)	((Lidx+=12)) ;;		# Block device
 230 		c)	((Lidx+=12)) ;;		# Character device
 231 		*)	((Lidx+=11)) ;;		# Anything else
 232 		esac
 233 		name=Lidx
 234 		case $ft in
 235 		-)	((Lidx+=1)) ;;		# The easy one
 236 		b)	((Lidx+=1)) ;;		# Block device
 237 		c)	((Lidx+=1)) ;;		# Character device
 238 		d)	((Lidx+=1)) ;;		# The other easy one
 239 		l)	((Lidx+=3)) ;;		# At LEAST two more fields
 240 #  A little more elegance here would handle pipes,
 241 #+ sockets, deleted files - later.
 242 		*)	until IsNumber ${LIST[$Lidx]} || ((Lidx >= Lcnt))
 243 			do
 244 				((Lidx+=1))
 245 			done
 246 			;;			# Not required
 247 		esac
 248 		INDEX[${#INDEX[*]}]=$inode
 249 		INDEX[${#INDEX[*]}]=$name
 250 		INDEX[0]=${INDEX[0]}+1		# One more "line" found
 251 # echo "Line: ${INDEX[0]} Type: $ft Links: $m Inode: \
 252 # ${LIST[$inode]} Name: ${LIST[$name]}"
 253 
 254 	else
 255 		((Lidx+=1))
 256 	fi
 257 	done
 258 	case "$of" in
 259 		0) eval $2=\( \"\$\{INDEX\[@\]\}\" \) ;;
 260 		1) echo "${INDEX[@]}" > "$2" ;;
 261 	esac
 262 	return 0				# What could go wrong?
 263 }
 264 
 265 # # # # # Content Identify File # # # # #
 266 #
 267 #	DigestFile Input-Array-Name Digest-Array-Name
 268 # or
 269 #	DigestFile -if Input-FileName Digest-Array-Name
 270 # # # # #
 271 
 272 # Here document used as a comment block.
 273 : <<DigestFilesDoc
 274 
 275 The key (no pun intended) to a Unified Content File System (UCFS)
 276 is to distinguish the files in the system based on their content.
 277 Distinguishing files by their name is just, so, 20th Century.
 278 
 279 The content is distinguished by computing a checksum of that content.
 280 This version uses the md5sum program to generate a 128 bit checksum
 281 representative of the file's contents.
 282 There is a chance that two files having different content might
 283 generate the same checksum using md5sum (or any checksum).  Should
 284 that become a problem, then the use of md5sum can be replace by a
 285 cyrptographic signature.  But until then...
 286 
 287 The md5sum program is documented as outputting three fields (and it
 288 does), but when read it appears as two fields (array elements).  This
 289 is caused by the lack of whitespace between the second and third field.
 290 So this function gropes the md5sum output and returns:
 291 	[0]	32 character checksum in hexidecimal (UCFS filename)
 292 	[1]	Single character: ' ' text file, '*' binary file
 293 	[2]	Filesystem (20th Century Style) name
 294 	Note: That name may be the character '-' indicating STDIN read.
 295 
 296 DigestFilesDoc
 297 
 298 
 299 
 300 DigestFile()
 301 {
 302 	local if=0		# Default, variable name
 303 	local -a T1 T2
 304 
 305 	case "$#" in
 306 	3)	case "$1" in
 307 		-if)	if=1 ; shift ;;
 308 		 * )	return 1 ;;
 309 		esac ;;
 310 	2)	: ;;		# Poor man's "continue"
 311 	*)	return 1 ;;
 312 	esac
 313 
 314 	case $if in
 315 	0) eval T1=\( \"\$\{$1\[@\]\}\" \)
 316 	   T2=( $(echo ${T1[@]} | md5sum -) )
 317 	   ;;
 318 	1) T2=( $(md5sum $1) )
 319 	   ;;
 320 	esac
 321 
 322 	case ${#T2[@]} in
 323 	0) return 1 ;;
 324 	1) return 1 ;;
 325 	2) case ${T2[1]:0:1} in		# SanScrit-2.0.5
 326 	   \*) T2[${#T2[@]}]=${T2[1]:1}
 327 	       T2[1]=\*
 328 	       ;;
 329 	    *) T2[${#T2[@]}]=${T2[1]}
 330 	       T2[1]=" "
 331 	       ;;
 332 	   esac
 333 	   ;;
 334 	3) : ;; # Assume it worked
 335 	*) return 1 ;;
 336 	esac
 337 
 338 	local -i len=${#T2[0]}
 339 	if [ $len -ne 32 ] ; then return 1 ; fi
 340 	eval $2=\( \"\$\{T2\[@\]\}\" \)
 341 }
 342 
 343 # # # # # Locate File # # # # #
 344 #
 345 #	LocateFile [-l] FileName Location-Array-Name
 346 # or
 347 #	LocateFile [-l] -of FileName Location-Array-FileName
 348 # # # # #
 349 
 350 # A file location is Filesystem-id and inode-number
 351 
 352 # Here document used as a comment block.
 353 : <<StatFieldsDoc
 354 	Based on stat, version 2.2
 355 	stat -t and stat -lt fields
 356 	[0]	name
 357 	[1]	Total size
 358 		File - number of bytes
 359 		Symbolic link - string length of pathname
 360 	[2]	Number of (512 byte) blocks allocated
 361 	[3]	File type and Access rights (hex)
 362 	[4]	User ID of owner
 363 	[5]	Group ID of owner
 364 	[6]	Device number
 365 	[7]	Inode number
 366 	[8]	Number of hard links
 367 	[9]	Device type (if inode device) Major
 368 	[10]	Device type (if inode device) Minor
 369 	[11]	Time of last access
 370 		May be disabled in 'mount' with noatime
 371 		atime of files changed by exec, read, pipe, utime, mknod (mmap?)
 372 		atime of directories changed by addition/deletion of files
 373 	[12]	Time of last modification
 374 		mtime of files changed by write, truncate, utime, mknod
 375 		mtime of directories changed by addtition/deletion of files
 376 	[13]	Time of last change
 377 		ctime reflects time of changed inode information (owner, group
 378 		permissions, link count
 379 -*-*- Per:
 380 	Return code: 0
 381 	Size of array: 14
 382 	Contents of array
 383 	Element 0: /home/mszick
 384 	Element 1: 4096
 385 	Element 2: 8
 386 	Element 3: 41e8
 387 	Element 4: 500
 388 	Element 5: 500
 389 	Element 6: 303
 390 	Element 7: 32385
 391 	Element 8: 22
 392 	Element 9: 0
 393 	Element 10: 0
 394 	Element 11: 1051221030
 395 	Element 12: 1051214068
 396 	Element 13: 1051214068
 397 
 398 	For a link in the form of linkname -> realname
 399 	stat -t  linkname returns the linkname (link) information
 400 	stat -lt linkname returns the realname information
 401 
 402 	stat -tf and stat -ltf fields
 403 	[0]	name
 404 	[1]	ID-0?		# Maybe someday, but Linux stat structure
 405 	[2]	ID-0?		# does not have either LABEL nor UUID
 406 				# fields, currently information must come
 407 				# from file-system specific utilities
 408 	These will be munged into:
 409 	[1]	UUID if possible
 410 	[2]	Volume Label if possible
 411 	Note: 'mount -l' does return the label and could return the UUID
 412 
 413 	[3]	Maximum length of filenames
 414 	[4]	Filesystem type
 415 	[5]	Total blocks in the filesystem
 416 	[6]	Free blocks
 417 	[7]	Free blocks for non-root user(s)
 418 	[8]	Block size of the filesystem
 419 	[9]	Total inodes
 420 	[10]	Free inodes
 421 
 422 -*-*- Per:
 423 	Return code: 0
 424 	Size of array: 11
 425 	Contents of array
 426 	Element 0: /home/mszick
 427 	Element 1: 0
 428 	Element 2: 0
 429 	Element 3: 255
 430 	Element 4: ef53
 431 	Element 5: 2581445
 432 	Element 6: 2277180
 433 	Element 7: 2146050
 434 	Element 8: 4096
 435 	Element 9: 1311552
 436 	Element 10: 1276425
 437 
 438 StatFieldsDoc
 439 
 440 
 441 #	LocateFile [-l] FileName Location-Array-Name
 442 #	LocateFile [-l] -of FileName Location-Array-FileName
 443 
 444 LocateFile()
 445 {
 446 	local -a LOC LOC1 LOC2
 447 	local lk="" of=0
 448 
 449 	case "$#" in
 450 	0) return 1 ;;
 451 	1) return 1 ;;
 452 	2) : ;;
 453 	*) while (( "$#" > 2 ))
 454 	   do
 455 	      case "$1" in
 456 	       -l) lk=-1 ;;
 457 	      -of) of=1 ;;
 458 	        *) return 1 ;;
 459 	      esac
 460 	   shift
 461            done ;;
 462 	esac
 463 
 464 # More Sanscrit-2.0.5
 465       # LOC1=( $(stat -t $lk $1) )
 466       # LOC2=( $(stat -tf $lk $1) )
 467       # Uncomment above two lines if system has "stat" command installed.
 468 	LOC=( ${LOC1[@]:0:1} ${LOC1[@]:3:11}
 469 	      ${LOC2[@]:1:2} ${LOC2[@]:4:1} )
 470 
 471 	case "$of" in
 472 		0) eval $2=\( \"\$\{LOC\[@\]\}\" \) ;;
 473 		1) echo "${LOC[@]}" > "$2" ;;
 474 	esac
 475 	return 0
 476 # Which yields (if you are lucky, and have "stat" installed)
 477 # -*-*- Location Discriptor -*-*-
 478 #	Return code: 0
 479 #	Size of array: 15
 480 #	Contents of array
 481 #	Element 0: /home/mszick		20th Century name
 482 #	Element 1: 41e8			Type and Permissions
 483 #	Element 2: 500			User
 484 #	Element 3: 500			Group
 485 #	Element 4: 303			Device
 486 #	Element 5: 32385		inode
 487 #	Element 6: 22			Link count
 488 #	Element 7: 0			Device Major
 489 #	Element 8: 0			Device Minor
 490 #	Element 9: 1051224608		Last Access
 491 #	Element 10: 1051214068		Last Modify
 492 #	Element 11: 1051214068		Last Status
 493 #	Element 12: 0			UUID (to be)
 494 #	Element 13: 0			Volume Label (to be)
 495 #	Element 14: ef53		Filesystem type
 496 }
 497 
 498 
 499 
 500 # And then there was some test code
 501 
 502 ListArray() # ListArray Name
 503 {
 504 	local -a Ta
 505 
 506 	eval Ta=\( \"\$\{$1\[@\]\}\" \)
 507 	echo
 508 	echo "-*-*- List of Array -*-*-"
 509 	echo "Size of array $1: ${#Ta[*]}"
 510 	echo "Contents of array $1:"
 511 	for (( i=0 ; i<${#Ta[*]} ; i++ ))
 512 	do
 513 	    echo -e "\tElement $i: ${Ta[$i]}"
 514 	done
 515 	return 0
 516 }
 517 
 518 declare -a CUR_DIR
 519 # For small arrays
 520 ListDirectory "${PWD}" CUR_DIR
 521 ListArray CUR_DIR
 522 
 523 declare -a DIR_DIG
 524 DigestFile CUR_DIR DIR_DIG
 525 echo "The new \"name\" (checksum) for ${CUR_DIR[9]} is ${DIR_DIG[0]}"
 526 
 527 declare -a DIR_ENT
 528 # BIG_DIR # For really big arrays - use a temporary file in ramdisk
 529 # BIG-DIR # ListDirectory -of "${CUR_DIR[11]}/*" "/tmpfs/junk2"
 530 ListDirectory "${CUR_DIR[11]}/*" DIR_ENT
 531 
 532 declare -a DIR_IDX
 533 # BIG-DIR # IndexList -if "/tmpfs/junk2" DIR_IDX
 534 IndexList DIR_ENT DIR_IDX
 535 
 536 declare -a IDX_DIG
 537 # BIG-DIR # DIR_ENT=( $(cat /tmpfs/junk2) )
 538 # BIG-DIR # DigestFile -if /tmpfs/junk2 IDX_DIG
 539 DigestFile DIR_ENT IDX_DIG
 540 # Small (should) be able to parallize IndexList & DigestFile
 541 # Large (should) be able to parallize IndexList & DigestFile & the assignment
 542 echo "The \"name\" (checksum) for the contents of ${PWD} is ${IDX_DIG[0]}"
 543 
 544 declare -a FILE_LOC
 545 LocateFile ${PWD} FILE_LOC
 546 ListArray FILE_LOC
 547 
 548 exit 0

Stephane Chazelas demonstrates object-oriented programming in a Bash script.


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

Now for a script that does something useful: installing and mounting those cute USB keychain solid-state "hard drives."


Example A-22. Mounting USB keychain storage devices

   1 #!/bin/bash
   2 # ==> usb.sh
   3 # ==> Script for mounting and installing pen/keychain USB storage devices.
   4 # ==> Runs as root at system startup (see below).
   5  
   6 #  This code is free software covered by GNU GPL license version 2 or above.
   7 #  Please refer to http://www.gnu.org/ for the full license text.
   8 #
   9 #  Some code lifted from usb-mount by Michael Hamilton's usb-mount (LGPL)
  10 #+ see http://users.actrix.co.nz/michael/usbmount.html
  11 #
  12 #  INSTALL
  13 #  -------
  14 #  Put this in /etc/hotplug/usb/diskonkey.
  15 #  Then look in /etc/hotplug/usb.distmap, and copy all usb-storage entries
  16 #+ into /etc/hotplug/usb.usermap, substituting "usb-storage" for "diskonkey".
  17 #  Otherwise this code is only run during the kernel module invocation/removal
  18 #+ (at least in my tests), which defeats the purpose.
  19 #
  20 #  TODO
  21 #  ----
  22 #  Handle more than one diskonkey device at one time (e.g. /dev/diskonkey1
  23 #+ and /mnt/diskonkey1), etc. The biggest problem here is the handling in
  24 #+ devlabel, which I haven't yet tried.
  25 #
  26 #  AUTHOR and SUPPORT
  27 #  ------------------
  28 #  Konstantin Riabitsev, <icon linux duke edu>.
  29 #  Send any problem reports to my email address at the moment.
  30 #
  31 # ==> Comments added by ABS Guide author.
  32 
  33 
  34 
  35 SYMLINKDEV=/dev/diskonkey
  36 MOUNTPOINT=/mnt/diskonkey
  37 DEVLABEL=/sbin/devlabel
  38 DEVLABELCONFIG=/etc/sysconfig/devlabel
  39 IAM=$0
  40 
  41 ##
  42 # Functions lifted near-verbatim from usb-mount code.
  43 #
  44 function allAttachedScsiUsb {
  45     find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f | xargs grep -l 'Attached: Yes'
  46 }
  47 function scsiDevFromScsiUsb {
  48     echo $1 | awk -F"[-/]" '{ n=$(NF-1);  print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1,
  49  1) }'
  50 }
  51 
  52 if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
  53     ##
  54     # lifted from usbcam code.
  55     #
  56     if [ -f /var/run/console.lock ]; then
  57         CONSOLEOWNER=`cat /var/run/console.lock`
  58     elif [ -f /var/lock/console.lock ]; then
  59         CONSOLEOWNER=`cat /var/lock/console.lock`
  60     else
  61         CONSOLEOWNER=
  62     fi
  63     for procEntry in $(allAttachedScsiUsb); do
  64         scsiDev=$(scsiDevFromScsiUsb $procEntry)
  65         #  Some bug with usb-storage?
  66         #  Partitions are not in /proc/partitions until they are accessed
  67         #+ somehow.
  68         /sbin/fdisk -l $scsiDev >/dev/null
  69         ##
  70         #  Most devices have partitioning info, so the data would be on
  71         #+ /dev/sd?1. However, some stupider ones don't have any partitioning
  72         #+ and use the entire device for data storage. This tries to
  73         #+ guess semi-intelligently if we have a /dev/sd?1 and if not, then
  74         #+ it uses the entire device and hopes for the better.
  75         #
  76         if grep -q `basename $scsiDev`1 /proc/partitions; then
  77             part="$scsiDev""1"
  78         else
  79             part=$scsiDev
  80         fi
  81         ##
  82         #  Change ownership of the partition to the console user so they can
  83         #+ mount it.
  84         #
  85         if [ ! -z "$CONSOLEOWNER" ]; then
  86             chown $CONSOLEOWNER:disk $part
  87         fi
  88         ##
  89         # This checks if we already have this UUID defined with devlabel.
  90         # If not, it then adds the device to the list.
  91         #
  92         prodid=`$DEVLABEL printid -d $part`
  93         if ! grep -q $prodid $DEVLABELCONFIG; then
  94             # cross our fingers and hope it works
  95             $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null
  96         fi
  97         ##
  98         # Check if the mount point exists and create if it doesn't.
  99         #
 100         if [ ! -e $MOUNTPOINT ]; then
 101             mkdir -p $MOUNTPOINT
 102         fi
 103         ##
 104         # Take care of /etc/fstab so mounting is easy.
 105         #
 106         if ! grep -q "^$SYMLINKDEV" /etc/fstab; then
 107             # Add an fstab entry
 108             echo -e \
 109                 "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \
 110                 >> /etc/fstab
 111         fi
 112     done
 113     if [ ! -z "$REMOVER" ]; then
 114         ##
 115         # Make sure this script is triggered on device removal.
 116         #
 117         mkdir -p `dirname $REMOVER`
 118         ln -s $IAM $REMOVER
 119     fi
 120 elif [ "${ACTION}" = "remove" ]; then
 121     ##
 122     # If the device is mounted, unmount it cleanly.
 123     #
 124     if grep -q "$MOUNTPOINT" /etc/mtab; then
 125         # unmount cleanly
 126         umount -l $MOUNTPOINT
 127     fi
 128     ##
 129     # Remove it from /etc/fstab if it's there.
 130     #
 131     if grep -q "^$SYMLINKDEV" /etc/fstab; then
 132         grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new
 133         mv -f /etc/.fstab.new /etc/fstab
 134     fi
 135 fi
 136 
 137 exit 0

Here is something to warm the hearts of webmasters and mistresses everywhere: a script that saves weblogs.


Example A-23. Preserving weblogs

   1 #!/bin/bash
   2 # archiveweblogs.sh v1.0
   3 
   4 # Troy Engel <tengel@fluid.com>
   5 # Slightly modified by document author.
   6 # Used with permission.
   7 #
   8 #  This script will preserve the normally rotated and
   9 #+ thrown away weblogs from a default RedHat/Apache installation.
  10 #  It will save the files with a date/time stamp in the filename,
  11 #+ bzipped, to a given directory.
  12 #
  13 #  Run this from crontab nightly at an off hour,
  14 #+ as bzip2 can suck up some serious CPU on huge logs:
  15 #  0 2 * * * /opt/sbin/archiveweblogs.sh
  16 
  17 
  18 PROBLEM=66
  19 
  20 # Set this to your backup dir.
  21 BKP_DIR=/opt/backups/weblogs
  22 
  23 # Default Apache/RedHat stuff
  24 LOG_DAYS="4 3 2 1"
  25 LOG_DIR=/var/log/httpd
  26 LOG_FILES="access_log error_log"
  27 
  28 # Default RedHat program locations
  29 LS=/bin/ls
  30 MV=/bin/mv
  31 ID=/usr/bin/id
  32 CUT=/bin/cut
  33 COL=/usr/bin/column
  34 BZ2=/usr/bin/bzip2
  35 
  36 # Are we root?
  37 USER=`$ID -u`
  38 if [ "X$USER" != "X0" ]; then
  39   echo "PANIC: Only root can run this script!"
  40   exit $PROBLEM
  41 fi
  42 
  43 # Backup dir exists/writable?
  44 if [ ! -x $BKP_DIR ]; then
  45   echo "PANIC: $BKP_DIR doesn't exist or isn't writable!"
  46   exit $PROBLEM
  47 fi
  48 
  49 # Move, rename and bzip2 the logs
  50 for logday in $LOG_DAYS; do
  51   for logfile in $LOG_FILES; do
  52     MYFILE="$LOG_DIR/$logfile.$logday"
  53     if [ -w $MYFILE ]; then
  54       DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7`
  55       $MV $MYFILE $BKP_DIR/$logfile.$DTS
  56       $BZ2 $BKP_DIR/$logfile.$DTS
  57     else
  58       # Only spew an error if the file exits (ergo non-writable).
  59       if [ -f $MYFILE ]; then
  60         echo "ERROR: $MYFILE not writable. Skipping."
  61       fi
  62     fi
  63   done
  64 done
  65 
  66 exit 0

How do you keep the shell from expanding and reinterpreting strings?


Example A-24. Protecting literal strings

   1 #! /bin/bash
   2 # protect_literal.sh
   3 
   4 # set -vx
   5 
   6 :<<-'_Protect_Literal_String_Doc'
   7 
   8     Copyright (c) Michael S. Zick, 2003; All Rights Reserved
   9     License: Unrestricted reuse in any form, for any purpose.
  10     Warranty: None
  11     Revision: $ID$
  12 
  13     Documentation redirected to the Bash no-operation.
  14     Bash will '/dev/null' this block when the script is first read.
  15     (Uncomment the above set command to see this action.)
  16 
  17     Remove the first (Sha-Bang) line when sourcing this as a library
  18     procedure.  Also comment out the example use code in the two
  19     places where shown.
  20 
  21 
  22     Usage:
  23         _protect_literal_str 'Whatever string meets your ${fancy}'
  24         Just echos the argument to standard out, hard quotes
  25         restored.
  26 
  27         $(_protect_literal_str 'Whatever string meets your ${fancy}')
  28         as the right-hand-side of an assignment statement.
  29 
  30     Does:
  31         As the right-hand-side of an assignment, preserves the
  32         hard quotes protecting the contents of the literal during
  33         assignment.
  34 
  35     Notes:
  36         The strange names (_*) are used to avoid trampling on
  37         the user's chosen names when this is sourced as a
  38         library.
  39 
  40 _Protect_Literal_String_Doc
  41 
  42 # The 'for illustration' function form
  43 
  44 _protect_literal_str() {
  45 
  46 # Pick an un-used, non-printing character as local IFS.
  47 # Not required, but shows that we are ignoring it.
  48     local IFS=$'\x1B'               # \ESC character
  49 
  50 # Enclose the All-Elements-Of in hard quotes during assignment.
  51     local tmp=$'\x27'$@$'\x27'
  52 #    local tmp=$'\''$@$'\''         # Even uglier.
  53 
  54     local len=${#tmp}               # Info only.
  55     echo $tmp is $len long.         # Output AND information.
  56 }
  57 
  58 # This is the short-named version.
  59 _pls() {
  60     local IFS=$'x1B'                # \ESC character (not required)
  61     echo $'\x27'$@$'\x27'           # Hard quoted parameter glob
  62 }
  63 
  64 # :<<-'_Protect_Literal_String_Test'
  65 # # # Remove the above "# " to disable this code. # # #
  66 
  67 # See how that looks when printed.
  68 echo
  69 echo "- - Test One - -"
  70 _protect_literal_str 'Hello $user'
  71 _protect_literal_str 'Hello "${username}"'
  72 echo
  73 
  74 # Which yields:
  75 # - - Test One - -
  76 # 'Hello $user' is 13 long.
  77 # 'Hello "${username}"' is 21 long.
  78 
  79 #  Looks as expected, but why all of the trouble?
  80 #  The difference is hidden inside the Bash internal order
  81 #+ of operations.
  82 #  Which shows when you use it on the RHS of an assignment.
  83 
  84 # Declare an array for test values.
  85 declare -a arrayZ
  86 
  87 # Assign elements with various types of quotes and escapes.
  88 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
  89 
  90 # Now list that array and see what is there.
  91 echo "- - Test Two - -"
  92 for (( i=0 ; i<${#arrayZ[*]} ; i++ ))
  93 do
  94     echo  Element $i: ${arrayZ[$i]} is: ${#arrayZ[$i]} long.
  95 done
  96 echo
  97 
  98 # Which yields:
  99 # - - Test Two - -
 100 # Element 0: zero is: 4 long.           # Our marker element
 101 # Element 1: 'Hello ${Me}' is: 13 long. # Our "$(_pls '...' )"
 102 # Element 2: Hello ${You} is: 12 long.  # Quotes are missing
 103 # Element 3: \'Pass: \' is: 10 long.    # ${pw} expanded to nothing
 104 
 105 # Now make an assignment with that result.
 106 declare -a array2=( ${arrayZ[@]} )
 107 
 108 # And print what happened.
 109 echo "- - Test Three - -"
 110 for (( i=0 ; i<${#array2[*]} ; i++ ))
 111 do
 112     echo  Element $i: ${array2[$i]} is: ${#array2[$i]} long.
 113 done
 114 echo
 115 
 116 # Which yields:
 117 # - - Test Three - -
 118 # Element 0: zero is: 4 long.           # Our marker element.
 119 # Element 1: Hello ${Me} is: 11 long.   # Intended result.
 120 # Element 2: Hello is: 5 long.          # ${You} expanded to nothing.
 121 # Element 3: 'Pass: is: 6 long.         # Split on the whitespace.
 122 # Element 4: ' is: 1 long.              # The end quote is here now.
 123 
 124 #  Our Element 1 has had its leading and trailing hard quotes stripped.
 125 #  Although not shown, leading and trailing whitespace is also stripped.
 126 #  Now that the string contents are set, Bash will always, internally,
 127 #+ hard quote the contents as required during its operations.
 128 
 129 #  Why?
 130 #  Considering our "$(_pls 'Hello ${Me}')" construction:
 131 #  " ... " -> Expansion required, strip the quotes.
 132 #  $( ... ) -> Replace with the result of..., strip this.
 133 #  _pls ' ... ' -> called with literal arguments, strip the quotes.
 134 #  The result returned includes hard quotes; BUT the above processing
 135 #+ has already been done, so they become part of the value assigned.
 136 #
 137 #  Similarly, during further usage of the string variable, the ${Me}
 138 #+ is part of the contents (result) and survives any operations
 139 #  (Until explicitly told to evaluate the string).
 140 
 141 #  Hint: See what happens when the hard quotes ($'\x27') are replaced
 142 #+ with soft quotes ($'\x22') in the above procedures.
 143 #  Interesting also is to remove the addition of any quoting.
 144 
 145 # _Protect_Literal_String_Test
 146 # # # Remove the above "# " to disable this code. # # #
 147 
 148 exit 0

What if you want the shell to expand and reinterpret strings?


Example A-25. Unprotecting literal strings

   1 #! /bin/bash
   2 # unprotect_literal.sh
   3 
   4 # set -vx
   5 
   6 :<<-'_UnProtect_Literal_String_Doc'
   7 
   8     Copyright (c) Michael S. Zick, 2003; All Rights Reserved
   9     License: Unrestricted reuse in any form, for any purpose.
  10     Warranty: None
  11     Revision: $ID$
  12 
  13     Documentation redirected to the Bash no-operation. Bash will
  14     '/dev/null' this block when the script is first read.
  15     (Uncomment the above set command to see this action.)
  16 
  17     Remove the first (Sha-Bang) line when sourcing this as a library
  18     procedure.  Also comment out the example use code in the two
  19     places where shown.
  20 
  21 
  22     Usage:
  23         Complement of the "$(_pls 'Literal String')" function.
  24         (See the protect_literal.sh example.)
  25 
  26         StringVar=$(_upls ProtectedSringVariable)
  27 
  28     Does:
  29         When used on the right-hand-side of an assignment statement;
  30         makes the substitions embedded in the protected string.
  31 
  32     Notes:
  33         The strange names (_*) are used to avoid trampling on
  34         the user's chosen names when this is sourced as a
  35         library.
  36 
  37 
  38 _UnProtect_Literal_String_Doc
  39 
  40 _upls() {
  41     local IFS=$'x1B'                # \ESC character (not required)
  42     eval echo $@                    # Substitution on the glob.
  43 }
  44 
  45 # :<<-'_UnProtect_Literal_String_Test'
  46 # # # Remove the above "# " to disable this code. # # #
  47 
  48 
  49 _pls() {
  50     local IFS=$'x1B'                # \ESC character (not required)
  51     echo $'\x27'$@$'\x27'           # Hard quoted parameter glob
  52 }
  53 
  54 # Declare an array for test values.
  55 declare -a arrayZ
  56 
  57 # Assign elements with various types of quotes and escapes.
  58 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
  59 
  60 # Now make an assignment with that result.
  61 declare -a array2=( ${arrayZ[@]} )
  62 
  63 # Which yielded:
  64 # - - Test Three - -
  65 # Element 0: zero is: 4 long            # Our marker element.
  66 # Element 1: Hello ${Me} is: 11 long    # Intended result.
  67 # Element 2: Hello is: 5 long           # ${You} expanded to nothing.
  68 # Element 3: 'Pass: is: 6 long          # Split on the whitespace.
  69 # Element 4: ' is: 1 long               # The end quote is here now.
  70 
  71 # set -vx
  72 
  73 #  Initialize 'Me' to something for the embedded ${Me} substitution.
  74 #  This needs to be done ONLY just prior to evaluating the
  75 #+ protected string.
  76 #  (This is why it was protected to begin with.)
  77 
  78 Me="to the array guy."
  79 
  80 # Set a string variable destination to the result.
  81 newVar=$(_upls ${array2[1]})
  82 
  83 # Show what the contents are.
  84 echo $newVar
  85 
  86 # Do we really need a function to do this?
  87 newerVar=$(eval echo ${array2[1]})
  88 echo $newerVar
  89 
  90 #  I guess not, but the _upls function gives us a place to hang
  91 #+ the documentation on.
  92 #  This helps when we forget what a # construction like:
  93 #+ $(eval echo ... ) means.
  94 
  95 # What if Me isn't set when the protected string is evaluated?
  96 unset Me
  97 newestVar=$(_upls ${array2[1]})
  98 echo $newestVar
  99 
 100 # Just gone, no hints, no runs, no errors.
 101 
 102 #  Why in the world?
 103 #  Setting the contents of a string variable containing character
 104 #+ sequences that have a meaning to Bash is a general problem in
 105 #+ script programming.
 106 #
 107 #  This problem is now solved in eight lines of code
 108 #+ (and four pages of description).
 109 
 110 #  Where is all this going?
 111 #  Dynamic content Web pages as an array of Bash strings.
 112 #  Content set per request by a Bash 'eval' command
 113 #+ on the stored page template.
 114 #  Not intended to replace PHP, just an interesting thing to do.
 115 ###
 116 #  Don't have a webserver application?
 117 #  No problem, check the example directory of the Bash source;
 118 #+ there is a Bash script for that also.
 119 
 120 # _UnProtect_Literal_String_Test
 121 # # # Remove the above "# " to disable this code. # # #
 122 
 123 exit 0

This powerful script helps hunt down spammers .


Example A-26. Spammer Identification

   1 #!/bin/bash
   2 # $Id: is_spammer.bash,v 1.12.2.11 2004/10/01 21:42:33 mszick Exp $
   3 # The latest version of this script is available from ftp://ftp.morethan.org.
   4 #
   5 # Spammer-identification
   6 # by Michael S. Zick
   7 # Used in the ABS Guide with permission.
   8 
   9 
  10 
  11 #######################################################
  12 # Documentation
  13 # See also "Quickstart" at end of script.
  14 #######################################################
  15 
  16 :<<-'__is_spammer_Doc_'
  17 
  18     Copyright (c) Michael S. Zick, 2004
  19     License: Unrestricted reuse in any form, for any purpose.
  20     Warranty: None -{Its a script; the user is on their own.}-
  21 
  22 Impatient?
  23     Application code: goto "# # # Hunt the Spammer' program code # # #"
  24     Example output: ":<<-'_is_spammer_outputs_'"
  25     How to use: Enter script name without arguments.
  26                 Or goto "Quickstart" at end of script.
  27 
  28 Provides
  29     Given a domain name or IP(v4) address as input:
  30 
  31     Does an exhaustive set of queries to find the associated
  32     network resources (short of recursing into TLDs).
  33 
  34     Checks the IP(v4) addresses found against Blacklist
  35     nameservers.
  36 
  37     If found to be a blacklisted IP(v4) address,
  38     reports the blacklist text records.
  39     (Usually hyper-links to the specific report.)
  40 
  41 Requires
  42     A working Internet connection.
  43     (Exercise: Add check and/or abort if not on-line when running script.)
  44     Bash with arrays (2.05b+).
  45 
  46     The external program 'dig' --
  47     a utility program provided with the 'bind' set of programs.
  48     Specifically, the version which is part of Bind series 9.x
  49     See: http://www.isc.org
  50 
  51     All usages of 'dig' are limited to wrapper functions,
  52     which may be rewritten as required.
  53     See: dig_wrappers.bash for details.
  54          ("Additional documentation" -- below)
  55 
  56 Usage
  57     Script requires a single argument, which may be:
  58     1) A domain name;
  59     2) An IP(v4) address;
  60     3) A filename, with one name or address per line.
  61 
  62     Script accepts an optional second argument, which may be:
  63     1) A Blacklist server name;
  64     2) A filename, with one Blacklist server name per line.
  65 
  66     If the second argument is not provided, the script uses
  67     a built-in set of (free) Blacklist servers.
  68 
  69     See also, the Quickstart at the end of this script (after 'exit').
  70 
  71 Return Codes
  72     0 - All OK
  73     1 - Script failure
  74     2 - Something is Blacklisted
  75 
  76 Optional environment variables
  77     SPAMMER_TRACE
  78         If set to a writable file,
  79         script will log an execution flow trace.
  80 
  81     SPAMMER_DATA
  82         If set to a writable file, script will dump its
  83         discovered data in the form of GraphViz file.
  84         See: http://www.research.att.com/sw/tools/graphviz
  85 
  86     SPAMMER_LIMIT
  87         Limits the depth of resource tracing.
  88 
  89         Default is 2 levels.
  90 
  91         A setting of 0 (zero) means 'unlimited' . . .
  92           Caution: script might recurse the whole Internet!
  93 
  94         A limit of 1 or 2 is most useful when processing
  95         a file of domain names and addresses.
  96         A higher limit can be useful when hunting spam gangs.
  97 
  98 
  99 Additional documentation
 100     Download the archived set of scripts
 101     explaining and illustrating the function contained within this script.
 102     http://personal.riverusers.com/mszick_clf.tar.bz2
 103 
 104 
 105 Study notes
 106     This script uses a large number of functions.
 107     Nearly all general functions have their own example script.
 108     Each of the example scripts have tutorial level comments.
 109 
 110 Scripting project
 111     Add support for IP(v6) addresses.
 112     IP(v6) addresses are recognized but not processed.
 113 
 114 Advanced project
 115     Add the reverse lookup detail to the discovered information.
 116 
 117     Report the delegation chain and abuse contacts.
 118 
 119     Modify the GraphViz file output to include the
 120     newly discovered information.
 121 
 122 __is_spammer_Doc_
 123 
 124 #######################################################
 125 
 126 
 127 
 128 
 129 #### Special IFS settings used for string parsing. ####
 130 
 131 # Whitespace == :Space:Tab:Line Feed:Carriage Return:
 132 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D'
 133 
 134 # No Whitespace == Line Feed:Carriage Return
 135 NO_WSP=$'\x0A'$'\x0D'
 136 
 137 # Field separator for dotted decimal IP addresses
 138 ADR_IFS=${NO_WSP}'.'
 139 
 140 # Array to dotted string conversions
 141 DOT_IFS='.'${WSP_IFS}
 142 
 143 # # # Pending operations stack machine # # #
 144 # This set of functions described in func_stack.bash.
 145 # (See "Additional documentation" above.)
 146 # # #
 147 
 148 # Global stack of pending operations.
 149 declare -f -a _pending_
 150 # Global sentinel for stack runners
 151 declare -i _p_ctrl_
 152 # Global holder for currently executing function
 153 declare -f _pend_current_
 154 
 155 # # # Debug version only - remove for regular use # # #
 156 #
 157 # The function stored in _pend_hook_ is called
 158 # immediately before each pending function is
 159 # evaluated.  Stack clean, _pend_current_ set.
 160 #
 161 # This thingy demonstrated in pend_hook.bash.
 162 declare -f _pend_hook_
 163 # # #
 164 
 165 # The do nothing function
 166 pend_dummy() { : ; }
 167 
 168 # Clear and initialize the function stack.
 169 pend_init() {
 170     unset _pending_[@]
 171     pend_func pend_stop_mark
 172     _pend_hook_='pend_dummy'  # Debug only
 173 }
 174 
 175 # Discard the top function on the stack.
 176 pend_pop() {
 177     if [ ${#_pending_[@]} -gt 0 ]
 178     then
 179         local -i _top_
 180         _top_=${#_pending_[@]}-1
 181         unset _pending_[$_top_]
 182     fi
 183 }
 184 
 185 # pend_func function_name [$(printf '%q\n' arguments)]
 186 pend_func() {
 187     local IFS=${NO_WSP}
 188     set -f
 189     _pending_[${#_pending_[@]}]=$@
 190     set +f
 191 }
 192 
 193 # The function which stops the release:
 194 pend_stop_mark() {
 195     _p_ctrl_=0
 196 }
 197 
 198 pend_mark() {
 199     pend_func pend_stop_mark
 200 }
 201 
 202 # Execute functions until 'pend_stop_mark' . . .
 203 pend_release() {
 204     local -i _top_             # Declare _top_ as integer.
 205     _p_ctrl_=${#_pending_[@]}
 206     while [ ${_p_ctrl_} -gt 0 ]
 207     do
 208        _top_=${#_pending_[@]}-1
 209        _pend_current_=${_pending_[$_top_]}
 210        unset _pending_[$_top_]
 211        $_pend_hook_            # Debug only
 212        eval $_pend_current_
 213     done
 214 }
 215 
 216 # Drop functions until 'pend_stop_mark' . . .
 217 pend_drop() {
 218     local -i _top_
 219     local _pd_ctrl_=${#_pending_[@]}
 220     while [ ${_pd_ctrl_} -gt 0 ]
 221     do
 222        _top_=$_pd_ctrl_-1
 223        if [ "${_pending_[$_top_]}" == 'pend_stop_mark' ]
 224        then
 225            unset _pending_[$_top_]
 226            break
 227        else
 228            unset _pending_[$_top_]
 229            _pd_ctrl_=$_top_
 230        fi
 231     done
 232     if [ ${#_pending_[@]} -eq 0 ]
 233     then
 234         pend_func pend_stop_mark
 235     fi
 236 }
 237 
 238 #### Array editors ####
 239 
 240 # This function described in edit_exact.bash.
 241 # (See "Additional documentation," above.)
 242 # edit_exact <excludes_array_name> <target_array_name>
 243 edit_exact() {
 244     [ $# -eq 2 ] ||
 245     [ $# -eq 3 ] || return 1
 246     local -a _ee_Excludes
 247     local -a _ee_Target
 248     local _ee_x
 249     local _ee_t
 250     local IFS=${NO_WSP}
 251     set -f
 252     eval _ee_Excludes=\( \$\{$1\[@\]\} \)
 253     eval _ee_Target=\( \$\{$2\[@\]\} \)
 254     local _ee_len=${#_ee_Target[@]}     # Original length.
 255     local _ee_cnt=${#_ee_Excludes[@]}   # Exclude list length.
 256     [ ${_ee_len} -ne 0 ] || return 0    # Can't edit zero length.
 257     [ ${_ee_cnt} -ne 0 ] || return 0    # Can't edit zero length.
 258     for (( x = 0; x < ${_ee_cnt} ; x++ ))
 259     do
 260         _ee_x=${_ee_Excludes[$x]}
 261         for (( n = 0 ; n < ${_ee_len} ; n++ ))
 262         do
 263             _ee_t=${_ee_Target[$n]}
 264             if [ x"${_ee_t}" == x"${_ee_x}" ]
 265             then
 266                 unset _ee_Target[$n]     # Discard match.
 267                 [ $# -eq 2 ] && break    # If 2 arguments, then done.
 268             fi
 269         done
 270     done
 271     eval $2=\( \$\{_ee_Target\[@\]\} \)
 272     set +f
 273     return 0
 274 }
 275 
 276 # This function described in edit_by_glob.bash.
 277 # edit_by_glob <excludes_array_name> <target_array_name>
 278 edit_by_glob() {
 279     [ $# -eq 2 ] ||
 280     [ $# -eq 3 ] || return 1
 281     local -a _ebg_Excludes
 282     local -a _ebg_Target
 283     local _ebg_x
 284     local _ebg_t
 285     local IFS=${NO_WSP}
 286     set -f
 287     eval _ebg_Excludes=\( \$\{$1\[@\]\} \)
 288     eval _ebg_Target=\( \$\{$2\[@\]\} \)
 289     local _ebg_len=${#_ebg_Target[@]}
 290     local _ebg_cnt=${#_ebg_Excludes[@]}
 291     [ ${_ebg_len} -ne 0 ] || return 0
 292     [ ${_ebg_cnt} -ne 0 ] || return 0
 293     for (( x = 0; x < ${_ebg_cnt} ; x++ ))
 294     do
 295         _ebg_x=${_ebg_Excludes[$x]}
 296         for (( n = 0 ; n < ${_ebg_len} ; n++ ))
 297         do
 298             [ $# -eq 3 ] && _ebg_x=${_ebg_x}'*'  #  Do prefix edit
 299             if [ ${_ebg_Target[$n]:=} ]          #+ if defined & set.
 300             then
 301                 _ebg_t=${_ebg_Target[$n]/#${_ebg_x}/}
 302                 [ ${#_ebg_t} -eq 0 ] && unset _ebg_Target[$n]
 303             fi
 304         done
 305     done
 306     eval $2=\( \$\{_ebg_Target\[@\]\} \)
 307     set +f
 308     return 0
 309 }
 310 
 311 # This function described in unique_lines.bash.
 312 # unique_lines <in_name> <out_name>
 313 unique_lines() {
 314     [ $# -eq 2 ] || return 1
 315     local -a _ul_in
 316     local -a _ul_out
 317     local -i _ul_cnt
 318     local -i _ul_pos
 319     local _ul_tmp
 320     local IFS=${NO_WSP}
 321     set -f
 322     eval _ul_in=\( \$\{$1\[@\]\} \)
 323     _ul_cnt=${#_ul_in[@]}
 324     for (( _ul_pos = 0 ; _ul_pos < ${_ul_cnt} ; _ul_pos++ ))
 325     do
 326         if [ ${_ul_in[${_ul_pos}]:=} ]      # If defined & not empty
 327         then
 328             _ul_tmp=${_ul_in[${_ul_pos}]}
 329             _ul_out[${#_ul_out[@]}]=${_ul_tmp}
 330             for (( zap = _ul_pos ; zap < ${_ul_cnt} ; zap++ ))
 331             do
 332                 [ ${_ul_in[${zap}]:=} ] &&
 333                 [ 'x'${_ul_in[${zap}]} == 'x'${_ul_tmp} ] &&
 334                     unset _ul_in[${zap}]
 335             done
 336         fi
 337     done
 338     eval $2=\( \$\{_ul_out\[@\]\} \)
 339     set +f
 340     return 0
 341 }
 342 
 343 # This function described in char_convert.bash.
 344 # to_lower <string>
 345 to_lower() {
 346     [ $# -eq 1 ] || return 1
 347     local _tl_out
 348     _tl_out=${1//A/a}
 349     _tl_out=${_tl_out//B/b}
 350     _tl_out=${_tl_out//C/c}
 351     _tl_out=${_tl_out//D/d}
 352     _tl_out=${_tl_out//E/e}
 353     _tl_out=${_tl_out//F/f}
 354     _tl_out=${_tl_out//G/g}
 355     _tl_out=${_tl_out//H/h}
 356     _tl_out=${_tl_out//I/i}
 357     _tl_out=${_tl_out//J/j}
 358     _tl_out=${_tl_out//K/k}
 359     _tl_out=${_tl_out//L/l}
 360     _tl_out=${_tl_out//M/m}
 361     _tl_out=${_tl_out//N/n}
 362     _tl_out=${_tl_out//O/o}
 363     _tl_out=${_tl_out//P/p}
 364     _tl_out=${_tl_out//Q/q}
 365     _tl_out=${_tl_out//R/r}
 366     _tl_out=${_tl_out//S/s}
 367     _tl_out=${_tl_out//T/t}
 368     _tl_out=${_tl_out//U/u}
 369     _tl_out=${_tl_out//V/v}
 370     _tl_out=${_tl_out//W/w}
 371     _tl_out=${_tl_out//X/x}
 372     _tl_out=${_tl_out//Y/y}
 373     _tl_out=${_tl_out//Z/z}
 374     echo ${_tl_out}
 375     return 0
 376 }
 377 
 378 #### Application helper functions ####
 379 
 380 # Not everybody uses dots as separators (APNIC, for example).
 381 # This function described in to_dot.bash
 382 # to_dot <string>
 383 to_dot() {
 384     [ $# -eq 1 ] || return 1
 385     echo ${1//[#|@|%]/.}
 386     return 0
 387 }
 388 
 389 # This function described in is_number.bash.
 390 # is_number <input>
 391 is_number() {
 392     [ "$#" -eq 1 ]    || return 1  # is blank?
 393     [ x"$1" == 'x0' ] && return 0  # is zero?
 394     local -i tst
 395     let tst=$1 2>/dev/null         # else is numeric!
 396     return $?
 397 }
 398 
 399 # This function described in is_address.bash.
 400 # is_address <input>
 401 is_address() {
 402     [ $# -eq 1 ] || return 1    # Blank ==> false
 403     local -a _ia_input
 404     local IFS=${ADR_IFS}
 405     _ia_input=( $1 )
 406     if  [ ${#_ia_input[@]} -eq 4 ]  &&
 407         is_number ${_ia_input[0]}   &&
 408         is_number ${_ia_input[1]}   &&
 409         is_number ${_ia_input[2]}   &&
 410         is_number ${_ia_input[3]}   &&
 411         [ ${_ia_input[0]} -lt 256 ] &&
 412         [ ${_ia_input[1]} -lt 256 ] &&
 413         [ ${_ia_input[2]} -lt 256 ] &&
 414         [ ${_ia_input[3]} -lt 256 ]
 415     then
 416         return 0
 417     else
 418         return 1
 419     fi
 420 }
 421 
 422 # This function described in split_ip.bash.
 423 # split_ip <IP_address> <array_name_norm> [<array_name_rev>]
 424 split_ip() {
 425     [ $# -eq 3 ] ||              #  Either three
 426     [ $# -eq 2 ] || return 1     #+ or two arguments
 427     local -a _si_input
 428     local IFS=${ADR_IFS}
 429     _si_input=( $1 )
 430     IFS=${WSP_IFS}
 431     eval $2=\(\ \$\{_si_input\[@\]\}\ \)
 432     if [ $# -eq 3 ]
 433     then
 434         # Build query order array.
 435         local -a _dns_ip
 436         _dns_ip[0]=${_si_input[3]}
 437         _dns_ip[1]=${_si_input[2]}
 438         _dns_ip[2]=${_si_input[1]}
 439         _dns_ip[3]=${_si_input[0]}
 440         eval $3=\(\ \$\{_dns_ip\[@\]\}\ \)
 441     fi
 442     return 0
 443 }
 444 
 445 # This function described in dot_array.bash.
 446 # dot_array <array_name>
 447 dot_array() {
 448     [ $# -eq 1 ] || return 1     # Single argument required.
 449     local -a _da_input
 450     eval _da_input=\(\ \$\{$1\[@\]\}\ \)
 451     local IFS=${DOT_IFS}
 452     local _da_output=${_da_input[@]}
 453     IFS=${WSP_IFS}
 454     echo ${_da_output}
 455     return 0
 456 }
 457 
 458 # This function described in file_to_array.bash
 459 # file_to_array <file_name> <line_array_name>
 460 file_to_array() {
 461     [ $# -eq 2 ] || return 1  # Two arguments required.
 462     local IFS=${NO_WSP}
 463     local -a _fta_tmp_
 464     _fta_tmp_=( $(cat $1) )
 465     eval $2=\( \$\{_fta_tmp_\[@\]\} \)
 466     return 0
 467 }
 468 
 469 # Columnized print of an array of multi-field strings.
 470 # col_print <array_name> <min_space> <tab_stop [tab_stops]>
 471 col_print() {
 472     [ $# -gt 2 ] || return 0
 473     local -a _cp_inp
 474     local -a _cp_spc
 475     local -a _cp_line
 476     local _cp_min
 477     local _cp_mcnt
 478     local _cp_pos
 479     local _cp_cnt
 480     local _cp_tab
 481     local -i _cp
 482     local -i _cpf
 483     local _cp_fld
 484     # WARNING: FOLLOWING LINE NOT BLANK -- IT IS QUOTED SPACES.
 485     local _cp_max='                                                            '
 486     set -f
 487     local IFS=${NO_WSP}
 488     eval _cp_inp=\(\ \$\{$1\[@\]\}\ \)
 489     [ ${#_cp_inp[@]} -gt 0 ] || return 0 # Empty is easy.
 490     _cp_mcnt=$2
 491     _cp_min=${_cp_max:1:${_cp_mcnt}}
 492     shift
 493     shift
 494     _cp_cnt=$#
 495     for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ ))
 496     do
 497         _cp_spc[${#_cp_spc[@]}]="${_cp_max:2:$1}" #"
 498         shift
 499     done
 500     _cp_cnt=${#_cp_inp[@]}
 501     for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ ))
 502     do
 503         _cp_pos=1
 504         IFS=${NO_WSP}$'\x20'
 505         _cp_line=( ${_cp_inp[${_cp}]} )
 506         IFS=${NO_WSP}
 507         for (( _cpf = 0 ; _cpf < ${#_cp_line[@]} ; _cpf++ ))
 508         do
 509             _cp_tab=${_cp_spc[${_cpf}]:${_cp_pos}}
 510             if [ ${#_cp_tab} -lt ${_cp_mcnt} ]
 511             then
 512                 _cp_tab="${_cp_min}"
 513             fi
 514             echo -n "${_cp_tab}"
 515             (( _cp_pos = ${_cp_pos} + ${#_cp_tab} ))
 516             _cp_fld="${_cp_line[${_cpf}]}"
 517             echo -n ${_cp_fld}
 518             (( _cp_pos = ${_cp_pos} + ${#_cp_fld} ))
 519         done
 520         echo
 521     done
 522     set +f
 523     return 0
 524 }
 525 
 526 # # # # 'Hunt the Spammer' data flow # # # #
 527 
 528 # Application return code
 529 declare -i _hs_RC
 530 
 531 # Original input, from which IP addresses are removed
 532 # After which, domain names to check
 533 declare -a uc_name
 534 
 535 # Original input IP addresses are moved here
 536 # After which, IP addresses to check
 537 declare -a uc_address
 538 
 539 # Names against which address expansion run
 540 # Ready for name detail lookup
 541 declare -a chk_name
 542 
 543 # Addresses against which name expansion run
 544 # Ready for address detail lookup
 545 declare -a chk_address
 546 
 547 #  Recursion is depth-first-by-name.
 548 #  The expand_input_address maintains this list
 549 #+ to prohibit looking up addresses twice during
 550 #+ domain name recursion.
 551 declare -a been_there_addr
 552 been_there_addr=( '127.0.0.1' ) # Whitelist localhost
 553 
 554 # Names which we have checked (or given up on)
 555 declare -a known_name
 556 
 557 # Addresses which we have checked (or given up on)
 558 declare -a known_address
 559 
 560 #  List of zero or more Blacklist servers to check.
 561 #  Each 'known_address' will be checked against each server,
 562 #+ with negative replies and failures suppressed.
 563 declare -a list_server
 564 
 565 # Indirection limit - set to zero == no limit
 566 indirect=${SPAMMER_LIMIT:=2}
 567 
 568 # # # # 'Hunt the Spammer' information output data # # # #
 569 
 570 # Any domain name may have multiple IP addresses.
 571 # Any IP address may have multiple domain names.
 572 # Therefore, track unique address-name pairs.
 573 declare -a known_pair
 574 declare -a reverse_pair
 575 
 576 #  In addition to the data flow variables; known_address
 577 #+ known_name and list_server, the following are output to the
 578 #+ external graphics interface file.
 579 
 580 # Authority chain, parent -> SOA fields.
 581 declare -a auth_chain
 582 
 583 # Reference chain, parent name -> child name
 584 declare -a ref_chain
 585 
 586 # DNS chain - domain name -> address
 587 declare -a name_address
 588 
 589 # Name and service pairs - domain name -> service
 590 declare -a name_srvc
 591 
 592 # Name and resource pairs - domain name -> Resource Record
 593 declare -a name_resource
 594 
 595 # Parent and Child pairs - parent name -> child name
 596 # This MAY NOT be the same as the ref_chain followed!
 597 declare -a parent_child
 598 
 599 # Address and Blacklist hit pairs - address->server
 600 declare -a address_hits
 601 
 602 # Dump interface file data
 603 declare -f _dot_dump
 604 _dot_dump=pend_dummy   # Initially a no-op
 605 
 606 #  Data dump is enabled by setting the environment variable SPAMMER_DATA
 607 #+ to the name of a writable file.
 608 declare _dot_file
 609 
 610 # Helper function for the dump-to-dot-file function
 611 # dump_to_dot <array_name> <prefix>
 612 dump_to_dot() {
 613     local -a _dda_tmp
 614     local -i _dda_cnt
 615     local _dda_form='    '${2}'%04u %s\n'
 616     local IFS=${NO_WSP}
 617     eval _dda_tmp=\(\ \$\{$1\[@\]\}\ \)
 618     _dda_cnt=${#_dda_tmp[@]}
 619     if [ ${_dda_cnt} -gt 0 ]
 620     then
 621         for (( _dda = 0 ; _dda < _dda_cnt ; _dda++ ))
 622         do
 623             printf "${_dda_form}" \
 624                    "${_dda}" "${_dda_tmp[${_dda}]}" >>${_dot_file}
 625         done
 626     fi
 627 }
 628 
 629 # Which will also set _dot_dump to this function . . .
 630 dump_dot() {
 631     local -i _dd_cnt
 632     echo '# Data vintage: '$(date -R) >${_dot_file}
 633     echo '# ABS Guide: is_spammer.bash; v2, 2004-msz' >>${_dot_file}
 634     echo >>${_dot_file}
 635     echo 'digraph G {' >>${_dot_file}
 636 
 637     if [ ${#known_name[@]} -gt 0 ]
 638     then
 639         echo >>${_dot_file}
 640         echo '# Known domain name nodes' >>${_dot_file}
 641         _dd_cnt=${#known_name[@]}
 642         for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
 643         do
 644             printf '    N%04u [label="%s"] ;\n' \
 645                    "${_dd}" "${known_name[${_dd}]}" >>${_dot_file}
 646         done
 647     fi
 648 
 649     if [ ${#known_address[@]} -gt 0 ]
 650     then
 651         echo >>${_dot_file}
 652         echo '# Known address nodes' >>${_dot_file}
 653         _dd_cnt=${#known_address[@]}
 654         for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
 655         do
 656             printf '    A%04u [label="%s"] ;\n' \
 657                    "${_dd}" "${known_address[${_dd}]}" >>${_dot_file}
 658         done
 659     fi
 660 
 661     echo                                   >>${_dot_file}
 662     echo '/*'                              >>${_dot_file}
 663     echo ' * Known relationships :: User conversion to'  >>${_dot_file}
 664     echo ' * graphic form by hand or program required.'  >>${_dot_file}
 665     echo ' *'                              >>${_dot_file}
 666 
 667     if [ ${#auth_chain[@]} -gt 0 ]
 668     then
 669         echo >>${_dot_file}
 670         echo '# Authority reference edges followed and field source.'  >>${_dot_file}
 671         dump_to_dot auth_chain AC
 672     fi
 673 
 674     if [ ${#ref_chain[@]} -gt 0 ]
 675     then
 676         echo >>${_dot_file}
 677         echo '# Name reference edges followed and field source.'  >>${_dot_file}
 678         dump_to_dot ref_chain RC
 679     fi
 680 
 681     if [ ${#name_address[@]} -gt 0 ]
 682     then
 683         echo >>${_dot_file}
 684         echo '# Known name->address edges' >>${_dot_file}
 685         dump_to_dot name_address NA
 686     fi
 687 
 688     if [ ${#name_srvc[@]} -gt 0 ]
 689     then
 690         echo >>${_dot_file}
 691         echo '# Known name->service edges' >>${_dot_file}
 692         dump_to_dot name_srvc NS
 693     fi
 694 
 695     if [ ${#name_resource[@]} -gt 0 ]
 696     then
 697         echo >>${_dot_file}
 698         echo '# Known name->resource edges' >>${_dot_file}
 699         dump_to_dot name_resource NR
 700     fi
 701 
 702     if [ ${#parent_child[@]} -gt 0 ]
 703     then
 704         echo >>${_dot_file}
 705         echo '# Known parent->child edges' >>${_dot_file}
 706         dump_to_dot parent_child PC
 707     fi
 708 
 709     if [ ${#list_server[@]} -gt 0 ]
 710     then
 711         echo >>${_dot_file}
 712         echo '# Known Blacklist nodes' >>${_dot_file}
 713         _dd_cnt=${#list_server[@]}
 714         for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
 715         do
 716             printf '    LS%04u [label="%s"] ;\n' \
 717                    "${_dd}" "${list_server[${_dd}]}" >>${_dot_file}
 718         done
 719     fi
 720 
 721     unique_lines address_hits address_hits
 722     if [ ${#address_hits[@]} -gt 0 ]
 723     then
 724         echo >>${_dot_file}
 725         echo '# Known address->Blacklist_hit edges' >>${_dot_file}
 726         echo '# CAUTION: dig warnings can trigger false hits.' >>${_dot_file}
 727         dump_to_dot address_hits AH
 728     fi
 729     echo          >>${_dot_file}
 730     echo ' *'     >>${_dot_file}
 731     echo ' * That is a lot of relationships. Happy graphing.' >>${_dot_file}
 732     echo ' */'    >>${_dot_file}
 733     echo '}'      >>${_dot_file}
 734     return 0
 735 }
 736 
 737 # # # # 'Hunt the Spammer' execution flow # # # #
 738 
 739 #  Execution trace is enabled by setting the
 740 #+ environment variable SPAMMER_TRACE to the name of a writable file.
 741 declare -a _trace_log
 742 declare _log_file
 743 
 744 # Function to fill the trace log
 745 trace_logger() {
 746     _trace_log[${#_trace_log[@]}]=${_pend_current_}
 747 }
 748 
 749 # Dump trace log to file function variable.
 750 declare -f _log_dump
 751 _log_dump=pend_dummy   # Initially a no-op.
 752 
 753 # Dump the trace log to a file.
 754 dump_log() {
 755     local -i _dl_cnt
 756     _dl_cnt=${#_trace_log[@]}
 757     for (( _dl = 0 ; _dl < _dl_cnt ; _dl++ ))
 758     do
 759         echo ${_trace_log[${_dl}]} >> ${_log_file}
 760     done
 761     _dl_cnt=${#_pending_[@]}
 762     if [ ${_dl_cnt} -gt 0 ]
 763     then
 764         _dl_cnt=${_dl_cnt}-1
 765         echo '# # # Operations stack not empty # # #' >> ${_log_file}
 766         for (( _dl = ${_dl_cnt} ; _dl >= 0 ; _dl-- ))
 767         do
 768             echo ${_pending_[${_dl}]} >> ${_log_file}
 769         done
 770     fi
 771 }
 772 
 773 # # # Utility program 'dig' wrappers # # #
 774 #
 775 #  These wrappers are derived from the
 776 #+ examples shown in dig_wrappers.bash.
 777 #
 778 #  The major difference is these return
 779 #+ their results as a list in an array.
 780 #
 781 #  See dig_wrappers.bash for details and
 782 #+ use that script to develop any changes.
 783 #
 784 # # #
 785 
 786 # Short form answer: 'dig' parses answer.
 787 
 788 # Forward lookup :: Name -> Address
 789 # short_fwd <domain_name> <array_name>
 790 short_fwd() {
 791     local -a _sf_reply
 792     local -i _sf_rc
 793     local -i _sf_cnt
 794     IFS=${NO_WSP}
 795 echo -n '.'
 796 # echo 'sfwd: '${1}
 797     _sf_reply=( $(dig +short ${1} -c in -t a 2>/dev/null) )
 798     _sf_rc=$?
 799     if [ ${_sf_rc} -ne 0 ]
 800     then
 801         _trace_log[${#_trace_log[@]}]='# # # Lookup error '${_sf_rc}' on '${1}' # # #'
 802 # [ ${_sf_rc} -ne 9 ] && pend_drop
 803         return ${_sf_rc}
 804     else
 805         # Some versions of 'dig' return warnings on stdout.
 806         _sf_cnt=${#_sf_reply[@]}
 807         for (( _sf = 0 ; _sf < ${_sf_cnt} ; _sf++ ))
 808         do
 809             [ 'x'${_sf_reply[${_sf}]:0:2} == 'x;;' ] &&
 810                 unset _sf_reply[${_sf}]
 811         done
 812         eval $2=\( \$\{_sf_reply\[@\]\} \)
 813     fi
 814     return 0
 815 }
 816 
 817 # Reverse lookup :: Address -> Name
 818 # short_rev <ip_address> <array_name>
 819 short_rev() {
 820     local -a _sr_reply
 821     local -i _sr_rc
 822     local -i _sr_cnt
 823     IFS=${NO_WSP}
 824 echo -n '.'
 825 # echo 'srev: '${1}
 826     _sr_reply=( $(dig +short -x ${1} 2>/dev/null) )
 827     _sr_rc=$?
 828     if [ ${_sr_rc} -ne 0 ]
 829     then
 830         _trace_log[${#_trace_log[@]}]='# # # Lookup error '${_sr_rc}' on '${1}' # # #'
 831 # [ ${_sr_rc} -ne 9 ] && pend_drop
 832         return ${_sr_rc}
 833     else
 834         # Some versions of 'dig' return warnings on stdout.
 835         _sr_cnt=${#_sr_reply[@]}
 836         for (( _sr = 0 ; _sr < ${_sr_cnt} ; _sr++ ))
 837         do
 838             [ 'x'${_sr_reply[${_sr}]:0:2} == 'x;;' ] &&
 839                 unset _sr_reply[${_sr}]
 840         done
 841         eval $2=\( \$\{_sr_reply\[@\]\} \)
 842     fi
 843     return 0
 844 }
 845 
 846 # Special format lookup used to query blacklist servers.
 847 # short_text <ip_address> <array_name>
 848 short_text() {
 849     local -a _st_reply
 850     local -i _st_rc
 851     local -i _st_cnt
 852     IFS=${NO_WSP}
 853 # echo 'stxt: '${1}
 854     _st_reply=( $(dig +short ${1} -c in -t txt 2>/dev/null) )
 855     _st_rc=$?
 856     if [ ${_st_rc} -ne 0 ]
 857     then
 858         _trace_log[${#_trace_log[@]}]='# # # Text lookup error '${_st_rc}' on '${1}' # # #'
 859 # [ ${_st_rc} -ne 9 ] && pend_drop
 860         return ${_st_rc}
 861     else
 862         # Some versions of 'dig' return warnings on stdout.
 863         _st_cnt=${#_st_reply[@]}
 864         for (( _st = 0 ; _st < ${#_st_cnt} ; _st++ ))
 865         do
 866             [ 'x'${_st_reply[${_st}]:0:2} == 'x;;' ] &&
 867                 unset _st_reply[${_st}]
 868         done
 869         eval $2=\( \$\{_st_reply\[@\]\} \)
 870     fi
 871     return 0
 872 }
 873 
 874 # The long forms, a.k.a., the parse it yourself versions
 875 
 876 # RFC 2782   Service lookups
 877 # dig +noall +nofail +answer _ldap._tcp.openldap.org -t srv
 878 # _<service>._<protocol>.<domain_name>
 879 # _ldap._tcp.openldap.org. 3600   IN      SRV     0 0 389 ldap.openldap.org.
 880 # domain TTL Class SRV Priority Weight Port Target
 881 
 882 # Forward lookup :: Name -> poor man's zone transfer
 883 # long_fwd <domain_name> <array_name>
 884 long_fwd() {
 885     local -a _lf_reply
 886     local -i _lf_rc
 887     local -i _lf_cnt
 888     IFS=${NO_WSP}
 889 echo -n ':'
 890 # echo 'lfwd: '${1}
 891     _lf_reply=( $(
 892         dig +noall +nofail +answer +authority +additional \
 893             ${1} -t soa ${1} -t mx ${1} -t any 2>/dev/null) )
 894     _lf_rc=$?
 895     if [ ${_lf_rc} -ne 0 ]
 896     then
 897         _trace_log[${#_trace_log[@]}]='# # # Zone lookup error '${_lf_rc}' on '${1}' # # #'
 898 # [ ${_lf_rc} -ne 9 ] && pend_drop
 899         return ${_lf_rc}
 900     else
 901         # Some versions of 'dig' return warnings on stdout.
 902         _lf_cnt=${#_lf_reply[@]}
 903         for (( _lf = 0 ; _lf < ${_lf_cnt} ; _lf++ ))
 904         do
 905             [ 'x'${_lf_reply[${_lf}]:0:2} == 'x;;' ] &&
 906                 unset _lf_reply[${_lf}]
 907         done
 908         eval $2=\( \$\{_lf_reply\[@\]\} \)
 909     fi
 910     return 0
 911 }
 912 #   The reverse lookup domain name corresponding to the IPv6 address:
 913 #       4321:0:1:2:3:4:567:89ab
 914 #   would be (nibble, I.E: Hexdigit) reversed:
 915 #   b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.IP6.ARPA.
 916 
 917 # Reverse lookup :: Address -> poor man's delegation chain
 918 # long_rev <rev_ip_address> <array_name>
 919 long_rev() {
 920     local -a _lr_reply
 921     local -i _lr_rc
 922     local -i _lr_cnt
 923     local _lr_dns
 924     _lr_dns=${1}'.in-addr.arpa.'
 925     IFS=${NO_WSP}
 926 echo -n ':'
 927 # echo 'lrev: '${1}
 928     _lr_reply=( $(
 929          dig +noall +nofail +answer +authority +additional \
 930              ${_lr_dns} -t soa ${_lr_dns} -t any 2>/dev/null) )
 931     _lr_rc=$?
 932     if [ ${_lr_rc} -ne 0 ]
 933     then
 934         _trace_log[${#_trace_log[@]}]='# # # Delegation lookup error '${_lr_rc}' on '${1}' # # #'
 935 # [ ${_lr_rc} -ne 9 ] && pend_drop
 936         return ${_lr_rc}
 937     else
 938         # Some versions of 'dig' return warnings on stdout.
 939         _lr_cnt=${#_lr_reply[@]}
 940         for (( _lr = 0 ; _lr < ${_lr_cnt} ; _lr++ ))
 941         do
 942             [ 'x'${_lr_reply[${_lr}]:0:2} == 'x;;' ] &&
 943                 unset _lr_reply[${_lr}]
 944         done
 945         eval $2=\( \$\{_lr_reply\[@\]\} \)
 946     fi
 947     return 0
 948 }
 949 
 950 # # # Application specific functions # # #
 951 
 952 # Mung a possible name; suppresses root and TLDs.
 953 # name_fixup <string>
 954 name_fixup(){
 955     local -a _nf_tmp
 956     local -i _nf_end
 957     local _nf_str
 958     local IFS
 959     _nf_str=$(to_lower ${1})
 960     _nf_str=$(to_dot ${_nf_str})
 961     _nf_end=${#_nf_str}-1
 962     [ ${_nf_str:${_nf_end}} != '.' ] &&
 963         _nf_str=${_nf_str}'.'
 964     IFS=${ADR_IFS}
 965     _nf_tmp=( ${_nf_str} )
 966     IFS=${WSP_IFS}
 967     _nf_end=${#_nf_tmp[@]}
 968     case ${_nf_end} in
 969     0) # No dots, only dots
 970         echo
 971         return 1
 972     ;;
 973     1) # Only a TLD.
 974         echo
 975         return 1
 976     ;;
 977     2) # Maybe okay.
 978        echo ${_nf_str}
 979        return 0
 980        # Needs a lookup table?
 981        if [ ${#_nf_tmp[1]} -eq 2 ]
 982        then # Country coded TLD.
 983            echo
 984            return 1
 985        else
 986            echo ${_nf_str}
 987            return 0
 988        fi
 989     ;;
 990     esac
 991     echo ${_nf_str}
 992     return 0
 993 }
 994 
 995 # Grope and mung original input(s).
 996 split_input() {
 997     [ ${#uc_name[@]} -gt 0 ] || return 0
 998     local -i _si_cnt
 999     local -i _si_len
 1000     local _si_str
 1001     unique_lines uc_name uc_name
 1002     _si_cnt=${#uc_name[@]}
 1003     for (( _si = 0 ; _si < _si_cnt ; _si++ ))
 1004     do
 1005         _si_str=${uc_name[$_si]}
 1006         if is_address ${_si_str}
 1007         then
 1008             uc_address[${#uc_address[@]}]=${_si_str}
 1009             unset uc_name[$_si]
 1010         else
 1011             if ! uc_name[$_si]=$(name_fixup ${_si_str})
 1012             then
 1013                 unset ucname[$_si]
 1014             fi
 1015         fi
 1016     done
 1017     uc_name=( ${uc_name[@]} )
 1018     _si_cnt=${#uc_name[@]}
 1019     _trace_log[${#_trace_log[@]}]='# # # Input '${_si_cnt}' unchecked name input(s). # # #'
 1020     _si_cnt=${#uc_address[@]}
 1021     _trace_log[${#_trace_log[@]}]='# # # Input '${_si_cnt}' unchecked address input(s). # # #'
 1022     return 0
 1023 }
 1024 
 1025 # # # Discovery functions -- recursively interlocked by external data # # #
 1026 # # # The leading 'if list is empty; return 0' in each is required. # # #
 1027 
 1028 # Recursion limiter
 1029 # limit_chk() <next_level>
 1030 limit_chk() {
 1031     local -i _lc_lmt
 1032     # Check indirection limit.
 1033     if [ ${indirect} -eq 0 ] || [ $# -eq 0 ]
 1034     then
 1035         # The 'do-forever' choice
 1036         echo 1                 # Any value will do.
 1037         return 0               # OK to continue.
 1038     else
 1039         # Limiting is in effect.
 1040         if [ ${indirect} -lt ${1} ]
 1041         then
 1042             echo ${1}          # Whatever.
 1043             return 1           # Stop here.
 1044         else
 1045             _lc_lmt=${1}+1     # Bump the given limit.
 1046             echo ${_lc_lmt}    # Echo it.
 1047             return 0           # OK to continue.
 1048         fi
 1049     fi
 1050 }
 1051 
 1052 # For each name in uc_name:
 1053 #     Move name to chk_name.
 1054 #     Add addresses to uc_address.
 1055 #     Pend expand_input_address.
 1056 #     Repeat until nothing new found.
 1057 # expand_input_name <indirection_limit>
 1058 expand_input_name() {
 1059     [ ${#uc_name[@]} -gt 0 ] || return 0
 1060     local -a _ein_addr
 1061     local -a _ein_new
 1062     local -i _ucn_cnt
 1063     local -i _ein_cnt
 1064     local _ein_tst
 1065     _ucn_cnt=${#uc_name[@]}
 1066 
 1067     if  ! _ein_cnt=$(limit_chk ${1})
 1068     then
 1069         return 0
 1070     fi
 1071 
 1072     for (( _ein = 0 ; _ein < _ucn_cnt ; _ein++ ))
 1073     do
 1074         if short_fwd ${uc_name[${_ein}]} _ein_new
 1075         then
 1076             for (( _ein_cnt = 0 ; _ein_cnt < ${#_ein_new[@]}; _ein_cnt++ ))
 1077             do
 1078                 _ein_tst=${_ein_new[${_ein_cnt}]}
 1079                 if is_address ${_ein_tst}
 1080                 then
 1081                     _ein_addr[${#_ein_addr[@]}]=${_ein_tst}
 1082                 fi
 1083            done
 1084         fi
 1085     done
 1086     unique_lines _ein_addr _ein_addr     # Scrub duplicates.
 1087     edit_exact chk_address _ein_addr     # Scrub pending detail.
 1088     edit_exact known_address _ein_addr   # Scrub already detailed.
 1089     if [ ${#_ein_addr[@]} -gt 0 ]        # Anything new?
 1090     then
 1091         uc_address=( ${uc_address[@]} ${_ein_addr[@]} )
 1092         pend_func expand_input_address ${1}
 1093         _trace_log[${#_trace_log[@]}]='# # # Added '${#_ein_addr[@]}' unchecked address input(s). # # #'
 1094     fi
 1095     edit_exact chk_name uc_name          # Scrub pending detail.
 1096     edit_exact known_name uc_name        # Scrub already detailed.
 1097     if [ ${#uc_name[@]} -gt 0 ]
 1098     then
 1099         chk_name=( ${chk_name[@]} ${uc_name[@]}  )
 1100         pend_func detail_each_name ${1}
 1101     fi
 1102     unset uc_name[@]
 1103     return 0
 1104 }
 1105 
 1106 # For each address in uc_address:
 1107 #     Move address to chk_address.
 1108 #     Add names to uc_name.
 1109 #     Pend expand_input_name.
 1110 #     Repeat until nothing new found.
 1111 # expand_input_address <indirection_limit>
 1112 expand_input_address() {
 1113     [ ${#uc_address[@]} -gt 0 ] || return 0
 1114     local -a _eia_addr
 1115     local -a _eia_name
 1116     local -a _eia_new
 1117     local -i _uca_cnt
 1118     local -i _eia_cnt
 1119     local _eia_tst
 1120     unique_lines uc_address _eia_addr
 1121     unset uc_address[@]
 1122     edit_exact been_there_addr _eia_addr
 1123     _uca_cnt=${#_eia_addr[@]}
 1124     [ ${_uca_cnt} -gt 0 ] &&
 1125         been_there_addr=( ${been_there_addr[@]} ${_eia_addr[@]} )
 1126 
 1127     for (( _eia = 0 ; _eia < _uca_cnt ; _eia++ ))
 1128     do
 1129             if short_rev ${_eia_addr[${_eia}]} _eia_new
 1130             then
 1131                 for (( _eia_cnt = 0 ; _eia_cnt < ${#_eia_new[@]} ; _eia_cnt++ ))
 1132                 do
 1133                     _eia_tst=${_eia_new[${_eia_cnt}]}
 1134                     if _eia_tst=$(name_fixup ${_eia_tst})
 1135                     then
 1136                         _eia_name[${#_eia_name[@]}]=${_eia_tst}
 1137                     fi
 1138                 done
 1139             fi
 1140     done
 1141     unique_lines _eia_name _eia_name     # Scrub duplicates.
 1142     edit_exact chk_name _eia_name        # Scrub pending detail.
 1143     edit_exact known_name _eia_name      # Scrub already detailed.
 1144     if [ ${#_eia_name[@]} -gt 0 ]        # Anything new?
 1145     then
 1146         uc_name=( ${uc_name[@]} ${_eia_name[@]} )
 1147         pend_func expand_input_name ${1}
 1148         _trace_log[${#_trace_log[@]}]='# # # Added '${#_eia_name[@]}' unchecked name input(s). # # #'
 1149     fi
 1150     edit_exact chk_address _eia_addr     # Scrub pending detail.
 1151     edit_exact known_address _eia_addr   # Scrub already detailed.
 1152     if [ ${#_eia_addr[@]} -gt 0 ]        # Anything new?
 1153     then
 1154         chk_address=( ${chk_address[@]} ${_eia_addr[@]} )
 1155         pend_func detail_each_address ${1}
 1156     fi
 1157     return 0
 1158 }
 1159 
 1160 # The parse-it-yourself zone reply.
 1161 # The input is the chk_name list.
 1162 # detail_each_name <indirection_limit>
 1163 detail_each_name() {
 1164     [ ${#chk_name[@]} -gt 0 ] || return 0
 1165     local -a _den_chk       # Names to check
 1166     local -a _den_name      # Names found here
 1167     local -a _den_address   # Addresses found here
 1168     local -a _den_pair      # Pairs found here
 1169     local -a _den_rev       # Reverse pairs found here
 1170     local -a _den_tmp       # Line being parsed
 1171     local -a _den_auth      # SOA contact being parsed
 1172     local -a _den_new       # The zone reply
 1173     local -a _den_pc        # Parent-Child gets big fast
 1174     local -a _den_ref       # So does reference chain
 1175     local -a _den_nr        # Name-Resource can be big
 1176     local -a _den_na        # Name-Address
 1177     local -a _den_ns        # Name-Service
 1178     local -a _den_achn      # Chain of Authority
 1179     local -i _den_cnt       # Count of names to detail
 1180     local -i _den_lmt       # Indirection limit
 1181     local _den_who          # Named being processed
 1182     local _den_rec          # Record type being processed
 1183     local _den_cont         # Contact domain
 1184     local _den_str          # Fixed up name string
 1185     local _den_str2         # Fixed up reverse
 1186     local IFS=${WSP_IFS}
 1187 
 1188     # Local, unique copy of names to check
 1189     unique_lines chk_name _den_chk
 1190     unset chk_name[@]       # Done with globals.
 1191 
 1192     # Less any names already known
 1193     edit_exact known_name _den_chk
 1194     _den_cnt=${#_den_chk[@]}
 1195 
 1196     # If anything left, add to known_name.
 1197     [ ${_den_cnt} -gt 0 ] &&
 1198         known_name=( ${known_name[@]} ${_den_chk[@]} )
 1199 
 1200     # for the list of (previously) unknown names . . .
 1201     for (( _den = 0 ; _den < _den_cnt ; _den++ ))
 1202     do
 1203         _den_who=${_den_chk[${_den}]}
 1204         if long_fwd ${_den_who} _den_new
 1205         then
 1206             unique_lines _den_new _den_new
 1207             if [ ${#_den_new[@]} -eq 0 ]
 1208             then
 1209                 _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who}
 1210             fi
 1211 
 1212             # Parse each line in the reply.
 1213             for (( _line = 0 ; _line < ${#_den_new[@]} ; _line++ ))
 1214             do
 1215                 IFS=${NO_WSP}$'\x09'$'\x20'
 1216                 _den_tmp=( ${_den_new[${_line}]} )
 1217                 IFS=${WSP_IFS}
 1218                 # If usable record and not a warning message . . .
 1219                 if [ ${#_den_tmp[@]} -gt 4 ] && [ 'x'${_den_tmp[0]} != 'x;;' ]
 1220                 then
 1221                     _den_rec=${_den_tmp[3]}
 1222                     _den_nr[${#_den_nr[@]}]=${_den_who}' '${_den_rec}
 1223                     # Begin at RFC1033 (+++)
 1224                     case ${_den_rec} in
 1225 
 1226                          #<name>  [<ttl>]  [<class>]  SOA  <origin>  <person>
 1227                     SOA) # Start Of Authority
 1228                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1229                         then
 1230                             _den_name[${#_den_name[@]}]=${_den_str}
 1231                             _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str}' SOA'
 1232                             # SOA origin -- domain name of master zone record
 1233                             if _den_str2=$(name_fixup ${_den_tmp[4]})
 1234                             then
 1235                                 _den_name[${#_den_name[@]}]=${_den_str2}
 1236                                 _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str2}' SOA.O'
 1237                             fi
 1238                             # Responsible party e-mail address (possibly bogus).
 1239                             # Possibility of first.last@domain.name ignored.
 1240                             set -f
 1241                             if _den_str2=$(name_fixup ${_den_tmp[5]})
 1242                             then
 1243                                 IFS=${ADR_IFS}
 1244                                 _den_auth=( ${_den_str2} )
 1245                                 IFS=${WSP_IFS}
 1246                                 if [ ${#_den_auth[@]} -gt 2 ]
 1247                                 then
 1248                                      _den_cont=${_den_auth[1]}
 1249                                      for (( _auth = 2 ; _auth < ${#_den_auth[@]} ; _auth++ ))
 1250                                      do
 1251                                        _den_cont=${_den_cont}'.'${_den_auth[${_auth}]}
 1252                                      done
 1253                                      _den_name[${#_den_name[@]}]=${_den_cont}'.'
 1254                                      _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_cont}'. SOA.C'
 1255                                 fi
 1256                             fi
 1257                             set +f
 1258                         fi
 1259                     ;;
 1260 
 1261 
 1262                     A) # IP(v4) Address Record
 1263                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1264                         then
 1265                             _den_name[${#_den_name[@]}]=${_den_str}
 1266                             _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str}
 1267                             _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]}
 1268                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' A'
 1269                         else
 1270                             _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain'
 1271                             _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]}
 1272                             _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain A'
 1273                         fi
 1274                         _den_address[${#_den_address[@]}]=${_den_tmp[4]}
 1275                         _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]}
 1276                     ;;
 1277 
 1278                     NS) # Name Server Record
 1279                         # Domain name being serviced (may be other than current)
 1280                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1281                         then
 1282                             _den_name[${#_den_name[@]}]=${_den_str}
 1283                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' NS'
 1284 
 1285                             # Domain name of service provider
 1286                             if _den_str2=$(name_fixup ${_den_tmp[4]})
 1287                             then
 1288                                 _den_name[${#_den_name[@]}]=${_den_str2}
 1289                                 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' NSH'
 1290                                 _den_ns[${#_den_ns[@]}]=${_den_str2}' NS'
 1291                                 _den_pc[${#_den_pc[@]}]=${_den_str}' '${_den_str2}
 1292                             fi
 1293                         fi
 1294                     ;;
 1295 
 1296                     MX) # Mail Server Record
 1297                         # Domain name being serviced (wildcards not handled here)
 1298                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1299                         then
 1300                             _den_name[${#_den_name[@]}]=${_den_str}
 1301                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MX'
 1302                         fi
 1303                         # Domain name of service provider
 1304                         if _den_str=$(name_fixup ${_den_tmp[5]})
 1305                         then
 1306                             _den_name[${#_den_name[@]}]=${_den_str}
 1307                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MXH'
 1308                             _den_ns[${#_den_ns[@]}]=${_den_str}' MX'
 1309                             _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
 1310                         fi
 1311                     ;;
 1312 
 1313                     PTR) # Reverse address record
 1314                          # Special name
 1315                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1316                         then
 1317                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' PTR'
 1318                             # Host name (not a CNAME)
 1319                             if _den_str2=$(name_fixup ${_den_tmp[4]})
 1320                             then
 1321                                 _den_rev[${#_den_rev[@]}]=${_den_str}' '${_den_str2}
 1322                                 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' PTRH'
 1323                                 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
 1324                             fi
 1325                         fi
 1326                     ;;
 1327 
 1328                     AAAA) # IP(v6) Address Record
 1329                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1330                         then
 1331                             _den_name[${#_den_name[@]}]=${_den_str}
 1332                             _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str}
 1333                             _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]}
 1334                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' AAAA'
 1335                         else
 1336                             _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain'
 1337                             _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]}
 1338                             _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain'
 1339                         fi
 1340                         # No processing for IPv6 addresses
 1341                             _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]}
 1342                     ;;
 1343 
 1344                     CNAME) # Alias name record
 1345                            # Nickname
 1346                         if _den_str=$(name_fixup ${_den_tmp[0]})
 1347                         then
 1348                             _den_name[${#_den_name[@]}]=${_den_str}
 1349                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CNAME'
 1350                             _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
 1351                         fi
 1352                         # Hostname
 1353                         if _den_str=$(name_fixup ${_den_tmp[4]})
 1354                         then
 1355                             _den_name[${#_den_name[@]}]=${_den_str}
 1356                             _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CHOST'
 1357                             _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
 1358                         fi
 1359                     ;;
 1360 #                   TXT)
 1361 #                   ;;
 1362                     esac
 1363                 fi
 1364             done
 1365         else # Lookup error == 'A' record 'unknown address'
 1366             _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who}
 1367         fi
 1368     done
 1369 
 1370     # Control dot array growth.
 1371     unique_lines _den_achn _den_achn      # Works best, all the same.
 1372     edit_exact auth_chain _den_achn       # Works best, unique items.
 1373     if [ ${#_den_achn[@]} -gt 0 ]
 1374     then
 1375         IFS=${NO_WSP}
 1376         auth_chain=( ${auth_chain[@]} ${_den_achn[@]} )
 1377         IFS=${WSP_IFS}
 1378     fi
 1379 
 1380     unique_lines _den_ref _den_ref      # Works best, all the same.
 1381     edit_exact ref_chain _den_ref       # Works best, unique items.
 1382     if [ ${#_den_ref[@]} -gt 0 ]
 1383     then
 1384         IFS=${NO_WSP}
 1385         ref_chain=( ${ref_chain[@]} ${_den_ref[@]} )
 1386         IFS=${WSP_IFS}
 1387     fi
 1388 
 1389     unique_lines _den_na _den_na
 1390     edit_exact name_address _den_na
 1391     if [ ${#_den_na[@]} -gt 0 ]
 1392     then
 1393         IFS=${NO_WSP}
 1394         name_address=( ${name_address[@]} ${_den_na[@]} )
 1395         IFS=${WSP_IFS}
 1396     fi
 1397 
 1398     unique_lines _den_ns _den_ns
 1399     edit_exact name_srvc _den_ns
 1400     if [ ${#_den_ns[@]} -gt 0 ]
 1401     then
 1402         IFS=${NO_WSP}
 1403         name_srvc=( ${name_srvc[@]} ${_den_ns[@]} )
 1404         IFS=${WSP_IFS}
 1405     fi
 1406 
 1407     unique_lines _den_nr _den_nr
 1408     edit_exact name_resource _den_nr
 1409     if [ ${#_den_nr[@]} -gt 0 ]
 1410     then
 1411         IFS=${NO_WSP}
 1412         name_resource=( ${name_resource[@]} ${_den_nr[@]} )
 1413         IFS=${WSP_IFS}
 1414     fi
 1415 
 1416     unique_lines _den_pc _den_pc
 1417     edit_exact parent_child _den_pc
 1418     if [ ${#_den_pc[@]} -gt 0 ]
 1419     then
 1420         IFS=${NO_WSP}
 1421         parent_child=( ${parent_child[@]} ${_den_pc[@]} )
 1422         IFS=${WSP_IFS}
 1423     fi
 1424 
 1425     # Update list known_pair (Address and Name).
 1426     unique_lines _den_pair _den_pair
 1427     edit_exact known_pair _den_pair
 1428     if [ ${#_den_pair[@]} -gt 0 ]  # Anything new?
 1429     then
 1430         IFS=${NO_WSP}
 1431         known_pair=( ${known_pair[@]} ${_den_pair[@]} )
 1432         IFS=${WSP_IFS}
 1433     fi
 1434 
 1435     # Update list of reverse pairs.
 1436     unique_lines _den_rev _den_rev
 1437     edit_exact reverse_pair _den_rev
 1438     if [ ${#_den_rev[@]} -gt 0 ]   # Anything new?
 1439     then
 1440         IFS=${NO_WSP}
 1441         reverse_pair=( ${reverse_pair[@]} ${_den_rev[@]} )
 1442         IFS=${WSP_IFS}
 1443     fi
 1444 
 1445     # Check indirection limit -- give up if reached.
 1446     if ! _den_lmt=$(limit_chk ${1})
 1447     then
 1448         return 0
 1449     fi
 1450 
 1451     # Execution engine is LIFO. Order of pend operations is important.
 1452     # Did we define any new addresses?
 1453     unique_lines _den_address _den_address    # Scrub duplicates.
 1454     edit_exact known_address _den_address     # Scrub already processed.
 1455     edit_exact un_address _den_address        # Scrub already waiting.
 1456     if [ ${#_den_address[@]} -gt 0 ]          # Anything new?
 1457     then
 1458         uc_address=( ${uc_address[@]} ${_den_address[@]} )
 1459         pend_func expand_input_address ${_den_lmt}
 1460         _trace_log[${#_trace_log[@]}]='# # # Added '${#_den_address[@]}' unchecked address(s). # # #'
 1461     fi
 1462 
 1463     # Did we find any new names?
 1464     unique_lines _den_name _den_name          # Scrub duplicates.
 1465     edit_exact known_name _den_name           # Scrub already processed.
 1466     edit_exact uc_name _den_name              # Scrub already waiting.
 1467     if [ ${#_den_name[@]} -gt 0 ]             # Anything new?
 1468     then
 1469         uc_name=( ${uc_name[@]} ${_den_name[@]} )
 1470         pend_func expand_input_name ${_den_lmt}
 1471         _trace_log[${#_trace_log[@]}]='# # # Added '${#_den_name[@]}' unchecked name(s). # # #'
 1472     fi
 1473     return 0
 1474 }
 1475 
 1476 # The parse-it-yourself delegation reply
 1477 # Input is the chk_address list.
 1478 # detail_each_address <indirection_limit>
 1479 detail_each_address() {
 1480     [ ${#chk_address[@]} -gt 0 ] || return 0
 1481     unique_lines chk_address chk_address
 1482     edit_exact known_address chk_address
 1483     if [ ${#chk_address[@]} -gt 0 ]
 1484     then
 1485         known_address=( ${known_address[@]} ${chk_address[@]} )
 1486         unset chk_address[@]
 1487     fi
 1488     return 0
 1489 }
 1490 
 1491 # # # Application specific output functions # # #
 1492 
 1493 # Pretty print the known pairs.
 1494 report_pairs() {
 1495     echo
 1496     echo 'Known network pairs.'
 1497     col_print known_pair 2 5 30
 1498 
 1499     if [ ${#auth_chain[@]} -gt 0 ]
 1500     then
 1501         echo
 1502         echo 'Known chain of authority.'
 1503         col_print auth_chain 2 5 30 55
 1504     fi
 1505 
 1506     if [ ${#reverse_pair[@]} -gt 0 ]
 1507     then
 1508         echo
 1509         echo 'Known reverse pairs.'
 1510         col_print reverse_pair 2 5 55
 1511     fi
 1512     return 0
 1513 }
 1514 
 1515 # Check an address against the list of blacklist servers.
 1516 # A good place to capture for GraphViz: address->status(server(reports))
 1517 # check_lists <ip_address>
 1518 check_lists() {
 1519     [ $# -eq 1 ] || return 1
 1520     local -a _cl_fwd_addr
 1521     local -a _cl_rev_addr
 1522     local -a _cl_reply
 1523     local -i _cl_rc
 1524     local -i _ls_cnt
 1525     local _cl_dns_addr
 1526     local _cl_lkup
 1527 
 1528     split_ip ${1} _cl_fwd_addr _cl_rev_addr
 1529     _cl_dns_addr=$(dot_array _cl_rev_addr)'.'
 1530     _ls_cnt=${#list_server[@]}
 1531     echo '    Checking address '${1}
 1532     for (( _cl = 0 ; _cl < _ls_cnt ; _cl++ ))
 1533     do
 1534         _cl_lkup=${_cl_dns_addr}${list_server[${_cl}]}
 1535         if short_text ${_cl_lkup} _cl_reply
 1536         then
 1537             if [ ${#_cl_reply[@]} -gt 0 ]
 1538             then
 1539                 echo '        Records from '${list_server[${_cl}]}
 1540                 address_hits[${#address_hits[@]}]=${1}' '${list_server[${_cl}]}
 1541                 _hs_RC=2
 1542                 for (( _clr = 0 ; _clr < ${#_cl_reply[@]} ; _clr++ ))
 1543                 do
 1544                     echo '            '${_cl_reply[${_clr}]}
 1545                 done
 1546             fi
 1547         fi
 1548     done
 1549     return 0
 1550 }
 1551 
 1552 # # # The usual application glue # # #
 1553 
 1554 # Who did it?
 1555 credits() {
 1556    echo
 1557    echo 'Advanced Bash Scripting Guide: is_spammer.bash, v2, 2004-msz'
 1558 }
 1559 
 1560 # How to use it?
 1561 # (See also, "Quickstart" at end of script.)
 1562 usage() {
 1563     cat <<-'_usage_statement_'
 1564     The script is_spammer.bash requires either one or two arguments.
 1565 
 1566     arg 1) May be one of:
 1567         a) A domain name
 1568         b) An IPv4 address
 1569         c) The name of a file with any mix of names
 1570            and addresses, one per line.
 1571 
 1572     arg 2) May be one of:
 1573         a) A Blacklist server domain name
 1574         b) The name of a file with Blacklist server
 1575            domain names, one per line.
 1576         c) If not present, a default list of (free)
 1577            Blacklist servers is used.
 1578         d) If a filename of an empty, readable, file
 1579            is given,
 1580            Blacklist server lookup is disabled.
 1581 
 1582     All script output is written to stdout.
 1583 
 1584     Return codes: 0 -> All OK, 1 -> Script failure,
 1585                   2 -> Something is Blacklisted.
 1586 
 1587     Requires the external program 'dig' from the 'bind-9'
 1588     set of DNS programs.  See: http://www.isc.org
 1589 
 1590     The domain name lookup depth limit defaults to 2 levels.
 1591     Set the environment variable SPAMMER_LIMIT to change.
 1592     SPAMMER_LIMIT=0 means 'unlimited'
 1593 
 1594     Limit may also be set on the command line.
 1595     If arg#1 is an integer, the limit is set to that value
 1596     and then the above argument rules are applied.
 1597 
 1598     Setting the environment variable 'SPAMMER_DATA' to a filename
 1599     will cause the script to write a GraphViz graphic file.
 1600 
 1601     For the development version;
 1602     Setting the environment variable 'SPAMMER_TRACE' to a filename
 1603     will cause the execution engine to log a function call trace.
 1604 
 1605 _usage_statement_
 1606 }
 1607 
 1608 # The default list of Blacklist servers:
 1609 # Many choices, see: http://www.spews.org/lists.html
 1610 
 1611 declare -a default_servers
 1612 # See: http://www.spamhaus.org (Conservative, well maintained)
 1613 default_servers[0]='sbl-xbl.spamhaus.org'
 1614 # See: http://ordb.org (Open mail relays)
 1615 default_servers[1]='relays.ordb.org'
 1616 # See: http://www.spamcop.net/ (You can report spammers here)
 1617 default_servers[2]='bl.spamcop.net'
 1618 # See: http://www.spews.org (An 'early detect' system)
 1619 default_servers[3]='l2.spews.dnsbl.sorbs.net'
 1620 # See: http://www.dnsbl.us.sorbs.net/using.shtml
 1621 default_servers[4]='dnsbl.sorbs.net'
 1622 # See: http://dsbl.org/usage (Various mail relay lists)
 1623 default_servers[5]='list.dsbl.org'
 1624 default_servers[6]='multihop.dsbl.org'
 1625 default_servers[7]='unconfirmed.dsbl.org'
 1626 
 1627 # User input argument #1
 1628 setup_input() {
 1629     if [ -e ${1} ] && [ -r ${1} ]  # Name of readable file
 1630     then
 1631         file_to_array ${1} uc_name
 1632         echo 'Using filename >'${1}'< as input.'
 1633     else
 1634         if is_address ${1}          # IP address?
 1635         then
 1636             uc_address=( ${1} )
 1637             echo 'Starting with address >'${1}'<'
 1638         else                       # Must be a name.
 1639             uc_name=( ${1} )
 1640             echo 'Starting with domain name >'${1}'<'
 1641         fi
 1642     fi
 1643     return 0
 1644 }
 1645 
 1646 # User input argument #2
 1647 setup_servers() {
 1648     if [ -e ${1} ] && [ -r ${1} ]  # Name of a readable file
 1649     then
 1650         file_to_array ${1} list_server
 1651         echo 'Using filename >'${1}'< as blacklist server list.'
 1652     else
 1653         list_server=( ${1} )
 1654         echo 'Using blacklist server >'${1}'<'
 1655     fi
 1656     return 0
 1657 }
 1658 
 1659 # User environment variable SPAMMER_TRACE
 1660 live_log_die() {
 1661     if [ ${SPAMMER_TRACE:=} ]    # Wants trace log?
 1662     then
 1663         if [ ! -e ${SPAMMER_TRACE} ]
 1664         then
 1665             if ! touch ${SPAMMER_TRACE} 2>/dev/null
 1666             then
 1667                 pend_func echo $(printf '%q\n' \
 1668                 'Unable to create log file >'${SPAMMER_TRACE}'<')
 1669                 pend_release
 1670                 exit 1
 1671             fi
 1672             _log_file=${SPAMMER_TRACE}
 1673             _pend_hook_=trace_logger
 1674             _log_dump=dump_log
 1675         else
 1676             if [ ! -w ${SPAMMER_TRACE} ]
 1677             then
 1678                 pend_func echo $(printf '%q\n' \
 1679                 'Unable to write log file >'${SPAMMER_TRACE}'<')
 1680                 pend_release
 1681                 exit 1
 1682             fi
 1683             _log_file=${SPAMMER_TRACE}
 1684             echo '' > ${_log_file}
 1685             _pend_hook_=trace_logger
 1686             _log_dump=dump_log
 1687         fi
 1688     fi
 1689     return 0
 1690 }
 1691 
 1692 # User environment variable SPAMMER_DATA
 1693 data_capture() {
 1694     if [ ${SPAMMER_DATA:=} ]    # Wants a data dump?
 1695     then
 1696         if [ ! -e ${SPAMMER_DATA} ]
 1697         then
 1698             if ! touch ${SPAMMER_DATA} 2>/dev/null
 1699             then
 1700                 pend_func echo $(printf '%q]n' \
 1701                 'Unable to create data output file >'${SPAMMER_DATA}'<')
 1702                 pend_release
 1703                 exit 1
 1704             fi
 1705             _dot_file=${SPAMMER_DATA}
 1706             _dot_dump=dump_dot
 1707         else
 1708             if [ ! -w ${SPAMMER_DATA} ]
 1709             then
 1710                 pend_func echo $(printf '%q\n' \
 1711                 'Unable to write data output file >'${SPAMMER_DATA}'<')
 1712                 pend_release
 1713                 exit 1
 1714             fi
 1715             _dot_file=${SPAMMER_DATA}
 1716             _dot_dump=dump_dot
 1717         fi
 1718     fi
 1719     return 0
 1720 }
 1721 
 1722 # Grope user specified arguments.
 1723 do_user_args() {
 1724     if [ $# -gt 0 ] && is_number $1
 1725     then
 1726         indirect=$1
 1727         shift
 1728     fi
 1729 
 1730     case $# in                     # Did user treat us well?
 1731         1)
 1732             if ! setup_input $1    # Needs error checking.
 1733             then
 1734                 pend_release
 1735                 $_log_dump
 1736                 exit 1
 1737             fi
 1738             list_server=( ${default_servers[@]} )
 1739             _list_cnt=${#list_server[@]}
 1740             echo 'Using default blacklist server list.'
 1741             echo 'Search depth limit: '${indirect}
 1742             ;;
 1743         2)
 1744             if ! setup_input $1    # Needs error checking.
 1745             then
 1746                 pend_release
 1747                 $_log_dump
 1748                 exit 1
 1749             fi
 1750             if ! setup_servers $2  # Needs error checking.
 1751             then
 1752                 pend_release
 1753                 $_log_dump
 1754                 exit 1
 1755             fi
 1756             echo 'Search depth limit: '${indirect}
 1757             ;;
 1758         *)
 1759             pend_func usage
 1760             pend_release
 1761             $_log_dump
 1762             exit 1
 1763             ;;
 1764     esac
 1765     return 0
 1766 }
 1767 
 1768 # A general purpose debug tool.
 1769 # list_array <array_name>
 1770 list_array() {
 1771     [ $# -eq 1 ] || return 1  # One argument required.
 1772 
 1773     local -a _la_lines
 1774     set -f
 1775     local IFS=${NO_WSP}
 1776     eval _la_lines=\(\ \$\{$1\[@\]\}\ \)
 1777     echo
 1778     echo "Element count "${#_la_lines[@]}" array "${1}
 1779     local _ln_cnt=${#_la_lines[@]}
 1780 
 1781     for (( _i = 0; _i < ${_ln_cnt}; _i++ ))
 1782     do
 1783         echo 'Element '$_i' >'${_la_lines[$_i]}'<'
 1784     done
 1785     set +f
 1786     return 0
 1787 }
 1788 
 1789 # # # 'Hunt the Spammer' program code # # #
 1790 pend_init                               # Ready stack engine.
 1791 pend_func credits                       # Last thing to print.
 1792 
 1793 # # # Deal with user # # #
 1794 live_log_die                            # Setup debug trace log.
 1795 data_capture                            # Setup data capture file.
 1796 echo
 1797 do_user_args $@
 1798 
 1799 # # # Haven't exited yet - There is some hope # # #
 1800 # Discovery group - Execution engine is LIFO - pend
 1801 # in reverse order of execution.
 1802 _hs_RC=0                                # Hunt the Spammer return code
 1803 pend_mark
 1804     pend_func report_pairs              # Report name-address pairs.
 1805 
 1806     # The two detail_* are mutually recursive functions.
 1807     # They also pend expand_* functions as required.
 1808     # These two (the last of ???) exit the recursion.
 1809     pend_func detail_each_address       # Get all resources of addresses.
 1810     pend_func detail_each_name          # Get all resources of names.
 1811 
 1812     #  The two expand_* are mutually recursive functions,
 1813     #+ which pend additional detail_* functions as required.
 1814     pend_func expand_input_address 1    # Expand input names by address.
 1815     pend_func expand_input_name 1       # #xpand input addresses by name.
 1816 
 1817     # Start with a unique set of names and addresses.
 1818     pend_func unique_lines uc_address uc_address
 1819     pend_func unique_lines uc_name uc_name
 1820 
 1821     # Separate mixed input of names and addresses.
 1822     pend_func split_input
 1823 pend_release
 1824 
 1825 # # # Pairs reported -- Unique list of IP addresses found
 1826 echo
 1827 _ip_cnt=${#known_address[@]}
 1828 if [ ${#list_server[@]} -eq 0 ]
 1829 then
 1830     echo 'Blacklist server list empty, none checked.'
 1831 else
 1832     if [ ${_ip_cnt} -eq 0 ]
 1833     then
 1834         echo 'Known address list empty, none checked.'
 1835     else
 1836         _ip_cnt=${_ip_cnt}-1   # Start at top.
 1837         echo 'Checking Blacklist servers.'
 1838         for (( _ip = _ip_cnt ; _ip >= 0 ; _ip-- ))
 1839         do
 1840             pend_func check_lists $( printf '%q\n' ${known_address[$_ip]} )
 1841         done
 1842     fi
 1843 fi
 1844 pend_release
 1845 $_dot_dump                   # Graphics file dump
 1846 $_log_dump                   # Execution trace
 1847 echo
 1848 
 1849 
 1850 ##############################
 1851 # Example output from script #
 1852 ##############################
 1853 :<<-'_is_spammer_outputs_'
 1854 
 1855 ./is_spammer.bash 0 web4.alojamentos7.com
 1856 
 1857 Starting with domain name >web4.alojamentos7.com<
 1858 Using default blacklist server list.
 1859 Search depth limit: 0
 1860 .:....::::...:::...:::.......::..::...:::.......::
 1861 Known network pairs.
 1862     66.98.208.97             web4.alojamentos7.com.
 1863     66.98.208.97             ns1.alojamentos7.com.
 1864     69.56.202.147            ns2.alojamentos.ws.
 1865     66.98.208.97             alojamentos7.com.
 1866     66.98.208.97             web.alojamentos7.com.
 1867     69.56.202.146            ns1.alojamentos.ws.
 1868     69.56.202.146            alojamentos.ws.
 1869     66.235.180.113           ns1.alojamentos.org.
 1870     66.235.181.192           ns2.alojamentos.org.
 1871     66.235.180.113           alojamentos.org.
 1872     66.235.180.113           web6.alojamentos.org.
 1873     216.234.234.30           ns1.theplanet.com.
 1874     12.96.160.115            ns2.theplanet.com.
 1875     216.185.111.52           mail1.theplanet.com.
 1876     69.56.141.4              spooling.theplanet.com.
 1877     216.185.111.40           theplanet.com.
 1878     216.185.111.40           www.theplanet.com.
 1879     216.185.111.52           mail.theplanet.com.
 1880 
 1881 Checking Blacklist servers.
 1882     Checking address 66.98.208.97
 1883         Records from dnsbl.sorbs.net
 1884             "Spam Received See: http://www.dnsbl.sorbs.net/lookup.shtml?66.98.208.97"
 1885     Checking address 69.56.202.147
 1886     Checking address 69.56.202.146
 1887     Checking address 66.235.180.113
 1888     Checking address 66.235.181.192
 1889     Checking address 216.185.111.40
 1890     Checking address 216.234.234.30
 1891     Checking address 12.96.160.115
 1892     Checking address 216.185.111.52
 1893     Checking address 69.56.141.4
 1894 
 1895 Advanced Bash Scripting Guide: is_spammer.bash, v2, 2004-msz
 1896 
 1897 _is_spammer_outputs_
 1898 
 1899 exit ${_hs_RC}
 1900 
 1901 ####################################################
 1902 #  The script ignores everything from here on down #
 1903 #+ because of the 'exit' command, just above.      #
 1904 ####################################################
 1905 
 1906 
 1907 
 1908 Quickstart
 1909 ==========
 1910 
 1911  Prerequisites
 1912 
 1913   Bash version 2.05b or 3.00 (bash --version)
 1914   A version of Bash which supports arrays. Array 
 1915   support is included by default Bash configurations.
 1916 
 1917   'dig,' version 9.x.x (dig $HOSTNAME, see first line of output)
 1918   A version of dig which supports the +short options. 
 1919   See: dig_wrappers.bash for details.
 1920 
 1921 
 1922  Optional Prerequisites
 1923 
 1924   'named,' a local DNS caching program. Any flavor will do.
 1925   Do twice: dig $HOSTNAME 
 1926   Check near bottom of output for: SERVER: 127.0.0.1#53
 1927   That means you have one running.
 1928 
 1929 
 1930  Optional Graphics Support
 1931 
 1932   'date,' a standard *nix thing. (date -R)
 1933 
 1934   dot Program to convert graphic description file to a 
 1935   diagram. (dot -V)
 1936   A part of the Graph-Viz set of programs.
 1937   See: [http://www.research.att.com/sw/tools/graphviz||GraphViz]
 1938 
 1939   'dotty,' a visual editor for graphic description files.
 1940   Also a part of the Graph-Viz set of programs.
 1941 
 1942 
 1943 
 1944 
 1945  Quick Start
 1946 
 1947 In the same directory as the is_spammer.bash script; 
 1948 Do: ./is_spammer.bash
 1949 
 1950  Usage Details
 1951 
 1952 1. Blacklist server choices.
 1953 
 1954   (a) To use default, built-in list: Do nothing.
 1955 
 1956   (b) To use your own list: 
 1957 
 1958     i. Create a file with a single Blacklist server 
 1959        domain name per line.
 1960 
 1961     ii. Provide that filename as the last argument to 
 1962         the script.
 1963 
 1964   (c) To use a single Blacklist server: Last argument 
 1965       to the script.
 1966 
 1967   (d) To disable Blacklist lookups:
 1968 
 1969     i. Create an empty file (touch spammer.nul)
 1970        Your choice of filename.
 1971 
 1972     ii. Provide the filename of that empty file as the 
 1973         last argument to the script.
 1974 
 1975 2. Search depth limit.
 1976 
 1977   (a) To use the default value of 2: Do nothing.
 1978 
 1979   (b) To set a different limit: 
 1980       A limit of 0 means: no limit.
 1981 
 1982     i. export SPAMMER_LIMIT=1
 1983        or whatever limit you want.
 1984 
 1985     ii. OR provide the desired limit as the first 
 1986        argument to the script.
 1987 
 1988 3. Optional execution trace log.
 1989 
 1990   (a) To use the default setting of no log output: Do nothing.
 1991 
 1992   (b) To write an execution trace log:
 1993       export SPAMMER_TRACE=spammer.log
 1994       or whatever filename you want.
 1995 
 1996 4. Optional graphic description file.
 1997 
 1998   (a) To use the default setting of no graphic file: Do nothing.
 1999 
 2000   (b) To write a Graph-Viz graphic description file:
 2001       export SPAMMER_DATA=spammer.dot
 2002       or whatever filename you want.
 2003 
 2004 5. Where to start the search.
 2005 
 2006   (a) Starting with a single domain name:
 2007 
 2008     i. Without a command line search limit: First 
 2009        argument to script.
 2010 
 2011     ii. With a command line search limit: Second 
 2012         argument to script.
 2013 
 2014   (b) Starting with a single IP address:
 2015 
 2016     i. Without a command line search limit: First 
 2017        argument to script.
 2018 
 2019     ii. With a command line search limit: Second 
 2020         argument to script.
 2021 
 2022   (c) Starting with (mixed) multiple name(s) and/or address(es):
 2023       Create a file with one name or address per line.
 2024       Your choice of filename.
 2025 
 2026     i. Without a command line search limit: Filename as 
 2027        first argument to script.
 2028 
 2029     ii. With a command line search limit: Filename as 
 2030         second argument to script.
 2031 
 2032 6. What to do with the display output.
 2033 
 2034   (a) To view display output on screen: Do nothing.
 2035 
 2036   (b) To save display output to a file: Redirect stdout to a filename.
 2037 
 2038   (c) To discard display output: Redirect stdout to /dev/null.
 2039 
 2040 7. Temporary end of decision making. 
 2041    press RETURN 
 2042    wait (optionally, watch the dots and colons).
 2043 
 2044 8. Optionally check the return code.
 2045 
 2046   (a) Return code 0: All OK
 2047 
 2048   (b) Return code 1: Script setup failure
 2049 
 2050   (c) Return code 2: Something was blacklisted.
 2051 
 2052 9. Where is my graph (diagram)?
 2053 
 2054 The script does not directly produce a graph (diagram). 
 2055 It only produces a graphic description file. You can 
 2056 process the graphic descriptor file that was output 
 2057 with the 'dot' program.
 2058 
 2059 Until you edit that descriptor file, to describe the 
 2060 relationships you want shown, all that you will get is 
 2061 a bunch of labeled name and address nodes.
 2062 
 2063 All of the script's discovered relationships are within 
 2064 a comment block in the graphic descriptor file, each 
 2065 with a descriptive heading.
 2066 
 2067 The editing required to draw a line between a pair of 
 2068 nodes from the information in the descriptor file may 
 2069 be done with a text editor. 
 2070 
 2071 Given these lines somewhere in the descriptor file:
 2072 
 2073 # Known domain name nodes
 2074 
 2075 N0000 [label="guardproof.info."] ;
 2076 
 2077 N0002 [label="third.guardproof.info."] ;
 2078 
 2079 
 2080 
 2081 # Known address nodes
 2082 
 2083 A0000 [label="61.141.32.197"] ;
 2084 
 2085 
 2086 
 2087 /*
 2088 
 2089 # Known name->address edges
 2090 
 2091 NA0000 third.guardproof.info. 61.141.32.197
 2092 
 2093 
 2094 
 2095 # Known parent->child edges
 2096 
 2097 PC0000 guardproof.info. third.guardproof.info.
 2098 
 2099  */
 2100 
 2101 Turn that into the following lines by substituting node 
 2102 identifiers into the relationships:
 2103 
 2104 # Known domain name nodes
 2105 
 2106 N0000 [label="guardproof.info."] ;
 2107 
 2108 N0002 [label="third.guardproof.info."] ;
 2109 
 2110 
 2111 
 2112 # Known address nodes
 2113 
 2114 A0000 [label="61.141.32.197"] ;
 2115 
 2116 
 2117 
 2118 # PC0000 guardproof.info. third.guardproof.info.
 2119 
 2120 N0000->N0002 ;
 2121 
 2122 
 2123 
 2124 # NA0000 third.guardproof.info. 61.141.32.197
 2125 
 2126 N0002->A0000 ;
 2127 
 2128 
 2129 
 2130 /*
 2131 
 2132 # Known name->address edges
 2133 
 2134 NA0000 third.guardproof.info. 61.141.32.197
 2135 
 2136 
 2137 
 2138 # Known parent->child edges
 2139 
 2140 PC0000 guardproof.info. third.guardproof.info.
 2141 
 2142  */
 2143 
 2144 Process that with the 'dot' program, and you have your 
 2145 first network diagram.
 2146 
 2147 In addition to the conventional graphic edges, the 
 2148 descriptor file includes similar format pair-data that 
 2149 describes services, zone records (sub-graphs?), 
 2150 blacklisted addresses, and other things which might be 
 2151 interesting to include in your graph. This additional 
 2152 information could be displayed as different node 
 2153 shapes, colors, line sizes, etc.
 2154 
 2155 The descriptor file can also be read and edited by a 
 2156 Bash script (of course). You should be able to find 
 2157 most of the functions required within the 
 2158 "is_spammer.bash" script.
 2159 
 2160 # End Quickstart.

To end this section, a review of the basics . . . and more.


Example A-27. Basics Reviewed

   1 #!/bin/bash
   2 # basics-reviewed.bash
   3 
   4 # File extension == *.bash == specific to Bash
   5 
   6 #   Copyright (c) Michael S. Zick, 2003; All rights reserved.
   7 #   License: Use in any form, for any purpose.
   8 #   Revision: $ID$
   9 #
  10 #              Edited for layout by M.C.
  11 #   (author of the "Advanced Bash Scripting Guide")
  12 
  13 
  14 #  This script tested under Bash versions 2.04, 2.05a and 2.05b.
  15 #  It may not work with earlier versions.
  16 #  This demonstration script generates one --intentional--
  17 #+ "command not found" error message. See line 394.
  18 
  19 #  The current Bash maintainer, Chet Ramey, has fixed the items noted
  20 #+ for an upcoming version of Bash.
  21 
  22 
  23 
  24         ###-------------------------------------------###
  25         ###  Pipe the output of this script to 'more' ###
  26         ###+ else it will scroll off the page.        ###
  27         ###                                           ###
  28         ###  You may also redirect its output         ###
  29         ###+ to a file for examination.               ###  
  30         ###-------------------------------------------###
  31 
  32 
  33 
  34 #  Most of the following points are described at length in
  35 #+ the text of the foregoing "Advanced Bash Scripting Guide."
  36 #  This demonstration script is mostly just a reorganized presentation.
  37 #      -- msz
  38 
  39 # Variables are not typed unless otherwise specified.
  40 
  41 #  Variables are named. Names must contain a non-digit.
  42 #  File descriptor names (as in, for example: 2>&1)
  43 #+ contain ONLY digits.
  44 
  45 # Parameters and Bash array elements are numbered.
  46 # (Parameters are very similar to Bash arrays.)
  47 
  48 # A variable name may be undefined (null reference).
  49 unset VarNull
  50 
  51 # A variable name may be defined but empty (null contents).
  52 VarEmpty=''                         # Two, adjacent, single quotes.
  53 
  54 # A variable name my be defined and non-empty
  55 VarSomething='Literal'
  56 
  57 # A variable may contain:
  58 #   * A whole number as a signed 32-bit (or larger) integer
  59 #   * A string
  60 # A variable may also be an array.
  61 
  62 #  A string may contain embedded blanks and may be treated
  63 #+ as if it where a function name with optional arguments.
  64 
  65 #  The names of variables and the names of functions
  66 #+ are in different namespaces.
  67 
  68 
  69 #  A variable may be defined as a Bash array either explicitly or
  70 #+ implicitly by the syntax of the assignment statement.
  71 #  Explicit:
  72 declare -a ArrayVar
  73 
  74 
  75 
  76 # The echo command is a built-in.
  77 echo $VarSomething
  78 
  79 # The printf command is a built-in.
  80 # Translate %s as: String-Format
  81 printf %s $VarSomething         # No linebreak specified, none output.
  82 echo                            # Default, only linebreak output.
  83 
  84 
  85 
  86 
  87 # The Bash parser word breaks on whitespace.
  88 # Whitespace, or the lack of it is significant.
  89 # (This holds true in general; there are, of course, exceptions.)
  90 
  91 
  92 
  93 
  94 # Translate the DOLLAR_SIGN character as: Content-Of.
  95 
  96 # Extended-Syntax way of writing Content-Of:
  97 echo ${VarSomething}
  98 
  99 #  The ${ ... } Extended-Syntax allows more than just the variable
 100 #+ name to be specified.
 101 #  In general, $VarSomething can always be written as: ${VarSomething}.
 102 
 103 # Call this script with arguments to see the following in action.
 104 
 105 
 106 
 107 #  Outside of double-quotes, the special characters @ and *
 108 #+ specify identical behavior.
 109 #  May be pronounced as: All-Elements-Of.
 110 
 111 #  Without specification of a name, they refer to the
 112 #+ pre-defined parameter Bash-Array.
 113 
 114 
 115 
 116 # Glob-Pattern references
 117 echo $*                         # All parameters to script or function
 118 echo ${*}                       # Same
 119 
 120 # Bash disables filename expansion for Glob-Patterns.
 121 # Only character matching is active.
 122 
 123 
 124 # All-Elements-Of references
 125 echo $@                         # Same as above
 126 echo ${@}                       # Same as above
 127 
 128 
 129 
 130 
 131 #  Within double-quotes, the behavior of Glob-Pattern references
 132 #+ depends on the setting of IFS (Input Field Separator).
 133 #  Within double-quotes, All-Elements-Of references behave the same.
 134 
 135 
 136 #  Specifying only the name of a variable holding a string refers
 137 #+ to all elements (characters) of a string.
 138 
 139 
 140 #  To specify an element (character) of a string,
 141 #+ the Extended-Syntax reference notation (see below) MAY be used.
 142 
 143 
 144 
 145 
 146 #  Specifying only the name of a Bash array references
 147 #+ the subscript zero element,
 148 #+ NOT the FIRST DEFINED nor the FIRST WITH CONTENTS element.
 149 
 150 #  Additional qualification is needed to reference other elements,
 151 #+ which means that the reference MUST be written in Extended-Syntax.
 152 #  The general form is: ${name[subscript]}.
 153 
 154 #  The string forms may also be used: ${name:subscript}
 155 #+ for Bash-Arrays when referencing the subscript zero element.
 156 
 157 
 158 # Bash-Arrays are implemented internally as linked lists,
 159 #+ not as a fixed area of storage as in some programming languages.
 160 
 161 
 162 #   Characteristics of Bash arrays (Bash-Arrays):
 163 #   --------------------------------------------
 164 
 165 #   If not otherwise specified, Bash-Array subscripts begin with
 166 #+  subscript number zero. Literally: [0]
 167 #   This is called zero-based indexing.
 168 ###
 169 #   If not otherwise specified, Bash-Arrays are subscript packed
 170 #+  (sequential subscripts without subscript gaps).
 171 ###
 172 #   Negative subscripts are not allowed.
 173 ###
 174 #   Elements of a Bash-Array need not all be of the same type.
 175 ###
 176 #   Elements of a Bash-Array may be undefined (null reference).
 177 #       That is, a Bash-Array my be "subscript sparse."
 178 ###
 179 #   Elements of a Bash-Array may be defined and empty (null contents).
 180 ###
 181 #   Elements of a Bash-Array may contain:
 182 #     * A whole number as a signed 32-bit (or larger) integer
 183 #     * A string
 184 #     * A string formated so that it appears to be a function name
 185 #     + with optional arguments
 186 ###
 187 #   Defined elements of a Bash-Array may be undefined (unset).
 188 #       That is, a subscript packed Bash-Array may be changed
 189 #   +   into a subscript sparse Bash-Array.
 190 ###
 191 #   Elements may be added to a Bash-Array by defining an element
 192 #+  not previously defined.
 193 ###
 194 # For these reasons, I have been calling them "Bash-Arrays".
 195 # I'll return to the generic term "array" from now on.
 196 #     -- msz
 197 
 198 
 199 
 200 
 201 #  Demo time -- initialize the previously declared ArrayVar as a
 202 #+ sparse array.
 203 #  (The 'unset ... ' is just documentation here.)
 204 
 205 unset ArrayVar[0]                   # Just for the record
 206 ArrayVar[1]=one                     # Unquoted literal
 207 ArrayVar[2]=''                      # Defined, and empty
 208 unset ArrayVar[3]                   # Just for the record
 209 ArrayVar[4]='four'                  # Quoted literal
 210 
 211 
 212 
 213 # Translate the %q format as: Quoted-Respecting-IFS-Rules.
 214 echo
 215 echo '- - Outside of double-quotes - -'
 216 ###
 217 printf %q ${ArrayVar[*]}            # Glob-Pattern All-Elements-Of
 218 echo
 219 echo 'echo command:'${ArrayVar[*]}
 220 ###
 221 printf %q ${ArrayVar[@]}            # All-Elements-Of
 222 echo
 223 echo 'echo command:'${ArrayVar[@]}
 224 
 225 # The use of double-quotes may be translated as: Enable-Substitution.
 226 
 227 # There are five cases recognized for the IFS setting.
 228 
 229 echo
 230 echo '- - Within double-quotes - Default IFS of space-tab-newline - -'
 231 IFS=$'\x20'$'\x09'$'\x0A'           #  These three bytes,
 232                                     #+ in exactly this order.
 233 
 234 
 235 printf %q "${ArrayVar[*]}"          # Glob-Pattern All-Elements-Of
 236 echo
 237 echo 'echo command:'"${ArrayVar[*]}"
 238 ###
 239 printf %q "${ArrayVar[@]}"          # All-Elements-Of
 240 echo
 241 echo 'echo command:'"${ArrayVar[@]}"
 242 
 243 
 244 echo
 245 echo '- - Within double-quotes - First character of IFS is ^ - -'
 246 # Any printing, non-whitespace character should do the same.
 247 IFS='^'$IFS                         # ^ + space tab newline
 248 ###
 249 printf %q "${ArrayVar[*]}"          # Glob-Pattern All-Elements-Of
 250 echo
 251 echo 'echo command:'"${ArrayVar[*]}"
 252 ###
 253 printf %q "${ArrayVar[@]}"          # All-Elements-Of
 254 echo
 255 echo 'echo command:'"${ArrayVar[@]}"
 256 
 257 
 258 echo
 259 echo '- - Within double-quotes - Without whitespace in IFS - -'
 260 IFS='^:%!'
 261 ###
 262 printf %q "${ArrayVar[*]}"          # Glob-Pattern All-Elements-Of
 263 echo
 264 echo 'echo command:'"${ArrayVar[*]}"
 265 ###
 266 printf %q "${ArrayVar[@]}"          # All-Elements-Of
 267 echo
 268 echo 'echo command:'"${ArrayVar[@]}"
 269 
 270 
 271 echo
 272 echo '- - Within double-quotes - IFS set and empty - -'
 273 IFS=''
 274 ###
 275 printf %q "${ArrayVar[*]}"          # Glob-Pattern All-Elements-Of
 276 echo
 277 echo 'echo command:'"${ArrayVar[*]}"
 278 ###
 279 printf %q "${ArrayVar[@]}"          # All-Elements-Of
 280 echo
 281 echo 'echo command:'"${ArrayVar[@]}"
 282 
 283 
 284 echo
 285 echo '- - Within double-quotes - IFS undefined - -'
 286 unset IFS
 287 ###
 288 printf %q "${ArrayVar[*]}"          # Glob-Pattern All-Elements-Of
 289 echo
 290 echo 'echo command:'"${ArrayVar[*]}"
 291 ###
 292 printf %q "${ArrayVar[@]}"          # All-Elements-Of
 293 echo
 294 echo 'echo command:'"${ArrayVar[@]}"
 295 
 296 
 297 # Put IFS back to the default.
 298 # Default is exactly these three bytes.
 299 IFS=$'\x20'$'\x09'$'\x0A'           # In exactly this order.
 300 
 301 # Interpretation of the above outputs:
 302 #   A Glob-Pattern is I/O; the setting of IFS matters.
 303 ###
 304 #   An All-Elements-Of does not consider IFS settings.
 305 ###
 306 #   Note the different output using the echo command and the
 307 #+  quoted format operator of the printf command.
 308 
 309 
 310 #  Recall:
 311 #   Parameters are similar to arrays and have the similar behaviors.
 312 ###
 313 #  The above examples demonstrate the possible variations.
 314 #  To retain the shape of a sparse array, additional script
 315 #+ programming is required.
 316 ###
 317 #  The source code of Bash has a routine to output the
 318 #+ [subscript]=value   array assignment format.
 319 #  As of version 2.05b, that routine is not used,
 320 #+ but that might change in future releases.
 321 
 322 
 323 
 324 # The length of a string, measured in non-null elements (characters):
 325 echo
 326 echo '- - Non-quoted references - -'
 327 echo 'Non-Null character count: '${#VarSomething}' characters.'
 328 
 329 # test='Lit'$'\x00''eral'           # $'\x00' is a null character.
 330 # echo ${#test}                     # See that?
 331 
 332 
 333 
 334 #  The length of an array, measured in defined elements,
 335 #+ including null content elements.
 336 echo
 337 echo 'Defined content count: '${#ArrayVar[@]}' elements.'
 338 # That is NOT the maximum subscript (4).
 339 # That is NOT the range of the subscripts (1 . . 4 inclusive).
 340 # It IS the length of the linked list.
 341 ###
 342 #  Both the maximum subscript and the range of the subscripts may
 343 #+ be found with additional script programming.
 344 
 345 # The length of a string, measured in non-null elements (characters):
 346 echo
 347 echo '- - Quoted, Glob-Pattern references - -'
 348 echo 'Non-Null character count: '"${#VarSomething}"' characters.'
 349 
 350 #  The length of an array, measured in defined elements,
 351 #+ including null-content elements.
 352 echo
 353 echo 'Defined element count: '"${#ArrayVar[*]}"' elements.'
 354 
 355 #  Interpretation: Substitution does not effect the ${# ... } operation.
 356 #  Suggestion:
 357 #  Always use the All-Elements-Of character
 358 #+ if that is what is intended (independence from IFS).
 359 
 360 
 361 
 362 #  Define a simple function.
 363 #  I include an underscore in the name
 364 #+ to make it distinctive in the examples below.
 365 ###
 366 #  Bash separates variable names and function names
 367 #+ in different namespaces.
 368 #  The Mark-One eyeball isn't that advanced.
 369 ###
 370 _simple() {
 371     echo -n 'SimpleFunc'$@          #  Newlines are swallowed in
 372 }                                   #+ result returned in any case.
 373 
 374 
 375 # The ( ... ) notation invokes a command or function.
 376 # The $( ... ) notation is pronounced: Result-Of.
 377 
 378 
 379 # Invoke the function _simple
 380 echo
 381 echo '- - Output of function _simple - -'
 382 _simple                             # Try passing arguments.
 383 echo
 384 # or
 385 (_simple)                           # Try passing arguments.
 386 echo
 387 
 388 echo '- Is there a variable of that name? -'
 389 echo $_simple not defined           # No variable by that name.
 390 
 391 # Invoke the result of function _simple (Error msg intended)
 392 
 393 ###
 394 $(_simple)                          # Gives an error message:
 395 #                          line 394: SimpleFunc: command not found
 396 #                          ---------------------------------------
 397 
 398 echo
 399 ###
 400 
 401 #  The first word of the result of function _simple
 402 #+ is neither a valid Bash command nor the name of a defined function.
 403 ###
 404 # This demonstrates that the output of _simple is subject to evaluation.
 405 ###
 406 # Interpretation:
 407 #   A function can be used to generate in-line Bash commands.
 408 
 409 
 410 # A simple function where the first word of result IS a bash command:
 411 ###
 412 _print() {
 413     echo -n 'printf %q '$@
 414 }
 415 
 416 echo '- - Outputs of function _print - -'
 417 _print parm1 parm2                  # An Output NOT A Command.
 418 echo
 419 
 420 $(_print parm1 parm2)               #  Executes: printf %q parm1 parm2
 421                                     #  See above IFS examples for the
 422                                     #+ various possibilities.
 423 echo
 424 
 425 $(_print $VarSomething)             # The predictable result.
 426 echo
 427 
 428 
 429 
 430 # Function variables
 431 # ------------------
 432 
 433 echo
 434 echo '- - Function variables - -'
 435 # A variable may represent a signed integer, a string or an array.
 436 # A string may be used like a function name with optional arguments.
 437 
 438 # set -vx                           #  Enable if desired
 439 declare -f funcVar                  #+ in namespace of functions
 440 
 441 funcVar=_print                      # Contains name of function.
 442 $funcVar parm1                      # Same as _print at this point.
 443 echo
 444 
 445 funcVar=$(_print )                  # Contains result of function.
 446 $funcVar                            # No input, No output.
 447 $funcVar $VarSomething              # The predictable result.
 448 echo
 449 
 450 funcVar=$(_print $VarSomething)     #  $VarSomething replaced HERE.
 451 $funcVar                            #  The expansion is part of the
 452 echo                                #+ variable contents.
 453 
 454 funcVar="$(_print $VarSomething)"   #  $VarSomething replaced HERE.
 455 $funcVar                            #  The expansion is part of the
 456 echo                                #+ variable contents.
 457 
 458 #  The difference between the unquoted and the double-quoted versions
 459 #+ above can be seen in the "protect_literal.sh" example.
 460 #  The first case above is processed as two, unquoted, Bash-Words.
 461 #  The second case above is processed as one, quoted, Bash-Word.
 462 
 463 
 464 
 465 
 466 # Delayed replacement
 467 # -------------------
 468 
 469 echo
 470 echo '- - Delayed replacement - -'
 471 funcVar="$(_print '$VarSomething')" # No replacement, single Bash-Word.
 472 eval $funcVar                       # $VarSomething replaced HERE.
 473 echo
 474 
 475 VarSomething='NewThing'
 476 eval $funcVar                       # $VarSomething replaced HERE.
 477 echo
 478 
 479 # Restore the original setting trashed above.
 480 VarSomething=Literal
 481 
 482 #  There are a pair of functions demonstrated in the
 483 #+ "protect_literal.sh" and "unprotect_literal.sh" examples.
 484 #  These are general purpose functions for delayed replacement literals
 485 #+ containing variables.
 486 
 487 
 488 
 489 
 490 
 491 # REVIEW:
 492 # ------
 493 
 494 #  A string can be considered a Classic-Array of elements (characters).
 495 #  A string operation applies to all elements (characters) of the string
 496 #+ (in concept, anyway).
 497 ###
 498 #  The notation: ${array_name[@]} represents all elements of the
 499 #+ Bash-Array: array_name.
 500 ###
 501 #  The Extended-Syntax string operations can be applied to all
 502 #+ elements of an array.
 503 ###
 504 #  This may be thought of as a For-Each operation on a vector of strings.
 505 ###
 506 #  Parameters are similar to an array.
 507 #  The initialization of a parameter array for a script
 508 #+ and a parameter array for a function only differ
 509 #+ in the initialization of ${0}, which never changes its setting.
 510 ###
 511 #  Subscript zero of the script's parameter array contains
 512 #+ the name of the script.
 513 ###
 514 #  Subscript zero of a function's parameter array DOES NOT contain
 515 #+ the name of the function.
 516 #  The name of the current function is accessed by the $FUNCNAME variable.
 517 ###
 518 #  A quick, review list follows (quick, not short).
 519 
 520 echo
 521 echo '- - Test (but not change) - -'
 522 echo '- null reference -'
 523 echo -n ${VarNull-'NotSet'}' '          # NotSet
 524 echo ${VarNull}                         # NewLine only
 525 echo -n ${VarNull:-'NotSet'}' '         # NotSet
 526 echo ${VarNull}                         # Newline only
 527 
 528 echo '- null contents -'
 529 echo -n ${VarEmpty-'Empty'}' '          # Only the space
 530 echo ${VarEmpty}                        # Newline only
 531 echo -n ${VarEmpty:-'Empty'}' '         # Empty
 532 echo ${VarEmpty}                        # Newline only
 533 
 534 echo '- contents -'
 535 echo ${VarSomething-'Content'}          # Literal
 536 echo ${VarSomething:-'Content'}         # Literal
 537 
 538 echo '- Sparse Array -'
 539 echo ${ArrayVar[@]-'not set'}
 540 
 541 # ASCII-Art time
 542 # State     Y==yes, N==no
 543 #           -       :-
 544 # Unset     Y       Y       ${# ... } == 0
 545 # Empty     N       Y       ${# ... } == 0
 546 # Contents  N       N       ${# ... } > 0
 547 
 548 #  Either the first and/or the second part of the tests
 549 #+ may be a command or a function invocation string.
 550 echo
 551 echo '- - Test 1 for undefined - -'
 552 declare -i t
 553 _decT() {
 554     t=$t-1
 555 }
 556 
 557 # Null reference, set: t == -1
 558 t=${#VarNull}                           # Results in zero.
 559 ${VarNull- _decT }                      # Function executes, t now -1.
 560 echo $t
 561 
 562 # Null contents, set: t == 0
 563 t=${#VarEmpty}                          # Results in zero.
 564 ${VarEmpty- _decT }                     # _decT function NOT executed.
 565 echo $t
 566 
 567 # Contents, set: t == number of non-null characters
 568 VarSomething='_simple'                  # Set to valid function name.
 569 t=${#VarSomething}                      # non-zero length
 570 ${VarSomething- _decT }                 # Function _simple executed.
 571 echo $t                                 # Note the Append-To action.
 572 
 573 # Exercise: clean up that example.
 574 unset t
 575 unset _decT
 576 VarSomething=Literal
 577 
 578 echo
 579 echo '- - Test and Change - -'
 580 echo '- Assignment if null reference -'
 581 echo -n ${VarNull='NotSet'}' '          # NotSet NotSet
 582 echo ${VarNull}
 583 unset VarNull
 584 
 585 echo '- Assignment if null reference -'
 586 echo -n ${VarNull:='NotSet'}' '         # NotSet NotSet
 587 echo ${VarNull}
 588 unset VarNull
 589 
 590 echo '- No assignment if null contents -'
 591 echo -n ${VarEmpty='Empty'}' '          # Space only
 592 echo ${VarEmpty}
 593 VarEmpty=''
 594 
 595 echo '- Assignment if null contents -'
 596 echo -n ${VarEmpty:='Empty'}' '         # Empty Empty
 597 echo ${VarEmpty}
 598 VarEmpty=''
 599 
 600 echo '- No change if already has contents -'
 601 echo ${VarSomething='Content'}          # Literal
 602 echo ${VarSomething:='Content'}         # Literal
 603 
 604 
 605 # "Subscript sparse" Bash-Arrays
 606 ###
 607 #  Bash-Arrays are subscript packed, beginning with
 608 #+ subscript zero unless otherwise specified.
 609 ###
 610 #  The initialization of ArrayVar was one way
 611 #+ to "otherwise specify".  Here is the other way:
 612 ###
 613 echo
 614 declare -a ArraySparse
 615 ArraySparse=( [1]=one [2]='' [4]='four' )
 616 # [0]=null reference, [2]=null content, [3]=null reference
 617 
 618 echo '- - Array-Sparse List - -'
 619 # Within double-quotes, default IFS, Glob-Pattern
 620 
 621 IFS=$'\x20'$'\x09'$'\x0A'
 622 printf %q "${ArraySparse[*]}"
 623 echo
 624 
 625 #  Note that the output does not distinguish between "null content"
 626 #+ and "null reference".
 627 #  Both print as escaped whitespace.
 628 ###
 629 #  Note also that the output does NOT contain escaped whitespace
 630 #+ for the "null reference(s)" prior to the first defined element.
 631 ###
 632 # This behavior of 2.04, 2.05a and 2.05b has been reported
 633 #+ and may change in a future version of Bash.
 634 
 635 #  To output a sparse array and maintain the [subscript]=value
 636 #+ relationship without change requires a bit of programming.
 637 #  One possible code fragment:
 638 ###
 639 # local l=${#ArraySparse[@]}        # Count of defined elements
 640 # local f=0                         # Count of found subscripts
 641 # local i=0                         # Subscript to test
 642 (                                   # Anonymous in-line function
 643     for (( l=${#ArraySparse[@]}, f = 0, i = 0 ; f < l ; i++ ))
 644     do
 645         # 'if defined then...'
 646         ${ArraySparse[$i]+ eval echo '\ ['$i']='${ArraySparse[$i]} ; (( f++ )) }
 647     done
 648 )
 649 
 650 # The reader coming upon the above code fragment cold
 651 #+ might want to review "command lists" and "multiple commands on a line"
 652 #+ in the text of the foregoing "Advanced Bash Scripting Guide."
 653 ###
 654 #  Note:
 655 #  The "read -a array_name" version of the "read" command
 656 #+ begins filling array_name at subscript zero.
 657 #  ArraySparse does not define a value at subscript zero.
 658 ###
 659 #  The user needing to read/write a sparse array to either
 660 #+ external storage or a communications socket must invent
 661 #+ a read/write code pair suitable for their purpose.
 662 ###
 663 # Exercise: clean it up.
 664 
 665 unset ArraySparse
 666 
 667 echo
 668 echo '- - Conditional alternate (But not change)- -'
 669 echo '- No alternate if null reference -'
 670 echo -n ${VarNull+'NotSet'}' '
 671 echo ${VarNull}
 672 unset VarNull
 673 
 674 echo '- No alternate if null reference -'
 675 echo -n ${VarNull:+'NotSet'}' '
 676 echo ${VarNull}
 677 unset VarNull
 678 
 679 echo '- Alternate if null contents -'
 680 echo -n ${VarEmpty+'Empty'}' '              # Empty
 681 echo ${VarEmpty}
 682 VarEmpty=''
 683 
 684 echo '- No alternate if null contents -'
 685 echo -n ${VarEmpty:+'Empty'}' '             # Space only
 686 echo ${VarEmpty}
 687 VarEmpty=''
 688 
 689 echo '- Alternate if already has contents -'
 690 
 691 # Alternate literal
 692 echo -n ${VarSomething+'Content'}' '        # Content Literal
 693 echo ${VarSomething}
 694 
 695 # Invoke function
 696 echo -n ${VarSomething:+ $(_simple) }' '    # SimpleFunc Literal
 697 echo ${VarSomething}
 698 echo
 699 
 700 echo '- - Sparse Array - -'
 701 echo ${ArrayVar[@]+'Empty'}                 # An array of 'Empty'(ies)
 702 echo
 703 
 704 echo '- - Test 2 for undefined - -'
 705 
 706 declare -i t
 707 _incT() {
 708     t=$t+1
 709 }
 710 
 711 #  Note:
 712 #  This is the same test used in the sparse array
 713 #+ listing code fragment.
 714 
 715 # Null reference, set: t == -1
 716 t=${#VarNull}-1                     # Results in minus-one.
 717 ${VarNull+ _incT }                  # Does not execute.
 718 echo $t' Null reference'
 719 
 720 # Null contents, set: t == 0
 721 t=${#VarEmpty}-1                    # Results in minus-one.
 722 ${VarEmpty+ _incT }                 # Executes.
 723 echo $t'  Null content'
 724 
 725 # Contents, set: t == (number of non-null characters)
 726 t=${#VarSomething}-1                # non-null length minus-one
 727 ${VarSomething+ _incT }             # Executes.
 728 echo $t'  Contents'
 729 
 730 # Exercise: clean up that example.
 731 unset t
 732 unset _incT
 733 
 734 # ${name?err_msg} ${name:?err_msg}
 735 #  These follow the same rules but always exit afterwards
 736 #+ if an action is specified following the question mark.
 737 #  The action following the question mark may be a literal
 738 #+ or a function result.
 739 ###
 740 #  ${name?} ${name:?} are test-only, the return can be tested.
 741 
 742 
 743 
 744 
 745 # Element operations
 746 # ------------------
 747 
 748 echo
 749 echo '- - Trailing sub-element selection - -'
 750 
 751 #  Strings, Arrays and Positional parameters
 752 
 753 #  Call this script with multiple arguments
 754 #+ to see the parameter selections.
 755 
 756 echo '- All -'
 757 echo ${VarSomething:0}              # all non-null characters
 758 echo ${ArrayVar[@]:0}               # all elements with content
 759 echo ${@:0}                         # all parameters with content;
 760                                     # ignoring parameter[0]
 761 
 762 echo
 763 echo '- All after -'
 764 echo ${VarSomething:1}              # all non-null after character[0]
 765 echo ${ArrayVar[@]:1}               # all after element[0] with content
 766 echo ${@:2}                         # all after param[1] with content
 767 
 768 echo
 769 echo '- Range after -'
 770 echo ${VarSomething:4:3}            # ral
 771                                     # Three characters after
 772                                     # character[3]
 773 
 774 echo '- Sparse array gotch -'
 775 echo ${ArrayVar[@]:1:2}     #  four - The only element with content.
 776                             #  Two elements after (if that many exist).
 777                             #  the FIRST WITH CONTENTS
 778                             #+ (the FIRST WITH  CONTENTS is being
 779                             #+ considered as if it
 780                             #+ were subscript zero).
 781 #  Executed as if Bash considers ONLY array elements with CONTENT
 782 #  printf %q "${ArrayVar[@]:0:3}"    # Try this one
 783 
 784 #  In versions 2.04, 2.05a and 2.05b,
 785 #+ Bash does not handle sparse arrays as expected using this notation.
 786 #
 787 #  The current Bash maintainer, Chet Ramey, has corrected this
 788 #+ for an upcoming version of Bash.
 789 
 790 
 791 echo '- Non-sparse array -'
 792 echo ${@:2:2}               # Two parameters following parameter[1]
 793 
 794 # New victims for string vector examples:
 795 stringZ=abcABC123ABCabc
 796 arrayZ=( abcabc ABCABC 123123 ABCABC abcabc )
 797 sparseZ=( [1]='abcabc' [3]='ABCABC' [4]='' [5]='123123' )
 798 
 799 echo
 800 echo ' - - Victim string - -'$stringZ'- - '
 801 echo ' - - Victim array - -'${arrayZ[@]}'- - '
 802 echo ' - - Sparse array - -'${sparseZ[@]}'- - '
 803 echo ' - [0]==null ref, [2]==null ref, [4]==null content - '
 804 echo ' - [1]=abcabc [3]=ABCABC [5]=123123 - '
 805 echo ' - non-null-reference count: '${#sparseZ[@]}' elements'
 806 
 807 echo
 808 echo '- - Prefix sub-element removal - -'
 809 echo '- - Glob-Pattern match must include the first character. - -'
 810 echo '- - Glob-Pattern may be a literal or a function result. - -'
 811 echo
 812 
 813 
 814 # Function returning a simple, Literal, Glob-Pattern
 815 _abc() {
 816     echo -n 'abc'
 817 }
 818 
 819 echo '- Shortest prefix -'
 820 echo ${stringZ#123}                 # Unchanged (not a prefix).
 821 echo ${stringZ#$(_abc)}             # ABC123ABCabc
 822 echo ${arrayZ[@]#abc}               # Applied to each element.
 823 
 824 # Fixed by Chet Ramey for an upcoming version of Bash.
 825 # echo ${sparseZ[@]#abc}            # Version-2.05b core dumps.
 826 
 827 # The -it would be nice- First-Subscript-Of
 828 # echo ${#sparseZ[@]#*}             # This is NOT valid Bash.
 829 
 830 echo
 831 echo '- Longest prefix -'
 832 echo ${stringZ##1*3}                # Unchanged (not a prefix)
 833 echo ${stringZ##a*C}                # abc
 834 echo ${arrayZ[@]##a*c}              # ABCABC 123123 ABCABC
 835 
 836 # Fixed by Chet Ramey for an upcoming version of Bash
 837 # echo ${sparseZ[@]##a*c}           # Version-2.05b core dumps.
 838 
 839 echo
 840 echo '- - Suffix sub-element removal - -'
 841 echo '- - Glob-Pattern match must include the last character. - -'
 842 echo '- - Glob-Pattern may be a literal or a function result. - -'
 843 echo
 844 echo '- Shortest suffix -'
 845 echo ${stringZ%1*3}                 # Unchanged (not a suffix).
 846 echo ${stringZ%$(_abc)}             # abcABC123ABC
 847 echo ${arrayZ[@]%abc}               # Applied to each element.
 848 
 849 # Fixed by Chet Ramey for an upcoming version of Bash.
 850 # echo ${sparseZ[@]%abc}            # Version-2.05b core dumps.
 851 
 852 # The -it would be nice- Last-Subscript-Of
 853 # echo ${#sparseZ[@]%*}             # This is NOT valid Bash.
 854 
 855 echo
 856 echo '- Longest suffix -'
 857 echo ${stringZ%%1*3}                # Unchanged (not a suffix)
 858 echo ${stringZ%%b*c}                # a
 859 echo ${arrayZ[@]%%b*c}              # a ABCABC 123123 ABCABC a
 860 
 861 # Fixed by Chet Ramey for an upcoming version of Bash.
 862 # echo ${sparseZ[@]%%b*c}           # Version-2.05b core dumps.
 863 
 864 echo
 865 echo '- - Sub-element replacement - -'
 866 echo '- - Sub-element at any location in string. - -'
 867 echo '- - First specification is a Glob-Pattern - -'
 868 echo '- - Glob-Pattern may be a literal or Glob-Pattern function result. - -'
 869 echo '- - Second specification may be a literal or function result. - -'
 870 echo '- - Second specification may be unspecified. Pronounce that'
 871 echo '    as: Replace-With-Nothing (Delete) - -'
 872 echo
 873 
 874 
 875 
 876 # Function returning a simple, Literal, Glob-Pattern
 877 _123() {
 878     echo -n '123'
 879 }
 880 
 881 echo '- Replace first occurrence -'
 882 echo ${stringZ/$(_123)/999}         # Changed (123 is a component).
 883 echo ${stringZ/ABC/xyz}             # xyzABC123ABCabc
 884 echo ${arrayZ[@]/ABC/xyz}           # Applied to each element.
 885 echo ${sparseZ[@]/ABC/xyz}          # Works as expected.
 886 
 887 echo
 888 echo '- Delete first occurrence -'
 889 echo ${stringZ/$(_123)/}
 890 echo ${stringZ/ABC/}
 891 echo ${arrayZ[@]/ABC/}
 892 echo ${sparseZ[@]/ABC/}
 893 
 894 #  The replacement need not be a literal,
 895 #+ since the result of a function invocation is allowed.
 896 #  This is general to all forms of replacement.
 897 echo
 898 echo '- Replace first occurrence with Result-Of -'
 899 echo ${stringZ/$(_123)/$(_simple)}  # Works as expected.
 900 echo ${arrayZ[@]/ca/$(_simple)}     # Applied to each element.
 901 echo ${sparseZ[@]/ca/$(_simple)}    # Works as expected.
 902 
 903 echo
 904 echo '- Replace all occurrences -'
 905 echo ${stringZ//[b2]/X}             # X-out b's and 2's
 906 echo ${stringZ//abc/xyz}            # xyzABC123ABCxyz
 907 echo ${arrayZ[@]//abc/xyz}          # Applied to each element.
 908 echo ${sparseZ[@]//abc/xyz}         # Works as expected.
 909 
 910 echo
 911 echo '- Delete all occurrences -'
 912 echo ${stringZ//[b2]/}
 913 echo ${stringZ//abc/}
 914 echo ${arrayZ[@]//abc/}
 915 echo ${sparseZ[@]//abc/}
 916 
 917 echo
 918 echo '- - Prefix sub-element replacement - -'
 919 echo '- - Match must include the first character. - -'
 920 echo
 921 
 922 echo '- Replace prefix occurrences -'
 923 echo ${stringZ/#[b2]/X}             # Unchanged (neither is a prefix).
 924 echo ${stringZ/#$(_abc)/XYZ}        # XYZABC123ABCabc
 925 echo ${arrayZ[@]/#abc/XYZ}          # Applied to each element.
 926 echo ${sparseZ[@]/#abc/XYZ}         # Works as expected.
 927 
 928 echo
 929 echo '- Delete prefix occurrences -'
 930 echo ${stringZ/#[b2]/}
 931 echo ${stringZ/#$(_abc)/}
 932 echo ${arrayZ[@]/#abc/}
 933 echo ${sparseZ[@]/#abc/}
 934 
 935 echo
 936 echo '- - Suffix sub-element replacement - -'
 937 echo '- - Match must include the last character. - -'
 938 echo
 939 
 940 echo '- Replace suffix occurrences -'
 941 echo ${stringZ/%[b2]/X}             # Unchanged (neither is a suffix).
 942 echo ${stringZ/%$(_abc)/XYZ}        # abcABC123ABCXYZ
 943 echo ${arrayZ[@]/%abc/XYZ}          # Applied to each element.
 944 echo ${sparseZ[@]/%abc/XYZ}         # Works as expected.
 945 
 946 echo
 947 echo '- Delete suffix occurrences -'
 948 echo ${stringZ/%[b2]/}
 949 echo ${stringZ/%$(_abc)/}
 950 echo ${arrayZ[@]/%abc/}
 951 echo ${sparseZ[@]/%abc/}
 952 
 953 echo
 954 echo '- - Special cases of null Glob-Pattern - -'
 955 echo
 956 
 957 echo '- Prefix all -'
 958 # null substring pattern means 'prefix'
 959 echo ${stringZ/#/NEW}               # NEWabcABC123ABCabc
 960 echo ${arrayZ[@]/#/NEW}             # Applied to each element.
 961 echo ${sparseZ[@]/#/NEW}            # Applied to null-content also.
 962                                     # That seems reasonable.
 963 
 964 echo
 965 echo '- Suffix all -'
 966 # null substring pattern means 'suffix'
 967 echo ${stringZ/%/NEW}               # abcABC123ABCabcNEW
 968 echo ${arrayZ[@]/%/NEW}             # Applied to each element.
 969 echo ${sparseZ[@]/%/NEW}            # Applied to null-content also.
 970                                     # That seems reasonable.
 971 
 972 echo
 973 echo '- - Special case For-Each Glob-Pattern - -'
 974 echo '- - - - This is a nice-to-have dream - - - -'
 975 echo
 976 
 977 _GenFunc() {
 978     echo -n ${0}                    # Illustration only.
 979     # Actually, that would be an arbitrary computation.
 980 }
 981 
 982 # All occurrences, matching the AnyThing pattern.
 983 # Currently //*/ does not match null-content nor null-reference.
 984 # /#/ and /%/ does match null-content but not null-reference.
 985 echo ${sparseZ[@]//*/$(_GenFunc)}
 986 
 987 
 988 #  A possible syntax would be to make
 989 #+ the parameter notation used within this construct mean:
 990 #   ${1} - The full element
 991 #   ${2} - The prefix, if any, to the matched sub-element
 992 #   ${3} - The matched sub-element
 993 #   ${4} - The suffix, if any, to the matched sub-element
 994 #
 995 # echo ${sparseZ[@]//*/$(_GenFunc ${3})}   # Same as ${1} here.
 996 # Perhaps it will be implemented in a future version of Bash.
 997 
 998 
 999 exit 0