Appendix A. Contributed Scripts

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


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

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


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

This script is a modification of Example 12-19.

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


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

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

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


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

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


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

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


Example A-6. Collatz series

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


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

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


Example A-8. Make a "dictionary"

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


Example A-9. Soundex conversion

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


Example A-10. "Game of Life"

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


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

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

+++

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


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

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


Example A-13. ftpget: Downloading files via ftp

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

+

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


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

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

+

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


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

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

+

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


Example A-16. 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-17. 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-18. 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-19. 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

Stéphane Chazelas demonstrates object-oriented programming in a Bash script.


Example A-20. Object-oriented database

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

Mariusz Gniazdowski contributes a hash library for use in scripts.


Example A-21. Library of hash functions

   1 # Hash:
   2 # Hash function library
   3 # Author: Mariusz Gniazdowski <mgniazd-at-gmail.com>
   4 # Date: 2005-04-07
   5 
   6 # Functions making emulating hashes in Bash a little less painful.
   7 
   8 
   9 #    Limitations:
  10 #  * Only global variables are supported.
  11 #  * Each hash instance generates one global variable per value.
  12 #  * Variable names collisions are possible
  13 #+   if you define variable like __hash__hashname_key
  14 #  * Keys must use chars that can be part of a Bash variable name
  15 #+   (no dashes, periods, etc.).
  16 #  * The hash is created as a variable:
  17 #    ... hashname_keyname
  18 #    So if somone will create hashes like:
  19 #      myhash_ + mykey = myhash__mykey
  20 #      myhash + _mykey = myhash__mykey
  21 #    Then there will be a collision.
  22 #    (This should not pose a major problem.)
  23 
  24 
  25 Hash_config_varname_prefix=__hash__
  26 
  27 
  28 # Emulates:  hash[key]=value
  29 #
  30 # Params:
  31 # 1 - hash
  32 # 2 - key
  33 # 3 - value
  34 function hash_set {
  35 	eval "${Hash_config_varname_prefix}${1}_${2}=\"${3}\""
  36 }
  37 
  38 
  39 # Emulates:  value=hash[key]
  40 #
  41 # Params:
  42 # 1 - hash
  43 # 2 - key
  44 # 3 - value (name of global variable to set)
  45 function hash_get_into {
  46 	eval "$3=\"\$${Hash_config_varname_prefix}${1}_${2}\""
  47 }
  48 
  49 
  50 # Emulates:  echo hash[key]
  51 #
  52 # Params:
  53 # 1 - hash
  54 # 2 - key
  55 # 3 - echo params (like -n, for example)
  56 function hash_echo {
  57 	eval "echo $3 \"\$${Hash_config_varname_prefix}${1}_${2}\""
  58 }
  59 
  60 
  61 # Emulates:  hash1[key1]=hash2[key2]
  62 #
  63 # Params:
  64 # 1 - hash1
  65 # 2 - key1
  66 # 3 - hash2
  67 # 4 - key2
  68 function hash_copy {
  69 	eval "${Hash_config_varname_prefix}${1}_${2}=\"\$${Hash_config_varname_prefix}${3}_${4}\""
  70 }
  71 
  72 
  73 # Emulates:  hash[keyN-1]=hash[key2]=...hash[key1]
  74 #
  75 # Copies first key to rest of keys.
  76 #
  77 # Params:
  78 # 1 - hash1
  79 # 2 - key1
  80 # 3 - key2
  81 # . . .
  82 # N - keyN
  83 function hash_dup {
  84 	local hashName="$1" keyName="$2"
  85 	shift 2
  86 	until [ ${#} -le 0 ]; do
  87 		eval "${Hash_config_varname_prefix}${hashName}_${1}=\"\$${Hash_config_varname_prefix}${hashName}_${keyName}\""
  88 		shift;
  89 	done;
  90 }
  91 
  92 
  93 # Emulates:  unset hash[key]
  94 #
  95 # Params:
  96 # 1 - hash
  97 # 2 - key
  98 function hash_unset {
  99 	eval "unset ${Hash_config_varname_prefix}${1}_${2}"
 100 }
 101 
 102 
 103 # Emulates something similar to:  ref=&hash[key]
 104 #
 105 # The reference is name of the variable in which value is held.
 106 #
 107 # Params:
 108 # 1 - hash
 109 # 2 - key
 110 # 3 - ref - Name of global variable to set.
 111 function hash_get_ref_into {
 112 	eval "$3=\"${Hash_config_varname_prefix}${1}_${2}\""
 113 }
 114 
 115 
 116 # Emulates something similar to:  echo &hash[key]
 117 #
 118 # That reference is name of variable in which value is held.
 119 #
 120 # Params:
 121 # 1 - hash
 122 # 2 - key
 123 # 3 - echo params (like -n for example)
 124 function hash_echo_ref {
 125 	eval "echo $3 \"${Hash_config_varname_prefix}${1}_${2}\""
 126 }
 127 
 128 
 129 
 130 # Emulates something similar to:  $$hash[key](param1, param2, ...)
 131 #
 132 # Params:
 133 # 1 - hash
 134 # 2 - key
 135 # 3,4, ... - Function parameters
 136 function hash_call {
 137 	local hash key
 138 	hash=$1
 139 	key=$2
 140 	shift 2
 141 	eval "eval \"\$${Hash_config_varname_prefix}${hash}_${key} \\\"\\\$@\\\"\""
 142 }
 143 
 144 
 145 # Emulates something similar to:  isset(hash[key]) or hash[key]==NULL
 146 #
 147 # Params:
 148 # 1 - hash
 149 # 2 - key
 150 # Returns:
 151 # 0 - there is such key
 152 # 1 - there is no such key
 153 function hash_is_set {
 154 	eval "if [[ \"\${${Hash_config_varname_prefix}${1}_${2}-a}\" = \"a\" && 
 155 			\"\${${Hash_config_varname_prefix}${1}_${2}-b}\" = \"b\" ]]; then return 1; else return 0; fi"
 156 }
 157 
 158 
 159 # Emulates something similar to:
 160 #   foreach($hash as $key => $value) { fun($key,$value); }
 161 #
 162 # It is possible to write different variations of this function.
 163 # Here we use a function call to make it as "generic" as possible.
 164 #
 165 # Params:
 166 # 1 - hash
 167 # 2 - function name
 168 function hash_foreach {
 169 	local keyname oldIFS="$IFS"
 170 	IFS=' '
 171 	for i in $(eval "echo \${!${Hash_config_varname_prefix}${1}_*}"); do
 172 		keyname=$(eval "echo \${i##${Hash_config_varname_prefix}${1}_}")
 173 		eval "$2 $keyname \"\$$i\""
 174 	done
 175 	IFS="$oldIFS"
 176 }
 177 
 178 # NOTE: In lines 103 and 116, ampersand changed.
 179 #       But, it doesn't matter, because these are comment lines anyhow.

Here is an example script using the foregoing hash library.


Example A-22. Colorizing text using hash functions

   1 #!/bin/bash
   2 # hash-example.sh: Colorizing text.
   3 # Author: Mariusz Gniazdowski <mgniazd-at-gmail.com>
   4 
   5 . Hash.lib      # Load the library of functions.
   6 
   7 hash_set colors red          "\033[0;31m"
   8 hash_set colors blue         "\033[0;34m"
   9 hash_set colors light_blue   "\033[1;34m"
  10 hash_set colors light_red    "\033[1;31m"
  11 hash_set colors cyan         "\033[0;36m"
  12 hash_set colors light_green  "\033[1;32m"
  13 hash_set colors light_gray   "\033[0;37m"
  14 hash_set colors green        "\033[0;32m"
  15 hash_set colors yellow       "\033[1;33m"
  16 hash_set colors light_purple "\033[1;35m"
  17 hash_set colors purple       "\033[0;35m"
  18 hash_set colors reset_color  "\033[0;00m"
  19 
  20 
  21 # $1 - keyname
  22 # $2 - value
  23 try_colors() {
  24 	echo -en "$2"
  25 	echo "This line is $1."
  26 }
  27 hash_foreach colors try_colors
  28 hash_echo colors reset_color -en
  29 
  30 echo -e '\nLet us overwrite some colors with yellow.\n'
  31 # It's hard to read yellow text on some terminals.
  32 hash_dup colors yellow   red light_green blue green light_gray cyan
  33 hash_foreach colors try_colors
  34 hash_echo colors reset_color -en
  35 
  36 echo -e '\nLet us delete them and try colors once more . . .\n'
  37 
  38 for i in red light_green blue green light_gray cyan; do
  39 	hash_unset colors $i
  40 done
  41 hash_foreach colors try_colors
  42 hash_echo colors reset_color -en
  43 
  44 hash_set other txt "Other examples . . ."
  45 hash_echo other txt
  46 hash_get_into other txt text
  47 echo $text
  48 
  49 hash_set other my_fun try_colors
  50 hash_call other my_fun   purple "`hash_echo colors purple`"
  51 hash_echo colors reset_color -en
  52 
  53 echo; echo "Back to normal?"; echo
  54 
  55 exit $?
  56 
  57 #  On some terminals, the "light" colors print in bold,
  58 #  and end up looking darker than the normal ones.
  59 #  Why is this?
  60 

Now for a script that installs and mounts those cute USB keychain solid-state "hard drives."


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

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


Example A-24. 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-25. 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-26. 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 in 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-27. Spammer Identification

   1 #!/bin/bash
   2 
   3 # $Id: is_spammer.bash,v 1.12.2.11 2004/10/01 21:42:33 mszick Exp $
   4 # Above line is RCS info.
   5 
   6 # The latest version of this script is available from http://www.morethan.org.
   7 #
   8 # Spammer-identification
   9 # by Michael S. Zick
  10 # Used in the ABS Guide with permission.
  11 
  12 
  13 
  14 #######################################################
  15 # Documentation
  16 # See also "Quickstart" at end of script.
  17 #######################################################
  18 
  19 :<<-'__is_spammer_Doc_'
  20 
  21     Copyright (c) Michael S. Zick, 2004
  22     License: Unrestricted reuse in any form, for any purpose.
  23     Warranty: None -{Its a script; the user is on their own.}-
  24 
  25 Impatient?
  26     Application code: goto "# # # Hunt the Spammer' program code # # #"
  27     Example output: ":<<-'_is_spammer_outputs_'"
  28     How to use: Enter script name without arguments.
  29                 Or goto "Quickstart" at end of script.
  30 
  31 Provides
  32     Given a domain name or IP(v4) address as input:
  33 
  34     Does an exhaustive set of queries to find the associated
  35     network resources (short of recursing into TLDs).
  36 
  37     Checks the IP(v4) addresses found against Blacklist
  38     nameservers.
  39 
  40     If found to be a blacklisted IP(v4) address,
  41     reports the blacklist text records.
  42     (Usually hyper-links to the specific report.)
  43 
  44 Requires
  45     A working Internet connection.
  46     (Exercise: Add check and/or abort if not on-line when running script.)
  47     Bash with arrays (2.05b+).
  48 
  49     The external program 'dig' --
  50     a utility program provided with the 'bind' set of programs.
  51     Specifically, the version which is part of Bind series 9.x
  52     See: http://www.isc.org
  53 
  54     All usages of 'dig' are limited to wrapper functions,
  55     which may be rewritten as required.
  56     See: dig_wrappers.bash for details.
  57          ("Additional documentation" -- below)
  58 
  59 Usage
  60     Script requires a single argument, which may be:
  61     1) A domain name;
  62     2) An IP(v4) address;
  63     3) A filename, with one name or address per line.
  64 
  65     Script accepts an optional second argument, which may be:
  66     1) A Blacklist server name;
  67     2) A filename, with one Blacklist server name per line.
  68 
  69     If the second argument is not provided, the script uses
  70     a built-in set of (free) Blacklist servers.
  71 
  72     See also, the Quickstart at the end of this script (after 'exit').
  73 
  74 Return Codes
  75     0 - All OK
  76     1 - Script failure
  77     2 - Something is Blacklisted
  78 
  79 Optional environment variables
  80     SPAMMER_TRACE
  81         If set to a writable file,
  82         script will log an execution flow trace.
  83 
  84     SPAMMER_DATA
  85         If set to a writable file, script will dump its
  86         discovered data in the form of GraphViz file.
  87         See: http://www.research.att.com/sw/tools/graphviz
  88 
  89     SPAMMER_LIMIT
  90         Limits the depth of resource tracing.
  91 
  92         Default is 2 levels.
  93 
  94         A setting of 0 (zero) means 'unlimited' . . .
  95           Caution: script might recurse the whole Internet!
  96 
  97         A limit of 1 or 2 is most useful when processing
  98         a file of domain names and addresses.
  99         A higher limit can be useful when hunting spam gangs.
 100 
 101 
 102 Additional documentation
 103     Download the archived set of scripts
 104     explaining and illustrating the function contained within this script.
 105     http://personal.riverusers.com/mszick_clf.tar.bz2
 106 
 107 
 108 Study notes
 109     This script uses a large number of functions.
 110     Nearly all general functions have their own example script.
 111     Each of the example scripts have tutorial level comments.
 112 
 113 Scripting project
 114     Add support for IP(v6) addresses.
 115     IP(v6) addresses are recognized but not processed.
 116 
 117 Advanced project
 118     Add the reverse lookup detail to the discovered information.
 119 
 120     Report the delegation chain and abuse contacts.
 121 
 122     Modify the GraphViz file output to include the
 123     newly discovered information.
 124 
 125 __is_spammer_Doc_
 126 
 127 #######################################################
 128 
 129 
 130 
 131 
 132 #### Special IFS settings used for string parsing. ####
 133 
 134 # Whitespace == :Space:Tab:Line Feed:Carriage Return:
 135 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D'
 136 
 137 # No Whitespace == Line Feed:Carriage Return
 138 NO_WSP=$'\x0A'$'\x0D'
 139 
 140 # Field separator for dotted decimal IP addresses
 141 ADR_IFS=${NO_WSP}'.'
 142 
 143 # Array to dotted string conversions
 144 DOT_IFS='.'${WSP_IFS}
 145 
 146 # # # Pending operations stack machine # # #
 147 # This set of functions described in func_stack.bash.
 148 # (See "Additional documentation" above.)
 149 # # #
 150 
 151 # Global stack of pending operations.
 152 declare -f -a _pending_
 153 # Global sentinel for stack runners
 154 declare -i _p_ctrl_
 155 # Global holder for currently executing function
 156 declare -f _pend_current_
 157 
 158 # # # Debug version only - remove for regular use # # #
 159 #
 160 # The function stored in _pend_hook_ is called
 161 # immediately before each pending function is
 162 # evaluated.  Stack clean, _pend_current_ set.
 163 #
 164 # This thingy demonstrated in pend_hook.bash.
 165 declare -f _pend_hook_
 166 # # #
 167 
 168 # The do nothing function
 169 pend_dummy() { : ; }
 170 
 171 # Clear and initialize the function stack.
 172 pend_init() {
 173     unset _pending_[@]
 174     pend_func pend_stop_mark
 175     _pend_hook_='pend_dummy'  # Debug only.
 176 }
 177 
 178 # Discard the top function on the stack.
 179 pend_pop() {
 180     if [ ${#_pending_[@]} -gt 0 ]
 181     then
 182         local -i _top_
 183         _top_=${#_pending_[@]}-1
 184         unset _pending_[$_top_]
 185     fi
 186 }
 187 
 188 # pend_func function_name [$(printf '%q\n' arguments)]
 189 pend_func() {
 190     local IFS=${NO_WSP}
 191     set -f
 192     _pending_[${#_pending_[@]}]=$@
 193     set +f
 194 }
 195 
 196 # The function which stops the release:
 197 pend_stop_mark() {
 198     _p_ctrl_=0
 199 }
 200 
 201 pend_mark() {
 202     pend_func pend_stop_mark
 203 }
 204 
 205 # Execute functions until 'pend_stop_mark' . . .
 206 pend_release() {
 207     local -i _top_             # Declare _top_ as integer.
 208     _p_ctrl_=${#_pending_[@]}
 209     while [ ${_p_ctrl_} -gt 0 ]
 210     do
 211        _top_=${#_pending_[@]}-1
 212        _pend_current_=${_pending_[$_top_]}
 213        unset _pending_[$_top_]
 214        $_pend_hook_            # Debug only.
 215        eval $_pend_current_
 216     done
 217 }
 218 
 219 # Drop functions until 'pend_stop_mark' . . .
 220 pend_drop() {
 221     local -i _top_
 222     local _pd_ctrl_=${#_pending_[@]}
 223     while [ ${_pd_ctrl_} -gt 0 ]
 224     do
 225        _top_=$_pd_ctrl_-1
 226        if [ "${_pending_[$_top_]}" == 'pend_stop_mark' ]
 227        then
 228            unset _pending_[$_top_]
 229            break
 230        else
 231            unset _pending_[$_top_]
 232            _pd_ctrl_=$_top_
 233        fi
 234     done
 235     if [ ${#_pending_[@]} -eq 0 ]
 236     then
 237         pend_func pend_stop_mark
 238     fi
 239 }
 240 
 241 #### Array editors ####
 242 
 243 # This function described in edit_exact.bash.
 244 # (See "Additional documentation," above.)
 245 # edit_exact <excludes_array_name> <target_array_name>
 246&n