Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting
Prev |  | Next

# Appendix A. Contributed Scripts

These scripts, while not fitting into the text of this document, do illustrate some interesting shell programming techniques. Some 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=85
      10 E_NOFILE=86
      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 # It's a useful technique.
      32 sedscript='s/^>//
      33 s/^  *>//
      34 s/^  *//
      35 s/		*//'
      36 # =================================
      37
      38 #  Delete carets and tabs at beginning of lines,
      39 #+ then fold lines to $MAXWIDTH characters.
      40 sed "$sedscript" $1 | fold -s --width=$MAXWIDTH
      41                         #  -s option to "fold"
      42                         #+ breaks lines at whitespace, if possible.
      43
      44
      45 #  This script was inspired by an article in a well-known trade journal
      46 #+ extolling a 164K MS Windows utility with similar functionality.
      47 #
      48 #  An nice set of text processing utilities and an efficient
      49 #+ scripting language provide an alternative to the bloated executables
      50 #+ of a clunky operating system.
      51
      52 exit $?

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

This script is a modification of Example 16-22.

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

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                      # Yes, this filename needs work.
      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=85
      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/xxxxxxxx/                      Change to suit your system.
       7 BLOCKSIZE=2048
       8 # SPEED=10                                 # If unspecified, uses max spd.
       9 # DEVICE=/dev/cdrom                          older version.
      10 DEVICE="1,0,0"
      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   # Old version.
      30 wodim -v -isosize dev=$DEVICE $OF
      31 # Uses Joerg Schilling's "cdrecord" package (see its docs).
      32 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html
      33 # Newer Linux distros may use "wodim" rather than "cdrecord" ...
      34
      35
      36 echo; echo "Done copying $OF to CDR on device $CDROM."
      37
      38 echo "Do you want to erase the image file (y/n)? "  # Probably a huge file.
      39 read answer
      40
      41 case "$answer" in
      42 [yY]) rm -f $OF
      43       echo "$OF erased."
      44       ;;
      45 *)    echo "$OF not erased.";;
      46 esac
      47
      48 echo
      49
      50 # Exercise:
      51 # Change the above "case" statement to also accept "yes" and "Yes" as input.
      52
      53 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 such 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 its input.
      21 #  Sometimes the result is a "chaotic" series.
      22
      23
      24 MAX_ITERATIONS=200
      25 # For large seed numbers (>32000), try increasing 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 # printf does it better ...
      42 COLWIDTH=%7d
      43 printf $COLWIDTH $h
      44
      45   let "remainder = h % 2"
      46   if [ "$remainder" -eq 0 ]   # Even?
      47   then
      48     let "h /= 2"              # Divide by 2.
      49   else
      50     let "h = h*3 + 1"         # Multiply by 3 and add 1.
      51   fi
      52
      53
      54 COLUMNS=10                    # Output 10 values per line.
      55 let "line_break = i % $COLUMNS"
      56 if [ "$line_break" -eq 0 ]
      57 then
      58   echo
      59 fi
      60
      61 done
      62
      63 echo
      64
      65 #  For more information on this strange mathematical function,
      66 #+ see _Computers, Pattern, Chaos, and Beauty_, by Pickover, p. 185 ff.,
      67 #+ as listed in the bibliography.
      68
      69 exit 0

Example A-7. days-between: 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, v. 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=85        # 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 permissible
      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" ] ||
      50   [ "$year" -lt "$REFYR" ] && Param_Error
      51   # Exit script on bad value(s).
      52   # Uses or-list / and-list.
      53   #
      54   # Exercise: Implement more rigorous date checking.
      55 }
      56
      57
      58 strip_leading_zero () #  Better to strip possible leading zero(s)
      59 {                     #+ from day and/or month
      60   return ${1#0}       #+ since otherwise Bash will interpret them
      61 }                     #+ as octal values (POSIX.2, sect 2.9.2.1).
      62
      63
      64 day_index ()          # Gauss' Formula:
      65 {                     # Days from March 1, 1600 to date passed as param.
      66                       #           ^^^^^^^^^^^^^
      67   day=$1
      68   month=$2
      69   year=$3
      70
      71   let "month = $month - 2"
      72   if [ "$month" -le 0 ]
      73   then
      74     let "month += 12"
      75     let "year -= 1"
      76   fi
      77
      78   let "year -= $REFYR"
      79   let "indexyr = $year / $CENTURY"
      80
      81
      82   let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr \
      83               + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM"
      84   #  For an in-depth explanation of this algorithm, see
      85   #+   http://weblogs.asp.net/pgreborio/archive/2005/01/06/347968.aspx
      86
      87
      88   echo $Days
      89
      90 }
      91
      92
      93 calculate_difference ()            # Difference between two day indices.
      94 {
      95   let "diff = $1 - $2"             # Global variable.
      96 }
      97
      98
      99 abs ()                             #  Absolute value
     100 {                                  #  Uses global "value" variable.
     101   if [ "$1" -lt 0 ]                #  If negative
     102   then                             #+ then
     103     let "value = 0 - $1"           #+ change sign,
     104   else                             #+ else
     105     let "value = $1"               #+ leave it alone.
     106   fi
     107 }
     108
     109
     110
     111 if [ $# -ne "$ARGS" ]              # Require two command-line params.
     112 then
     113   Param_Error
     114 fi
     115
     116 Parse_Date $1
     117 check_date $day $month $year       #  See if valid date.
     118
     119 strip_leading_zero $day            #  Remove any leading zeroes
     120 day=$?                             #+ on day and/or month.
     121 strip_leading_zero $month
     122 month=$?
     123
     124 let "date1 = `day_index $day $month $year`"
     125
     126
     127 Parse_Date $2
     128 check_date $day $month $year
     129
     130 strip_leading_zero $day
     131 day=$?
     132 strip_leading_zero $month
     133 month=$?
     134
     135 date2=$(day_index $day $month $year) # Command substitution.
     136
     137
     138 calculate_difference $date1 $date2
     139
     140 abs $diff                            # Make sure it's positive.
     141 diff=$value
     142
     143 echo $diff
     144
     145 exit 0
     146
     147 #  Exercise:
     148 #  --------
     149 #  If given only one command-line parameter, have the script
     150 #+ use today's date as the second.
     151
     152
     153 #  Compare this script with
     154 #+ the implementation of Gauss' Formula in a C program at
     155 #+    http://buschencrew.hypermart.net/software/datedif

Example A-8. Making a dictionary

       1 #!/bin/bash
       2 # makedict.sh  [make dictionary]
       3
       4 # Modification of /usr/sbin/mkdict (/usr/sbin/cracklib-forman) 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 other lexicographic purposes.
      15
      16
      17 E_BADARGS=85
      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
      27                                     #+ options to sort. Changed from
      28                                     #+ original script.
      29
      30 cat $* |                            #  Dump specified files to stdout.
      31         tr A-Z a-z |                #  Convert to lowercase.
      32         tr ' ' '\012' |             #  New: change spaces to newlines.
      33 #       tr -cd '\012[a-z][0-9]' |   #  Get rid of everything
      34                                     #+ non-alphanumeric (in orig. script).
      35         tr -c '\012a-z'  '\012' |   #  Rather than deleting non-alpha
      36                                     #+ chars, change them to newlines.
      37         sort |                      #  $SORT options unnecessary now.
      38         uniq |                      #  Remove duplicates.
      39         grep -v '^#' |              #  Delete lines starting with #.
      40         grep -v '^$'                #  Delete blank lines.
      41
      42 exit $?

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

Example A-11. Data file for Game of Life

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

+++

The following script is by Mark Moraes of the University of Toronto. See the file `Moraes-COPYRIGHT` for permissions and restrictions. This file is included in the combined HTML/source tarball of the ABS Guide.

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 #  Author: 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 exit
      22
      23 # ==> Exercise: Add error checking and other options.
      24 # ==>
      25 # ==> Note that the small sed script repeats, except for the arg passed.
      26 # ==> Does it make sense to embed it in a function? Why or why not?
      27
      28
      29 /*
      30  * Copyright University of Toronto 1988, 1989.
      31  * Written by Mark Moraes
      32  *
      33  * Permission is granted to anyone to use this software for any purpose on
      34  * any computer system, and to alter it and redistribute it freely, subject
      35  * to the following restrictions:
      36  *
      37  * 1. The author and the University of Toronto are not responsible
      38  *    for the consequences of use of this software, no matter how awful,
      39  *    even if they arise from flaws in it.
      40  *
      41  * 2. The origin of this software must not be misrepresented, either by
      42  *    explicit claim or by omission.  Since few users ever read sources,
      43  *    credits must appear in the documentation.
      44  *
      45  * 3. Altered versions must be plainly marked as such, and must not be
      46  *    misrepresented as being the original software.  Since few users
      47  *    ever read sources, credits must appear in the documentation.
      48  *
      49  * 4. This notice may not be removed or altered.
      50  */

+

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

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

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

+

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

Example A-14. 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   # ==>  Is it necessary to delete the pipe before exiting the script?
      31   # ==>  How could that be done?
      32
      33
      34   exit 0

+

Stéphane Chazelas used the following script to demonstrate generating prime numbers without arrays.

Example A-15. 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 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" 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
      33 			      #+ positional parameters.
      34                               #  "$@" is the accumulating list of primes.
      35 }
      36
      37 Primes 1
      38
      39 exit $?
      40
      41 # Pipe output of the script to 'fmt' for prettier printing.
      42
      43 #  Uncomment lines 16 and 24 to help figure out what is going on.
      44
      45 #  Compare the speed of this algorithm for generating primes
      46 #+ with the Sieve of Eratosthenes (ex68.sh).
      47
      48
      49 #  Exercise: Rewrite this script without recursion.

+

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

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

Patsie's version of a directory tree script.

Example A-17. tree2: Alternate directory tree script

       1 #!/bin/bash
       2 # tree2.sh
       3
       4 # Lightly modified/reformatted by ABS Guide author.
       5 # Included in ABS Guide with permission of script author (thanks!).
       6
       7 ## Recursive file/dirsize checking script, by Patsie
       8 ##
       9 ## This script builds a list of files/directories and their size (du -akx)
      10 ## and processes this list to a human readable tree shape
      11 ## The 'du -akx' is only as good as the permissions the owner has.
      12 ## So preferably run as root* to get the best results, or use only on
      13 ## directories for which you have read permissions. Anything you can't
      14 ## read is not in the list.
      15
      16 #* ABS Guide author advises caution when running scripts as root!
      17
      18
      19 ##########  THIS IS CONFIGURABLE  ##########
      20
      21 TOP=5                   # Top 5 biggest (sub)directories.
      22 MAXRECURS=5             # Max 5 subdirectories/recursions deep.
      23 E_BL=80                 # Blank line already returned.
      24 E_DIR=81                # Directory not specified.
      25
      26
      27 ##########  DON'T CHANGE ANYTHING BELOW THIS LINE  ##########
      28
      29 PID=$$                            # Our own process ID.
      30 SELF=`basename $0`                # Our own program name.
      31 TMP="/tmp/${SELF}.${PID}.tmp"     # Temporary 'du' result.
      32
      33 # Convert number to dotted thousand.
      34 function dot { echo "            $*" |
      35                sed -e :a -e 's/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/;ta' |
      36                tail -c 12; }
      37
      38 # Usage: tree <recursion> <indent prefix> <min size> <directory>
      39 function tree {
      40   recurs="$1"           # How deep nested are we?
      41   prefix="$2"           # What do we display before file/dirname?
      42   minsize="$3"          # What is the minumum file/dirsize?
      43   dirname="$4"          # Which directory are we checking?
      44
      45 # Get ($TOP) biggest subdirs/subfiles from TMP file.
      46   LIST=`egrep "[[:space:]]${dirname}/[^/]*$" "$TMP" |
      47         awk '{if($1>'$minsize') print;}' | sort -nr | head -$TOP`
      48   [ -z "$LIST" ] && return        # Empty list, then go back.
      49
      50   cnt=0
      51   num=`echo "$LIST" | wc -l`      # How many entries in the list.
      52
      53   ## Main loop
      54   echo "$LIST" | while read size name; do
      55     ((cnt+=1))		          # Count entry number.
      56     bname=`basename "$name"`      # We only need a basename of the entry.
      57     [ -d "$name" ] && bname="$bname/"
      58                                   # If it's a directory, append a slash.
      59     echo "`dot $size`$prefix +-$bname"
      60                                   # Display the result.
      61     #  Call ourself recursively if it's a directory
      62     #+ and we're not nested too deep ($MAXRECURS).
      63     #  The recursion goes up: $((recurs+1))
      64     #  The prefix gets a space if it's the last entry,
      65     #+ or a pipe if there are more entries.
      66     #  The minimum file/dirsize becomes
      67     #+ a tenth of his parent: $((size/10)).
      68     # Last argument is the full directory name to check.
      69     if [ -d "$name" -a $recurs -lt $MAXRECURS ]; then
      70       [ $cnt -lt $num ] \
      71         || (tree $((recurs+1)) "$prefix  " $((size/10)) "$name") \
      72         && (tree $((recurs+1)) "$prefix |" $((size/10)) "$name")
      73     fi
      74   done
      75
      76   [ $? -eq 0 ] && echo "           $prefix"
      77   # Every time we jump back add a 'blank' line.
      78   return $E_BL
      79   # We return 80 to tell we added a blank line already.
      80 }
      81
      82 ###                ###
      83 ###  main program  ###
      84 ###                ###
      85
      86 rootdir="$@"
      87 [ -d "$rootdir" ] ||
      88   { echo "$SELF: Usage: $SELF <directory>" >&2; exit $E_DIR; }
      89   # We should be called with a directory name.
      90
      91 echo "Building inventory list, please wait ..."
      92      # Show "please wait" message.
      93 du -akx "$rootdir" 1>"$TMP" 2>/dev/null
      94      # Build a temporary list of all files/dirs and their size.
      95 size=`tail -1 "$TMP" | awk '{print $1}'`
      96      # What is our rootdirectory's size?
      97 echo "`dot $size` $rootdir"
      98      # Display rootdirectory's entry.
      99 tree 0 "" 0 "$rootdir"
     100      # Display the tree below our rootdirectory.
     101
     102 rm "$TMP" 2>/dev/null
     103      # Clean up TMP file.
     104
     105 exit $?

Noah Friedman permitted use of his string function script. It essentially reproduces some of the C-library string manipulation functions.

Example A-18. string functions: C-style 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.

Mariusz Gniazdowski contributed a hash library for use in scripts.

Example A-20. Library of hash functions

       1 # Hash:
       2 # Hash function library
       3 # Author: Mariusz Gniazdowski <mariusz.gn-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}\
      70 =\"\$${Hash_config_varname_prefix}${3}_${4}\""
      71 }
      72
      73
      74 # Emulates:  hash[keyN-1]=hash[key2]=...hash[key1]
      75 #
      76 # Copies first key to rest of keys.
      77 #
      78 # Params:
      79 # 1 - hash1
      80 # 2 - key1
      81 # 3 - key2
      82 # . . .
      83 # N - keyN
      84 function hash_dup {
      85   local hashName="$1" keyName="$2"
      86   shift 2
      87   until [ ${#} -le 0 ]; do
      88     eval "${Hash_config_varname_prefix}${hashName}_${1}\
      89 =\"\$${Hash_config_varname_prefix}${hashName}_${keyName}\""
      90   shift;
      91   done;
      92 }
      93
      94
      95 # Emulates:  unset hash[key]
      96 #
      97 # Params:
      98 # 1 - hash
      99 # 2 - key
     100 function hash_unset {
     101 	eval "unset ${Hash_config_varname_prefix}${1}_${2}"
     102 }
     103
     104
     105 # Emulates something similar to:  ref=&hash[key]
     106 #
     107 # The reference is name of the variable in which value is held.
     108 #
     109 # Params:
     110 # 1 - hash
     111 # 2 - key
     112 # 3 - ref - Name of global variable to set.
     113 function hash_get_ref_into {
     114 	eval "$3=\"${Hash_config_varname_prefix}${1}_${2}\""
     115 }
     116
     117
     118 # Emulates something similar to:  echo &hash[key]
     119 #
     120 # That reference is name of variable in which value is held.
     121 #
     122 # Params:
     123 # 1 - hash
     124 # 2 - key
     125 # 3 - echo params (like -n for example)
     126 function hash_echo_ref {
     127 	eval "echo $3 \"${Hash_config_varname_prefix}${1}_${2}\""
     128 }
     129
     130
     131
     132 # Emulates something similar to:  $$hash[key](param1, param2, ...)
     133 #
     134 # Params:
     135 # 1 - hash
     136 # 2 - key
     137 # 3,4, ... - Function parameters
     138 function hash_call {
     139   local hash key
     140   hash=$1
     141   key=$2
     142   shift 2
     143   eval "eval \"\$${Hash_config_varname_prefix}${hash}_${key} \\\"\\\$@\\\"\""
     144 }
     145
     146
     147 # Emulates something similar to:  isset(hash[key]) or hash[key]==NULL
     148 #
     149 # Params:
     150 # 1 - hash
     151 # 2 - key
     152 # Returns:
     153 # 0 - there is such key
     154 # 1 - there is no such key
     155 function hash_is_set {
     156   eval "if [[ \"\${${Hash_config_varname_prefix}${1}_${2}-a}\" = \"a\" &&
     157   \"\${${Hash_config_varname_prefix}${1}_${2}-b}\" = \"b\" ]]
     158     then return 1; else return 0; fi"
     159 }
     160
     161
     162 # Emulates something similar to:
     163 #   foreach($hash as $key => $value) { fun($key,$value); }
     164 #
     165 # It is possible to write different variations of this function.
     166 # Here we use a function call to make it as "generic" as possible.
     167 #
     168 # Params:
     169 # 1 - hash
     170 # 2 - function name
     171 function hash_foreach {
     172   local keyname oldIFS="$IFS"
     173   IFS=' '
     174   for i in $(eval "echo \${!${Hash_config_varname_prefix}${1}_*}"); do
     175     keyname=$(eval "echo \${i##${Hash_config_varname_prefix}${1}_}")
     176     eval "$2 $keyname \"\$$i\""
     177   done
     178 IFS="$oldIFS"
     179 }
     180
     181 #  NOTE: In lines 103 and 116, ampersand changed.
     182 #  But, it doesn't matter, because these are comment lines anyhow.

Here is an example script using the foregoing hash library.

Example A-21. Colorizing text using hash functions

       1 #!/bin/bash
       2 # hash-example.sh: Colorizing text.
       3 # Author: Mariusz Gniazdowski <mariusz.gn-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

An example illustrating the mechanics of hashing, but from a different point of view.

Example A-22. More on hash functions

       1 #!/bin/bash
       2 # $Id: ha.sh,v 1.2 2005/04/21 23:24:26 oliver Exp $
       3 # Copyright 2005 Oliver Beckstein
       4 # Released under the GNU Public License
       5 # Author of script granted permission for inclusion in ABS Guide.
       6 # (Thank you!)
       7
       8 #----------------------------------------------------------------
       9 # pseudo hash based on indirect parameter expansion
      10 # API: access through functions:
      11 #
      12 # create the hash:
      13 #
      14 #      newhash Lovers
      15 #
      16 # add entries (note single quotes for spaces)
      17 #
      18 #      addhash Lovers Tristan Isolde
      19 #      addhash Lovers 'Romeo Montague' 'Juliet Capulet'
      20 #
      21 # access value by key
      22 #
      23 #      gethash Lovers Tristan   ---->  Isolde
      24 #
      25 # show all keys
      26 #
      27 #      keyshash Lovers         ----> 'Tristan'  'Romeo Montague'
      28 #
      29 #
      30 # Convention: instead of perls' foo{bar} = boing' syntax,
      31 # use
      32 #       '_foo_bar=boing' (two underscores, no spaces)
      33 #
      34 # 1) store key   in _NAME_keys[]
      35 # 2) store value in _NAME_values[] using the same integer index
      36 # The integer index for the last entry is _NAME_ptr
      37 #
      38 # NOTE: No error or sanity checks, just bare bones.
      39
      40
      41 function _inihash () {
      42     # private function
      43     # call at the beginning of each procedure
      44     # defines: _keys _values _ptr
      45     #
      46     # Usage: _inihash NAME
      47     local name=$1
      48     _keys=_${name}_keys
      49     _values=_${name}_values
      50     _ptr=_${name}_ptr
      51 }
      52
      53 function newhash () {
      54     # Usage: newhash NAME
      55     #        NAME should not contain spaces or dots.
      56     #        Actually: it must be a legal name for a Bash variable.
      57     # We rely on Bash automatically recognising arrays.
      58     local name=$1
      59     local _keys _values _ptr
      60     _inihash ${name}
      61     eval ${_ptr}=0
      62 }
      63
      64
      65 function addhash () {
      66     # Usage: addhash NAME KEY 'VALUE with spaces'
      67     #        arguments with spaces need to be quoted with single quotes ''
      68     local name=$1 k="$2" v="$3"
      69     local _keys _values _ptr
      70     _inihash ${name}
      71
      72     #echo "DEBUG(addhash): ${_ptr}=${!_ptr}"
      73
      74     eval let ${_ptr}=${_ptr}+1
      75     eval "$_keys[${!_ptr}]=\"${k}\""
      76     eval "$_values[${!_ptr}]=\"${v}\""
      77 }
      78
      79 function gethash () {
      80     #  Usage: gethash NAME KEY
      81     #         Returns boing
      82     #         ERR=0 if entry found, 1 otherwise
      83     #  That's not a proper hash --
      84     #+ we simply linearly search through the keys.
      85     local name=$1 key="$2"
      86     local _keys _values _ptr
      87     local k v i found h
      88     _inihash ${name}
      89
      90     # _ptr holds the highest index in the hash
      91     found=0
      92
      93     for i in $(seq 1 ${!_ptr}); do
      94 	h="\${${_keys}[${i}]}"  #  Safer to do it in two steps,
      95 	eval k=${h}             #+ especially when quoting for spaces.
      96 	if [ "${k}" = "${key}" ]; then found=1; break; fi
      97     done;
      98
      99     [ ${found} = 0 ] && return 1;
     100     # else: i is the index that matches the key
     101     h="\${${_values}[${i}]}"
     102     eval echo "${h}"
     103     return 0;
     104 }
     105
     106 function keyshash () {
     107     # Usage: keyshash NAME
     108     # Returns list of all keys defined for hash name.
     109     local name=$1 key="$2"
     110     local _keys _values _ptr
     111     local k i h
     112     _inihash ${name}
     113
     114     # _ptr holds the highest index in the hash
     115     for i in $(seq 1 ${!_ptr}); do
     116 	h="\${${_keys}[${i}]}"   #  Safer to do it in two steps,
     117 	eval k=${h}              #+ especially when quoting for spaces.
     118 	echo -n "'${k}' "
     119     done;
     120 }
     121
     122
     123 # -----------------------------------------------------------------------
     124
     125 # Now, let's test it.
     126 # (Per comments at the beginning of the script.)
     127 newhash Lovers
     128 addhash Lovers Tristan Isolde
     129 addhash Lovers 'Romeo Montague' 'Juliet Capulet'
     130
     131 # Output results.
     132 echo
     133 gethash Lovers Tristan      # Isolde
     134 echo
     135 keyshash Lovers             # 'Tristan' 'Romeo Montague'
     136 echo; echo
     137
     138
     139 exit 0
     140
     141 # Exercise:
     142 # --------
     143
     144 # Add error checks to the functions.

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 |
      50   xargs grep -l 'Attached: Yes'
      51 }
      52 function scsiDevFromScsiUsb {
      53   echo $1 | awk -F"[-/]" '{ n=$(NF-1);
      54   print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }'
      55 }
      56
      57 if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
      58     ##
      59     # lifted from usbcam code.
      60     #
      61     if [ -f /var/run/console.lock ]; then
      62         CONSOLEOWNER=`cat /var/run/console.lock`
      63     elif [ -f /var/lock/console.lock ]; then
      64         CONSOLEOWNER=`cat /var/lock/console.lock`
      65     else
      66         CONSOLEOWNER=
      67     fi
      68     for procEntry in $(allAttachedScsiUsb); do
      69         scsiDev=$(scsiDevFromScsiUsb $procEntry)
      70         #  Some bug with usb-storage?
      71         #  Partitions are not in /proc/partitions until they are accessed
      72         #+ somehow.
      73         /sbin/fdisk -l $scsiDev >/dev/null
      74         ##
      75         #  Most devices have partitioning info, so the data would be on
      76         #+ /dev/sd?1. However, some stupider ones don't have any partitioning
      77         #+ and use the entire device for data storage. This tries to
      78         #+ guess semi-intelligently if we have a /dev/sd?1 and if not, then
      79         #+ it uses the entire device and hopes for the better.
      80         #
      81         if grep -q `basename $scsiDev`1 /proc/partitions; then
      82             part="$scsiDev""1"
      83         else
      84             part=$scsiDev
      85         fi
      86         ##
      87         #  Change ownership of the partition to the console user so they can
      88         #+ mount it.
      89         #
      90         if [ ! -z "$CONSOLEOWNER" ]; then
      91             chown $CONSOLEOWNER:disk $part
      92         fi
      93         ##
      94         # This checks if we already have this UUID defined with devlabel.
      95         # If not, it then adds the device to the list.
      96         #
      97         prodid=`$DEVLABEL printid -d $part`
      98         if ! grep -q $prodid $DEVLABELCONFIG; then
      99             # cross our fingers and hope it works
     100             $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null
     101         fi
     102         ##
     103         # Check if the mount point exists and create if it doesn't.
     104         #
     105         if [ ! -e $MOUNTPOINT ]; then
     106             mkdir -p $MOUNTPOINT
     107         fi
     108         ##
     109         # Take care of /etc/fstab so mounting is easy.
     110         #
     111         if ! grep -q "^$SYMLINKDEV" /etc/fstab; then
     112             # Add an fstab entry
     113             echo -e \
     114                 "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \
     115                 >> /etc/fstab
     116         fi
     117     done
     118     if [ ! -z "$REMOVER" ]; then
     119         ##
     120         # Make sure this script is triggered on device removal.
     121         #
     122         mkdir -p `dirname $REMOVER`
     123         ln -s $IAM $REMOVER
     124     fi
     125 elif [ "${ACTION}" = "remove" ]; then
     126     ##
     127     # If the device is mounted, unmount it cleanly.
     128     #
     129     if grep -q "$MOUNTPOINT" /etc/mtab; then
     130         # unmount cleanly
     131         umount -l $MOUNTPOINT
     132     fi
     133     ##
     134     # Remove it from /etc/fstab if it's there.
     135     #
     136     if grep -q "^$SYMLINKDEV" /etc/fstab; then
     137         grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new
     138         mv -f /etc/.fstab.new /etc/fstab
     139     fi
     140 fi
     141
     142 exit 0

Converting a text file to HTML format.

Example A-24. Converting to HTML

       1 #!/bin/bash
       2 # tohtml.sh [v. 0.2.01, reldate: 04/13/12, a teeny bit less buggy]
       3
       4 # Convert a text file to HTML format.
       5 # Author: Mendel Cooper
       6 # License: GPL3
       7 # Usage: sh tohtml.sh < textfile > htmlfile
       8 # Script can easily be modified to accept source and target filenames.
       9
      10 #    Assumptions:
      11 # 1) Paragraphs in (target) text file are separated by a blank line.
      12 # 2) Jpeg images (*.jpg) are located in "images" subdirectory.
      13 #    In the target file, the image names are enclosed in square brackets,
      14 #    for example, [image01.jpg].
      15 # 3) Emphasized (italic) phrases begin with a space+underscore
      16 #+   or the first character on the line is an underscore,
      17 #+   and end with an underscore+space or underscore+end-of-line.
      18
      19
      20 # Settings
      21 FNTSIZE=2        # Small-medium font size
      22 IMGDIR="images"  # Image directory
      23 # Headers
      24 HDR01='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
      25 HDR02='<!-- Converted to HTML by ***tohtml.sh*** script -->'
      26 HDR03='<!-- script author: M. Leo Cooper <thegrendel.abs@gmail.com> -->'
      27 HDR10='<html>'
      28 HDR11='<head>'
      29 HDR11a='</head>'
      30 HDR12a='<title>'
      31 HDR12b='</title>'
      32 HDR121='<META NAME="GENERATOR" CONTENT="tohtml.sh script">'
      33 HDR13='<body bgcolor="#dddddd">'   # Change background color to suit.
      34 HDR14a='<font size='
      35 HDR14b='>'
      36 # Footers
      37 FTR10='</body>'
      38 FTR11='</html>'
      39 # Tags
      40 BOLD="<b>"
      41 CENTER="<center>"
      42 END_CENTER="</center>"
      43 LF="<br>"
      44
      45
      46 write_headers ()
      47   {
      48   echo "$HDR01"
      49   echo
      50   echo "$HDR02"
      51   echo "$HDR03"
      52   echo
      53   echo
      54   echo "$HDR10"
      55   echo "$HDR11"
      56   echo "$HDR121"
      57   echo "$HDR11a"
      58   echo "$HDR13"
      59   echo
      60   echo -n "$HDR14a"
      61   echo -n "$FNTSIZE"
      62   echo "$HDR14b"
      63   echo
      64   echo "$BOLD"        # Everything in bold (more easily readable).
      65   }
      66
      67
      68 process_text ()
      69   {
      70   while read line     # Read one line at a time.
      71   do
      72     {
      73     if [ ! "$line" ]  # Blank line?
      74     then              # Then new paragraph must follow.
      75       echo
      76       echo "$LF"      # Insert two <br> tags.
      77       echo "$LF"
      78       echo
      79       continue        # Skip the underscore test.
      80     else              # Otherwise . . .
      81
      82       if [[ "$line" =~ \[*jpg\] ]]    # Is a graphic?
      83       then                            # Strip away brackets.
      84         temp=$( echo "$line" | sed -e 's/\[//' -e 's/\]//' )
      85         line=""$CENTER" <img src="\"$IMGDIR"/$temp\"> "$END_CENTER" "
      86                                       # Add image tag.
      87                                       # And, center it.
      88       fi
      89
      90     fi
      91
      92
      93     echo "$line" | grep -q _
      94     if [ "$?" -eq 0 ]    # If line contains underscore ...
      95     then
      96       # ===================================================
      97       # Convert underscored phrase to italics.
      98       temp=$( echo "$line" |
      99               sed -e 's/ _/ <i>/' -e 's/_/<\/i> /' |
     100               sed -e 's/^_/<i>/'  -e 's/_/<\/i>/' )
     101       #  Process only underscores prefixed by space,
     102       #+ or at beginning or end of line.
     103       #  Do not convert underscores embedded within a word!
     104       line="$temp"
     105       # Slows script execution. Can be optimized?
     106       # ===================================================
     107     fi
     108
     109
     110
     111 #   echo
     112     echo "$line"
     113 #   echo
     114 #   Don't want extra blank lines in generated text!
     115     } # End while
     116   done
     117   }   # End process_text ()
     118
     119
     120 write_footers ()  # Termination tags.
     121   {
     122   echo "$FTR10"
     123   echo "$FTR11"
     124   }
     125
     126
     127 # main () {
     128 # =========
     129 write_headers
     130 process_text
     131 write_footers
     132 # =========
     133 #         }
     134
     135 exit $?
     136
     137 #  Exercises:
     138 #  ---------
     139 #  1) Fixup: Check for closing underscore before a comma or period.
     140 #  2) Add a test for the presence of a closing underscore
     141 #+    in phrases to be italicized.

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

Example A-25. 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 to keep the shell from expanding and reinterpreting text strings.

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

But, what if you want the shell to expand and reinterpret strings?

Example A-27. 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 interesting script helps hunt down spammers.

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

Another anti-spam script.

Example A-29. Spammer Hunt

       1 #!/bin/bash
       2 # whx.sh: "whois" spammer lookup
       3 # Author: Walter Dnes
       4 # Slight revisions (first section) by ABS Guide author.
       5 # Used in ABS Guide with permission.
       6
       7 # Needs version 3.x or greater of Bash to run (because of =~ operator).
       8 # Commented by script author and ABS Guide author.
       9
      10
      11
      12 E_BADARGS=85        # Missing command-line arg.
      13 E_NOHOST=86         # Host not found.
      14 E_TIMEOUT=87        # Host lookup timed out.
      15 E_UNDEF=88          # Some other (undefined) error.
      16
      17 HOSTWAIT=10         # Specify up to 10 seconds for host query reply.
      18                     # The actual wait may be a bit longer.
      19 OUTFILE=whois.txt   # Output file.
      20 PORT=4321
      21
      22
      23 if [ -z "$1" ]      # Check for (required) command-line arg.
      24 then
      25   echo "Usage: $0 domain name or IP address"
      26   exit $E_BADARGS
      27 fi
      28
      29
      30 if [[ "$1" =~ [a-zA-Z][a-zA-Z]$ ]]  #  Ends in two alpha chars?
      31 then                                  #  It's a domain name &&
      32                                       #+ must do host lookup.
      33   IPADDR=$(host -W $HOSTWAIT $1 | awk '{print $4}')
      34                                       #  Doing host lookup
      35                                       #+ to get IP address.
      36 				      #  Extract final field.
      37 else
      38   IPADDR="$1"                         #  Command-line arg was IP address.
      39 fi
      40
      41 echo; echo "IP Address is: "$IPADDR""; echo
      42
      43 if [ -e "$OUTFILE" ]
      44 then
      45   rm -f "$OUTFILE"
      46   echo "Stale output file \"$OUTFILE\" removed."; echo
      47 fi
      48
      49
      50 #  Sanity checks.
      51 #  (This section needs more work.)
      52 #  ===============================
      53 if [ -z "$IPADDR" ]
      54 # No response.
      55 then
      56   echo "Host not found!"
      57   exit $E_NOHOST    # Bail out.
      58 fi
      59
      60 if [[ "$IPADDR" =~ ^[;;] ]]
      61 #  ;; Connection timed out; no servers could be reached.
      62 then
      63   echo "Host lookup timed out!"
      64   exit $E_TIMEOUT   # Bail out.
      65 fi
      66
      67 if [[ "$IPADDR" =~ [(NXDOMAIN)]$ ]]
      68 #  Host xxxxxxxxx.xxx not found: 3(NXDOMAIN)
      69 then
      70   echo "Host not found!"
      71   exit $E_NOHOST    # Bail out.
      72 fi
      73
      74 if [[ "$IPADDR" =~ [(SERVFAIL)]$ ]]
      75 #  Host xxxxxxxxx.xxx not found: 2(SERVFAIL)
      76 then
      77   echo "Host not found!"
      78   exit $E_NOHOST    # Bail out.
      79 fi
      80
      81
      82
      83
      84 # ======================== Main body of script ========================
      85
      86 AFRINICquery() {
      87 #  Define the function that queries AFRINIC. Echo a notification to the
      88 #+ screen, and then run the actual query, redirecting output to $OUTFILE.
      89
      90   echo "Searching for $IPADDR in whois.afrinic.net"
      91   whois -h whois.afrinic.net "$IPADDR" > $OUTFILE
      92
      93 #  Check for presence of reference to an rwhois.
      94 #  Warn about non-functional rwhois.infosat.net server
      95 #+ and attempt rwhois query.
      96   if grep -e "^remarks: .*rwhois\.[^ ]\+" "$OUTFILE"
      97   then
      98     echo " " >> $OUTFILE
      99     echo "***" >> $OUTFILE
     100     echo "***" >> $OUTFILE
     101     echo "Warning: rwhois.infosat.net was not working \
     102       as of 2005/02/02" >> $OUTFILE
     103     echo "         when this script was written." >> $OUTFILE
     104     echo "***" >> $OUTFILE
     105     echo "***" >> $OUTFILE
     106     echo " " >> $OUTFILE
     107     RWHOIS=`grep "^remarks: .*rwhois\.[^ ]\+" "$OUTFILE" | tail -n 1 |\
     108     sed "s/\(^.*\)\(rwhois\..*\)\(:4.*\)/\2/"`
     109     whois -h ${RWHOIS}:${PORT} "$IPADDR" >> $OUTFILE
     110   fi
     111 }
     112
     113 APNICquery() {
     114   echo "Searching for $IPADDR in whois.apnic.net"
     115   whois -h whois.apnic.net "$IPADDR" > $OUTFILE
     116
     117 #  Just  about  every  country has its own internet registrar.
     118 #  I don't normally bother consulting them, because the regional registry
     119 #+ usually supplies sufficient information.
     120 #  There are a few exceptions, where the regional registry simply
     121 #+ refers to the national registry for direct data.
     122 #  These are Japan and South Korea in APNIC, and Brasil in LACNIC.
     123 #  The following if statement checks $OUTFILE (whois.txt) for the presence
     124 #+ of "KR" (South Korea) or "JP" (Japan) in the country field.
     125 #  If either is found, the query is re-run against the appropriate
     126 #+ national registry.
     127
     128   if grep -E "^country:[ ]+KR$" "$OUTFILE"
     129   then
     130     echo "Searching for $IPADDR in whois.krnic.net"
     131     whois -h whois.krnic.net "$IPADDR" >> $OUTFILE
     132   elif grep -E "^country:[ ]+JP$" "$OUTFILE"
     133   then
     134     echo "Searching for $IPADDR in whois.nic.ad.jp"
     135     whois -h whois.nic.ad.jp "$IPADDR"/e >> $OUTFILE
     136   fi
     137 }
     138
     139 ARINquery() {
     140   echo "Searching for $IPADDR in whois.arin.net"
     141   whois -h whois.arin.net "$IPADDR" > $OUTFILE
     142
     143 #  Several large internet providers listed by ARIN have their own
     144 #+ internal whois service, referred to as "rwhois".
     145 #  A large block of IP addresses is listed with the provider
     146 #+ under the ARIN registry.
     147 #  To get the IP addresses of 2nd-level ISPs or other large customers,
     148 #+ one has to refer to the rwhois server on port 4321.
     149 #  I originally started with a bunch of "if" statements checking for
     150 #+ the larger providers.
     151 #  This approach is unwieldy, and there's always another rwhois server
     152 #+ that I didn't know about.
     153 #  A more elegant approach is to check $OUTFILE for a reference
     154 #+ to a whois server, parse that server name out of the comment section,
     155 #+ and re-run the query against the appropriate rwhois server.
     156 #  The parsing looks a bit ugly, with a long continued line inside
     157 #+ backticks.
     158 #  But it only has to be done once, and will work as new servers are added.
     159 #@   ABS Guide author comment: it isn't all that ugly, and is, in fact,
     160 #@+  an instructive use of Regular Expressions.
     161
     162   if grep -E "^Comment: .*rwhois.[^ ]+" "$OUTFILE"
     163   then
     164     RWHOIS=`grep -e "^Comment:.*rwhois\.[^ ]\+" "$OUTFILE" | tail -n 1 |\
     165     sed "s/^\(.*\)\(rwhois\.[^ ]\+\)\(.*$\)/\2/"`
     166     echo "Searching for $IPADDR in ${RWHOIS}"
     167     whois -h ${RWHOIS}:${PORT} "$IPADDR" >> $OUTFILE
     168   fi
     169 }
     170
     171 LACNICquery() {
     172   echo "Searching for $IPADDR in whois.lacnic.net"
     173   whois -h whois.lacnic.net "$IPADDR" > $OUTFILE
     174
     175 #  The  following if statement checks $OUTFILE (whois.txt) for
     176 #+ the presence of "BR" (Brasil) in the country field.
     177 #  If it is found, the query is re-run against whois.registro.br.
     178
     179   if grep -E "^country:[ ]+BR$" "$OUTFILE"
     180   then
     181     echo "Searching for $IPADDR in whois.registro.br"
     182     whois -h whois.registro.br "$IPADDR" >> $OUTFILE
     183   fi
     184 }
     185
     186 RIPEquery() {
     187   echo "Searching for $IPADDR in whois.ripe.net"
     188   whois -h whois.ripe.net "$IPADDR" > $OUTFILE
     189 }
     190
     191 #  Initialize a few variables.
     192 #  * slash8 is the most significant octet
     193 #  * slash16 consists of the two most significant octets
     194 #  * octet2 is the second most significant octet
     195
     196
     197
     198
     199 slash8=`echo $IPADDR | cut -d. -f 1`
     200   if [ -z "$slash8" ]  # Yet another sanity check.
     201   then
     202     echo "Undefined error!"
     203     exit $E_UNDEF
     204   fi
     205 slash16=`echo $IPADDR | cut -d. -f 1-2`
     206 #                             ^ Period specified as 'cut" delimiter.
     207   if [ -z "$slash16" ]
     208   then
     209     echo "Undefined error!"
     210     exit $E_UNDEF
     211   fi
     212 octet2=`echo $slash16 | cut -d. -f 2`
     213   if [ -z "$octet2" ]
     214   then
     215     echo "Undefined error!"
     216     exit $E_UNDEF
     217   fi
     218
     219
     220 #  Check for various odds and ends of reserved space.
     221 #  There is no point in querying for those addresses.
     222
     223 if [ $slash8 == 0 ]; then
     224   echo $IPADDR is '"This Network"' space\; Not querying
     225 elif [ $slash8 == 10 ]; then
     226   echo $IPADDR is RFC1918 space\; Not querying
     227 elif [ $slash8 == 14 ]; then
     228   echo $IPADDR is '"Public Data Network"' space\; Not querying
     229 elif [ $slash8 == 127 ]; then
     230   echo $IPADDR is loopback space\; Not querying
     231 elif [ $slash16 == 169.254 ]; then
     232   echo $IPADDR is link-local space\; Not querying
     233 elif [ $slash8 == 172 ] && [ $octet2 -ge 16 ] && [ $octet2 -le 31 ];then
     234   echo $IPADDR is RFC1918 space\; Not querying
     235 elif [ $slash16 == 192.168 ]; then
     236   echo $IPADDR is RFC1918 space\; Not querying
     237 elif [ $slash8 -ge 224 ]; then
     238   echo $IPADDR is either Multicast or reserved space\; Not querying
     239 elif [ $slash8 -ge 200 ] && [ $slash8 -le 201 ]; then LACNICquery "$IPADDR"
     240 elif [ $slash8 -ge 202 ] && [ $slash8 -le 203 ]; then APNICquery "$IPADDR"
     241 elif [ $slash8 -ge 210 ] && [ $slash8 -le 211 ]; then APNICquery "$IPADDR"
     242 elif [ $slash8 -ge 218 ] && [ $slash8 -le 223 ]; then APNICquery "$IPADDR"
     243
     244 #  If we got this far without making a decision, query ARIN.
     245 #  If a reference is found in $OUTFILE to APNIC, AFRINIC, LACNIC, or RIPE,
     246 #+ query the appropriate whois server.
     247
     248 else
     249   ARINquery "$IPADDR"
     250   if grep "whois.afrinic.net" "$OUTFILE"; then
     251     AFRINICquery "$IPADDR"
     252   elif grep -E "^OrgID:[ ]+RIPE$" "$OUTFILE"; then
     253     RIPEquery "$IPADDR"
     254   elif grep -E "^OrgID:[ ]+APNIC$" "$OUTFILE"; then
     255     APNICquery "$IPADDR"
     256   elif grep -E "^OrgID:[ ]+LACNIC$" "$OUTFILE"; then
     257     LACNICquery "$IPADDR"
     258   fi
     259 fi
     260
     261 #@  ---------------------------------------------------------------
     262 #   Try also:
     263 #   wget http://logi.cc/nw/whois.php3?ACTION=doQuery&DOMAIN=$IPADDR
     264 #@  ---------------------------------------------------------------
     265
     266 #  We've  now  finished  the querying.
     267 #  Echo a copy of the final result to the screen.
     268
     269 cat $OUTFILE
     270 # Or "less $OUTFILE" . . .
     271
     272
     273 exit 0
     274
     275 #@  ABS Guide author comments:
     276 #@  Nothing fancy here, but still a very useful tool for hunting spammers.
     277 #@  Sure, the script can be cleaned up some, and it's still a bit buggy,
     278 #@+ (exercise for reader), but all the same, it's a nice piece of coding
     279 #@+ by Walter Dnes.
     280 #@  Thank you!

"Little Monster's" front end to wget.

Example A-30. Making wget easier to use

       1 #!/bin/bash
       2 # wgetter2.bash
       3
       4 # Author: Little Monster [monster@monstruum.co.uk]
       5 # ==> Used in ABS Guide with permission of script author.
       6 # ==> This script still needs debugging and fixups (exercise for reader).
       7 # ==> It could also use some additional editing in the comments.
       8
       9
      10 #  This is wgetter2 --
      11 #+ a Bash script to make wget a bit more friendly, and save typing.
      12
      13 #  Carefully crafted by Little Monster.
      14 #  More or less complete on 02/02/2005.
      15 #  If you think this script can be improved,
      16 #+ email me at: monster@monstruum.co.uk
      17 # ==> and cc: to the author of the ABS Guide, please.
      18 #  This script is licenced under the GPL.
      19 #  You are free to copy, alter and re-use it,
      20 #+ but please don't try to claim you wrote it.
      21 #  Log your changes here instead.
      22
      23 # =======================================================================
      24 # changelog:
      25
      26 # 07/02/2005.  Fixups by Little Monster.
      27 # 02/02/2005.  Minor additions by Little Monster.
      28 #              (See after # +++++++++++ )
      29 # 29/01/2005.  Minor stylistic edits and cleanups by author of ABS Guide.
      30 #              Added exit error codes.
      31 # 22/11/2004.  Finished initial version of second version of wgetter:
      32 #              wgetter2 is born.
      33 # 01/12/2004.  Changed 'runn' function so it can be run 2 ways --
      34 #              either ask for a file name or have one input on the CL.
      35 # 01/12/2004.  Made sensible handling of no URL's given.
      36 # 01/12/2004.  Made loop of main options, so you don't
      37 #              have to keep calling wgetter 2 all the time.
      38 #              Runs as a session instead.
      39 # 01/12/2004.  Added looping to 'runn' function.
      40 #              Simplified and improved.
      41 # 01/12/2004.  Added state to recursion setting.
      42 #              Enables re-use of previous value.
      43 # 05/12/2004.  Modified the file detection routine in the 'runn' function
      44 #              so it's not fooled by empty values, and is cleaner.
      45 # 01/02/2004.  Added cookie finding routine from later version (which
      46 #              isn't ready yet), so as not to have hard-coded paths.
      47 # =======================================================================
      48
      49 # Error codes for abnormal exit.
      50 E_USAGE=67        # Usage message, then quit.
      51 E_NO_OPTS=68      # No command-line args entered.
      52 E_NO_URLS=69      # No URLs passed to script.
      53 E_NO_SAVEFILE=70  # No save filename passed to script.
      54 E_USER_EXIT=71    # User decides to quit.
      55
      56
      57 #  Basic default wget command we want to use.
      58 #  This is the place to change it, if required.
      59 #  NB: if using a proxy, set http_proxy = yourproxy in .wgetrc.
      60 #  Otherwise delete --proxy=on, below.
      61 # ====================================================================
      62 CommandA="wget -nc -c -t 5 --progress=bar --random-wait --proxy=on -r"
      63 # ====================================================================
      64
      65
      66
      67 # --------------------------------------------------------------------
      68 # Set some other variables and explain them.
      69
      70 pattern=" -A .jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.htm,.html,.shtml,.php"
      71                     # wget's option to only get certain types of file.
      72                     # comment out if not using
      73 today=`date +%F`    # Used for a filename.
      74 home=$HOME          # Set HOME to an internal variable.
      75                     # In case some other path is used, change it here.
      76 depthDefault=3      # Set a sensible default recursion.
      77 Depth=$depthDefault # Otherwise user feedback doesn't tie in properly.
      78 RefA=""             # Set blank referring page.
      79 Flag=""             #  Default to not saving anything,
      80                     #+ or whatever else might be wanted in future.
      81 lister=""           # Used for passing a list of urls directly to wget.
      82 Woptions=""         # Used for passing wget some options for itself.
      83 inFile=""           # Used for the run function.
      84 newFile=""          # Used for the run function.
      85 savePath="$home/w-save"
      86 Config="$home/.wgetter2rc"
      87                     #  This is where some variables can be stored,
      88                     #+ if permanently changed from within the script.
      89 Cookie_List="$home/.cookielist"
      90                     # So we know where the cookies are kept . . .
      91 cFlag=""            # Part of the cookie file selection routine.
      92
      93 # Define the options available. Easy to change letters here if needed.
      94 # These are the optional options; you don't just wait to be asked.
      95
      96 save=s   # Save command instead of executing it.
      97 cook=c   # Change cookie file for this session.
      98 help=h   # Usage guide.
      99 list=l   # Pass wget the -i option and URL list.
     100 runn=r   # Run saved commands as an argument to the option.
     101 inpu=i   # Run saved commands interactively.
     102 wopt=w   # Allow to enter options to pass directly to wget.
     103 # --------------------------------------------------------------------
     104
     105
     106 if [ -z "$1" ]; then   # Make sure we get something for wget to eat.
     107    echo "You must at least enter a URL or option!"
     108    echo "-$help for usage."
     109    exit $E_NO_OPTS
     110 fi
     111
     112
     113
     114 # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     115 # added added added added added added added added added added added added
     116
     117 if [ ! -e "$Config" ]; then   # See if configuration file exists.
     118    echo "Creating configuration file, $Config"
     119    echo "# This is the configuration file for wgetter2" > "$Config"
     120    echo "# Your customised settings will be saved in this file" >> "$Config"
     121 else
     122    source $Config             # Import variables we set outside the script.
     123 fi
     124
     125 if [ ! -e "$Cookie_List" ]; then
     126    # Set up a list of cookie files, if there isn't one.
     127    echo "Hunting for cookies . . ."
     128    find -name cookies.txt >> $Cookie_List # Create the list of cookie files.
     129 fi #  Isolate this in its own 'if' statement,
     130    #+ in case we got interrupted while searching.
     131
     132 if [ -z "$cFlag" ]; then # If we haven't already done this . . .
     133    echo                  # Make a nice space after the command prompt.
     134    echo "Looks like you haven't set up your source of cookies yet."
     135    n=0                   #  Make sure the counter
     136                          #+ doesn't contain random values.
     137    while read; do
     138       Cookies[$n]=$REPLY # Put the cookie files we found into an array.
     139       echo "$n) ${Cookies[$n]}"  # Create a menu.
     140       n=$(( n + 1 ))     # Increment the counter.
     141    done < $Cookie_List   # Feed the read statement.
     142    echo "Enter the number of the cookie file you want to use."
     143    echo "If you won't be using cookies, just press RETURN."
     144    echo
     145    echo "I won't be asking this again. Edit $Config"
     146    echo "If you decide to change at a later date"
     147    echo "or use the -${cook} option for per session changes."
     148    read
     149    if [ ! -z $REPLY ]; then   # User didn't just press return.
     150       Cookie=" --load-cookies ${Cookies[$REPLY]}"
     151       # Set the variable here as well as in the config file.
     152
     153       echo "Cookie=\" --load-cookies ${Cookies[$REPLY]}\"" >> $Config
     154    fi
     155    echo "cFlag=1" >> $Config  # So we know not to ask again.
     156 fi
     157
     158 # end added section end added section end added section end added section
     159 # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     160
     161
     162
     163 # Another variable.
     164 # This one may or may not be subject to variation.
     165 # A bit like the small print.
     166 CookiesON=$Cookie
     167 # echo "cookie file is $CookiesON" # For debugging.
     168 # echo "home is ${home}"           # For debugging.
     169                                    # Got caught with this one!
     170
     171
     172 wopts()
     173 {
     174 echo "Enter options to pass to wget."
     175 echo "It is assumed you know what you're doing."
     176 echo
     177 echo "You can pass their arguments here too."
     178 # That is to say, everything passed here is passed to wget.
     179
     180 read Wopts
     181 # Read in the options to be passed to wget.
     182
     183 Woptions=" $Wopts"
     184 #         ^  Why the leading space?
     185 # Assign to another variable.
     186 # Just for fun, or something . . .
     187
     188 echo "passing options ${Wopts} to wget"
     189 # Mainly for debugging.
     190 # Is cute.
     191
     192 return
     193 }
     194
     195
     196 save_func()
     197 {
     198 echo "Settings will be saved."
     199 if [ ! -d $savePath ]; then  #  See if directory exists.
     200    mkdir $savePath           #  Create the directory to save things in
     201                              #+ if it isn't already there.
     202 fi
     203
     204 Flag=S
     205 # Tell the final bit of code what to do.
     206 # Set a flag since stuff is done in main.
     207
     208 return
     209 }
     210
     211
     212 usage() # Tell them how it works.
     213 {
     214     echo "Welcome to wgetter.  This is a front end to wget."
     215     echo "It will always run wget with these options:"
     216     echo "$CommandA"
     217     echo "and the pattern to match: $pattern \
     218 (which you can change at the top of this script)."
     219     echo "It will also ask you for recursion depth, \
     220 and if you want to use a referring page."
     221     echo "Wgetter accepts the following options:"
     222     echo ""
     223     echo "-$help : Display this help."
     224     echo "-$save : Save the command to a file $savePath/wget-($today) \
     225 instead of running it."
     226     echo "-$runn : Run saved wget commands instead of starting a new one -"
     227     echo "Enter filename as argument to this option."
     228     echo "-$inpu : Run saved wget commands interactively --"
     229     echo "The script will ask you for the filename."
     230     echo "-$cook : Change the cookies file for this session."
     231     echo "-$list : Tell wget to use URL's from a list instead of \
     232 from the command-line."
     233     echo "-$wopt : Pass any other options direct to wget."
     234     echo ""
     235     echo "See the wget man page for additional options \
     236 you can pass to wget."
     237     echo ""
     238
     239     exit $E_USAGE  # End here. Don't process anything else.
     240 }
     241
     242
     243
     244 list_func() #  Gives the user the option to use the -i option to wget,
     245             #+ and a list of URLs.
     246 {
     247 while [ 1 ]; do
     248    echo "Enter the name of the file containing URL's (press q to change
     249 your mind)."
     250    read urlfile
     251    if [ ! -e "$urlfile" ] && [ "$urlfile" != q ]; then
     252        # Look for a file, or the quit option.
     253        echo "That file does not exist!"
     254    elif [ "$urlfile" = q ]; then # Check quit option.
     255        echo "Not using a url list."
     256        return
     257    else
     258       echo "using $urlfile."
     259       echo "If you gave url's on the command-line, I'll use those first."
     260                             # Report wget standard behaviour to the user.
     261       lister=" -i $urlfile" # This is what we want to pass to wget.
     262       return
     263    fi
     264 done
     265 }
     266
     267
     268 cookie_func() # Give the user the option to use a different cookie file.
     269 {
     270 while [ 1 ]; do
     271    echo "Change the cookies file. Press return if you don't want to change
     272 it."
     273    read Cookies
     274    # NB: this is not the same as Cookie, earlier.
     275    # There is an 's' on the end.
     276    # Bit like chocolate chips.
     277    if [ -z "$Cookies" ]; then                 # Escape clause for wusses.
     278       return
     279    elif [ ! -e "$Cookies" ]; then
     280       echo "File does not exist.  Try again." # Keep em going . . .
     281    else
     282        CookiesON=" --load-cookies $Cookies"   # File is good -- use it!
     283        return
     284    fi
     285 done
     286 }
     287
     288
     289
     290 run_func()
     291 {
     292 if [ -z "$OPTARG" ]; then
     293 # Test to see if we used the in-line option or the query one.
     294    if [ ! -d "$savePath" ]; then      # If directory doesn't exist . . .
     295       echo "$savePath does not appear to exist."
     296       echo "Please supply path and filename of saved wget commands:"
     297       read newFile
     298          until [ -f "$newFile" ]; do  # Keep going till we get something.
     299             echo "Sorry, that file does not exist.  Please try again."
     300             # Try really hard to get something.
     301             read newFile
     302          done
     303
     304
     305 # -----------------------------------------------------------------------
     306 #       if [ -z ( grep wget ${newfile} ) ]; then
     307         # Assume they haven't got the right file and bail out.
     308 #       echo "Sorry, that file does not contain wget commands.  Aborting."
     309 #       exit
     310 #       fi
     311 #
     312 # This is bogus code.
     313 # It doesn't actually work.
     314 # If anyone wants to fix it, feel free!
     315 # -----------------------------------------------------------------------
     316
     317
     318       filePath="${newFile}"
     319    else
     320    echo "Save path is $savePath"
     321      echo "Please enter name of the file which you want to use."
     322      echo "You have a choice of:"
     323      ls $savePath                                    # Give them a choice.
     324      read inFile
     325        until [ -f "$savePath/$inFile" ]; do         #  Keep going till
     326                                                     #+ we get something.
     327           if [ ! -f "${savePath}/${inFile}" ]; then # If file doesn't exist.
     328              echo "Sorry, that file does not exist.  Please choose from:"
     329              ls $savePath                           # If a mistake is made.
     330              read inFile
     331           fi
     332          done
     333       filePath="${savePath}/${inFile}"  # Make one variable . . .
     334    fi
     335 else filePath="${savePath}/${OPTARG}"   # Which can be many things . . .
     336 fi
     337
     338 if [ ! -f "$filePath" ]; then           # If a bogus file got through.
     339    echo "You did not specify a suitable file."
     340    echo "Run this script with the -${save} option first."
     341    echo "Aborting."
     342    exit $E_NO_SAVEFILE
     343 fi
     344 echo "Using: $filePath"
     345 while read; do
     346     eval $REPLY
     347     echo "Completed: $REPLY"
     348 done < $filePath  # Feed the actual file we are using into a 'while' loop.
     349
     350 exit
     351 }
     352
     353
     354
     355 # Fish out any options we are using for the script.
     356 # This is based on the demo in "Learning The Bash Shell" (O'Reilly).
     357 while getopts ":$save$cook$help$list$runn:$inpu$wopt" opt
     358 do
     359   case $opt in
     360      $save) save_func;;   #  Save some wgetter sessions for later.
     361      $cook) cookie_func;; #  Change cookie file.
     362      $help) usage;;       #  Get help.
     363      $list) list_func;;   #  Allow wget to use a list of URLs.
     364      $runn) run_func;;    #  Useful if you are calling wgetter from,
     365                           #+ for example, a cron script.
     366      $inpu) run_func;;    #  When you don't know what your files are named.
     367      $wopt) wopts;;       #  Pass options directly to wget.
     368         \?) echo "Not a valid option."
     369             echo "Use -${wopt} to pass options directly to wget,"
     370             echo "or -${help} for help";;      # Catch anything else.
     371   esac
     372 done
     373 shift $((OPTIND - 1))     # Do funky magic stuff with $#.
     374
     375
     376 if [ -z "$1" ] && [ -z "$lister" ]; then
     377                           #  We should be left with at least one URL
     378                           #+ on the command-line, unless a list is
     379 			  #+ being used -- catch empty CL's.
     380    echo "No URL's given! You must enter them on the same line as wgetter2."
     381    echo "E.g.,  wgetter2 http://somesite http://anothersite."
     382    echo "Use $help option for more information."
     383    exit $E_NO_URLS        # Bail out, with appropriate error code.
     384 fi
     385
     386 URLS=" $@"
     387 # Use this so that URL list can be changed if we stay in the option loop.
     388
     389 while [ 1 ]; do
     390    # This is where we ask for the most used options.
     391    # (Mostly unchanged from version 1 of wgetter)
     392    if [ -z $curDepth ]; then
     393       Current=""
     394    else Current=" Current value is $curDepth"
     395    fi
     396        echo "How deep should I go? \
     397 (integer: Default is $depthDefault.$Current)"
     398        read Depth   # Recursion -- how far should we go?
     399        inputB=""    # Reset this to blank on each pass of the loop.
     400        echo "Enter the name of the referring page (default is none)."
     401        read inputB  # Need this for some sites.
     402
     403        echo "Do you want to have the output logged to the terminal"
     404        echo "(y/n, default is yes)?"
     405        read noHide  # Otherwise wget will just log it to a file.
     406
     407        case $noHide in    # Now you see me, now you don't.
     408           y|Y ) hide="";;
     409           n|N ) hide=" -b";;
     410             * ) hide="";;
     411        esac
     412
     413        if [ -z ${Depth} ]; then
     414        #  User accepted either default or current depth,
     415        #+ in which case Depth is now empty.
     416           if [ -z ${curDepth} ]; then
     417           #  See if a depth was set on a previous iteration.
     418              Depth="$depthDefault"
     419              #  Set the default recursion depth if nothing
     420              #+ else to use.
     421           else Depth="$curDepth" #  Otherwise, set the one we used before.
     422           fi
     423        fi
     424    Recurse=" -l $Depth"          # Set how deep we want to go.
     425    curDepth=$Depth               # Remember setting for next time.
     426
     427        if [ ! -z $inputB ]; then
     428           RefA=" --referer=$inputB"   # Option to use referring page.
     429        fi
     430
     431    WGETTER="${CommandA}${pattern}${hide}${RefA}${Recurse}\
     432 ${CookiesON}${lister}${Woptions}${URLS}"
     433    #  Just string the whole lot together . . .
     434    #  NB: no embedded spaces.
     435    #  They are in the individual elements so that if any are empty,
     436    #+ we don't get an extra space.
     437
     438    if [ -z "${CookiesON}" ] && [ "$cFlag" = "1" ] ; then
     439        echo "Warning -- can't find cookie file"
     440        #  This should be changed,
     441        #+ in case the user has opted to not use cookies.
     442    fi
     443
     444    if [ "$Flag" = "S" ]; then
     445       echo "$WGETTER" >> $savePath/wget-${today}
     446       #  Create a unique filename for today, or append to it if it exists.
     447       echo "$inputB" >> $savePath/site-list-${today}
     448       #  Make a list, so it's easy to refer back to,
     449       #+ since the whole command is a bit confusing to look at.
     450       echo "Command saved to the file $savePath/wget-${today}"
     451            # Tell the user.
     452       echo "Referring page URL saved to the file$ \
     453 savePath/site-list-${today}"
     454            # Tell the user.
     455       Saver=" with save option"
     456       # Stick this somewhere, so it appears in the loop if set.
     457    else
     458        echo "*****************"
     459        echo "*****Getting*****"
     460        echo "*****************"
     461        echo ""
     462        echo "$WGETTER"
     463        echo ""
     464        echo "*****************"
     465        eval "$WGETTER"
     466    fi
     467
     468        echo ""
     469        echo "Starting over$Saver."
     470        echo "If you want to stop, press q."
     471        echo "Otherwise, enter some URL's:"
     472        # Let them go again. Tell about save option being set.
     473
     474        read
     475        case $REPLY in
     476        # Need to change this to a 'trap' clause.
     477           q|Q ) exit $E_USER_EXIT;;  # Exercise for the reader?
     478             * ) URLS=" $REPLY";;
     479        esac
     480
     481        echo ""
     482 done
     483
     484
     485 exit 0

Example A-31. A podcasting script

       1 #!/bin/bash
       2
       3 #  bashpodder.sh:
       4 #  By Linc 10/1/2004
       5 #  Find the latest script at
       6 #+ http://linc.homeunix.org:8080/scripts/bashpodder
       7 #  Last revision 12/14/2004 - Many Contributors!
       8 #  If you use this and have made improvements or have comments
       9 #+ drop me an email at linc dot fessenden at gmail dot com
      10 #  I'd appreciate it!
      11
      12 # ==>  ABS Guide extra comments.
      13
      14 # ==>  Author of this script has kindly granted permission
      15 # ==>+ for inclusion in ABS Guide.
      16
      17
      18 # ==> ################################################################
      19 #
      20 # ==> What is "podcasting"?
      21
      22 # ==> It's broadcasting "radio shows" over the Internet.
      23 # ==> These shows can be played on iPods and other music file players.
      24
      25 # ==> This script makes it possible.
      26 # ==> See documentation at the script author's site, above.
      27
      28 # ==> ################################################################
      29
      30
      31 # Make script crontab friendly:
      32 cd $(dirname $0)
      33 # ==> Change to directory where this script lives.
      34
      35 # datadir is the directory you want podcasts saved to:
      36 datadir=$(date +%Y-%m-%d)
      37 # ==> Will create a date-labeled directory, named: YYYY-MM-DD
      38
      39 # Check for and create datadir if necessary:
      40 if test ! -d $datadir
      41         then
      42         mkdir $datadir
      43 fi
      44
      45 # Delete any temp file:
      46 rm -f temp.log
      47
      48 #  Read the bp.conf file and wget any url not already
      49 #+ in the podcast.log file:
      50 while read podcast
      51   do # ==> Main action follows.
      52   file=$(wget -q $podcast -O - | tr '\r' '\n' | tr \' \" | \
      53 sed -n 's/.*url="\([^"]*\)".*/\1/p')
      54   for url in $file
      55                 do
      56                 echo $url >> temp.log
      57                 if ! grep "$url" podcast.log > /dev/null
      58                         then
      59                         wget -q -P $datadir "$url"
      60                 fi
      61                 done
      62     done < bp.conf
      63
      64 # Move dynamically created log file to permanent log file:
      65 cat podcast.log >> temp.log
      66 sort temp.log | uniq > podcast.log
      67 rm temp.log
      68 # Create an m3u playlist:
      69 ls $datadir | grep -v m3u > $datadir/podcast.m3u
      70
      71
      72 exit 0
      73
      74 #################################################
      75 For a different scripting approach to Podcasting,
      76 see Phil Salkie's article,
      77 "Internet Radio to Podcast with Shell Tools"
      78 in the September, 2005 issue of LINUX JOURNAL,
      79 http://www.linuxjournal.com/article/8171
      80 #################################################

Example A-32. Nightly backup to a firewire HD

       1 #!/bin/bash
       2 # nightly-backup.sh
       3 # http://www.richardneill.org/source.php#nightly-backup-rsync
       4 # Copyright (c) 2005 Richard Neill <backup@richardneill.org>.
       5 # This is Free Software licensed under the GNU GPL.
       6 # ==> Included in ABS Guide with script author's kind permission.
       7 # ==> (Thanks!)
       8
       9 #  This does a backup from the host computer to a locally connected
      10 #+ firewire HDD using rsync and ssh.
      11 #  (Script should work with USB-connected device (see lines 40-43).
      12 #  It then rotates the backups.
      13 #  Run it via cron every night at 5am.
      14 #  This only backs up the home directory.
      15 #  If ownerships (other than the user's) should be preserved,
      16 #+ then run the rsync process as root (and re-instate the -o).
      17 #  We save every day for 7 days, then every week for 4 weeks,
      18 #+ then every month for 3 months.
      19
      20 #  See: http://www.mikerubel.org/computers/rsync_snapshots/
      21 #+ for more explanation of the theory.
      22 #  Save as: $HOME/bin/nightly-backup_firewire-hdd.sh
      23
      24 #  Known bugs:
      25 #  ----------
      26 #  i)  Ideally, we want to exclude ~/.tmp and the browser caches.
      27
      28 #  ii) If the user is sitting at the computer at 5am,
      29 #+     and files are modified while the rsync is occurring,
      30 #+     then the BACKUP_JUSTINCASE branch gets triggered.
      31 #      To some extent, this is a
      32 #+     feature, but it also causes a "disk-space leak".
      33
      34
      35
      36
      37
      38 ##### BEGIN CONFIGURATION SECTION ############################################
      39 LOCAL_USER=rjn                # User whose home directory should be backed up.
      40 MOUNT_POINT=/backup           # Mountpoint of backup drive.
      41                               # NO trailing slash!
      42                               # This must be unique (eg using a udev symlink)
      43 # MOUNT_POINT=/media/disk     # For USB-connected device.
      44 SOURCE_DIR=/home/$LOCAL_USER  # NO trailing slash - it DOES matter to rsync.
      45 BACKUP_DEST_DIR=$MOUNT_POINT/backup/`hostname -s`.${LOCAL_USER}.nightly_backup
      46 DRY_RUN=false                 #If true, invoke rsync with -n, to do a dry run.
      47                               # Comment out or set to false for normal use.
      48 VERBOSE=false                 # If true, make rsync verbose.
      49                               # Comment out or set to false otherwise.
      50 COMPRESS=false                # If true, compress.
      51                               # Good for internet, bad on LAN.
      52                               # Comment out or set to false otherwise.
      53
      54 ### Exit Codes ###
      55 E_VARS_NOT_SET=64
      56 E_COMMANDLINE=65
      57 E_MOUNT_FAIL=70
      58 E_NOSOURCEDIR=71
      59 E_UNMOUNTED=72
      60 E_BACKUP=73
      61 ##### END CONFIGURATION SECTION ##############################################
      62
      63
      64 # Check that all the important variables have been set:
      65 if [ -z "$LOCAL_USER" ] ||
      66    [ -z "$SOURCE_DIR" ] ||
      67    [ -z "$MOUNT_POINT" ]  ||
      68    [ -z "$BACKUP_DEST_DIR" ]
      69 then
      70    echo 'One of the variables is not set! Edit the file: $0. BACKUP FAILED.'
      71    exit $E_VARS_NOT_SET
      72 fi
      73
      74 if [ "$#" != 0 ]  # If command-line param(s) . . .
      75 then              # Here document(ation).
      76   cat <<-ENDOFTEXT
      77     Automatic Nightly backup run from cron.
      78     Read the source for more details: $0
      79     The backup directory is $BACKUP_DEST_DIR .
      80     It will be created if necessary; initialisation is no longer required.
      81
      82     WARNING: Contents of $BACKUP_DEST_DIR are rotated.
      83     Directories named 'backup.\$i' will eventually be DELETED.
      84     We keep backups from every day for 7 days (1-8),
      85     then every week for 4 weeks (9-12),
      86     then every month for 3 months (13-15).
      87
      88     You may wish to add this to your crontab using 'crontab -e'
      89     #  Back up files: $SOURCE_DIR to $BACKUP_DEST_DIR
      90     #+ every night at 3:15 am
      91          15 03 * * * /home/$LOCAL_USER/bin/nightly-backup_firewire-hdd.sh
      92
      93     Don't forget to verify the backups are working,
      94     especially if you don't read cron's mail!"
      95 	ENDOFTEXT
      96    exit $E_COMMANDLINE
      97 fi
      98
      99
     100 # Parse the options.
     101 # ==================
     102
     103 if [ "$DRY_RUN" == "true" ]; then
     104   DRY_RUN="-n"
     105   echo "WARNING:"
     106   echo "THIS IS A 'DRY RUN'!"
     107   echo "No data will actually be transferred!"
     108 else
     109   DRY_RUN=""
     110 fi
     111
     112 if [ "$VERBOSE" == "true" ]; then
     113   VERBOSE="-v"
     114 else
     115   VERBOSE=""
     116 fi
     117
     118 if [ "$COMPRESS" == "true" ]; then
     119   COMPRESS="-z"
     120 else
     121   COMPRESS=""
     122 fi
     123
     124
     125 #  Every week (actually of 8 days) and every month,
     126 #+ extra backups are preserved.
     127 DAY_OF_MONTH=`date +%d`            # Day of month (01..31).
     128 if [ $DAY_OF_MONTH = 01 ]; then    # First of month.
     129   MONTHSTART=true
     130 elif [ $DAY_OF_MONTH = 08 \
     131     -o $DAY_OF_MONTH = 16 \
     132     -o $DAY_OF_MONTH = 24 ]; then
     133     # Day 8,16,24  (use 8, not 7 to better handle 31-day months)
     134       WEEKSTART=true
     135 fi
     136
     137
     138
     139 #  Check that the HDD is mounted.
     140 #  At least, check that *something* is mounted here!
     141 #  We can use something unique to the device, rather than just guessing
     142 #+ the scsi-id by having an appropriate udev rule in
     143 #+ /etc/udev/rules.d/10-rules.local
     144 #+ and by putting a relevant entry in /etc/fstab.
     145 #  Eg: this udev rule:
     146 # BUS="scsi", KERNEL="sd*", SYSFS{vendor}="WDC WD16",
     147 # SYSFS{model}="00JB-00GVA0     ", NAME="%k", SYMLINK="lacie_1394d%n"
     148
     149 if mount | grep $MOUNT_POINT >/dev/null; then
     150   echo "Mount point $MOUNT_POINT is indeed mounted. OK"
     151 else
     152   echo -n "Attempting to mount $MOUNT_POINT..."
     153            # If it isn't mounted, try to mount it.
     154   sudo mount $MOUNT_POINT 2>/dev/null
     155
     156   if mount | grep $MOUNT_POINT >/dev/null; then
     157     UNMOUNT_LATER=TRUE
     158     echo "OK"
     159     #  Note: Ensure that this is also unmounted
     160     #+ if we exit prematurely with failure.
     161   else
     162     echo "FAILED"
     163     echo -e "Nothing is mounted at $MOUNT_POINT. BACKUP FAILED!"
     164     exit $E_MOUNT_FAIL
     165   fi
     166 fi
     167
     168
     169 # Check that source dir exists and is readable.
     170 if [ ! -r  $SOURCE_DIR ] ; then
     171   echo "$SOURCE_DIR does not exist, or cannot be read. BACKUP FAILED."
     172   exit $E_NOSOURCEDIR
     173 fi
     174
     175
     176 # Check that the backup directory structure is as it should be.
     177 # If not, create it.
     178 # Create the subdirectories.
     179 # Note that backup.0 will be created as needed by rsync.
     180
     181 for ((i=1;i<=15;i++)); do
     182   if [ ! -d $BACKUP_DEST_DIR/backup.$i ]; then
     183     if /bin/mkdir -p $BACKUP_DEST_DIR/backup.$i ; then
     184     #  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  No [ ] test brackets. Why?
     185       echo "Warning: directory $BACKUP_DEST_DIR/backup.$i is missing,"
     186       echo "or was not initialised. (Re-)creating it."
     187     else
     188       echo "ERROR: directory $BACKUP_DEST_DIR/backup.$i"
     189       echo "is missing and could not be created."
     190     if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
     191         # Before we exit, unmount the mount point if necessary.
     192         cd
     193 	sudo umount $MOUNT_POINT &&
     194 	echo "Unmounted $MOUNT_POINT again. Giving up."
     195     fi
     196       exit $E_UNMOUNTED
     197   fi
     198 fi
     199 done
     200
     201
     202 #  Set the permission to 700 for security
     203 #+ on an otherwise permissive multi-user system.
     204 if ! /bin/chmod 700 $BACKUP_DEST_DIR ; then
     205   echo "ERROR: Could not set permissions on $BACKUP_DEST_DIR to 700."
     206
     207   if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
     208   # Before we exit, unmount the mount point if necessary.
     209      cd ; sudo umount $MOUNT_POINT \
     210      && echo "Unmounted $MOUNT_POINT again. Giving up."
     211   fi
     212
     213   exit $E_UNMOUNTED
     214 fi
     215
     216 # Create the symlink: current -> backup.1 if required.
     217 # A failure here is not critical.
     218 cd $BACKUP_DEST_DIR
     219 if [ ! -h current ] ; then
     220   if ! /bin/ln -s backup.1 current ; then
     221     echo "WARNING: could not create symlink current -> backup.1"
     222   fi
     223 fi
     224
     225
     226 # Now, do the rsync.
     227 echo "Now doing backup with rsync..."
     228 echo "Source dir: $SOURCE_DIR"
     229 echo -e "Backup destination dir: $BACKUP_DEST_DIR\n"
     230
     231
     232 /usr/bin/rsync $DRY_RUN $VERBOSE -a -S --delete --modify-window=60 \
     233 --link-dest=../backup.1 $SOURCE_DIR $BACKUP_DEST_DIR/backup.0/
     234
     235 #  Only warn, rather than exit if the rsync failed,
     236 #+ since it may only be a minor problem.
     237 #  E.g., if one file is not readable, rsync will fail.
     238 #  This shouldn't prevent the rotation.
     239 #  Not using, e.g., `date +%a`  since these directories
     240 #+ are just full of links and don't consume *that much* space.
     241
     242 if [ $? != 0 ]; then
     243   BACKUP_JUSTINCASE=backup.`date +%F_%T`.justincase
     244   echo "WARNING: the rsync process did not entirely succeed."
     245   echo "Something might be wrong."
     246   echo "Saving an extra copy at: $BACKUP_JUSTINCASE"
     247   echo "WARNING: if this occurs regularly, a LOT of space will be consumed,"
     248   echo "even though these are just hard-links!"
     249 fi
     250
     251 # Save a readme in the backup parent directory.
     252 # Save another one in the recent subdirectory.
     253 echo "Backup of $SOURCE_DIR on `hostname` was last run on \
     254 `date`" > $BACKUP_DEST_DIR/README.txt
     255 echo "This backup of $SOURCE_DIR on `hostname` was created on \
     256 `date`" > $BACKUP_DEST_DIR/backup.0/README.txt
     257
     258 # If we are not in a dry run, rotate the backups.
     259 [ -z "$DRY_RUN" ] &&
     260
     261   #  Check how full the backup disk is.
     262   #  Warn if 90%. if 98% or more, we'll probably fail, so give up.
     263   #  (Note: df can output to more than one line.)
     264   #  We test this here, rather than before
     265   #+ so that rsync may possibly have a chance.
     266   DISK_FULL_PERCENT=`/bin/df $BACKUP_DEST_DIR |
     267   tr "\n" ' ' | awk '{print $12}' | grep -oE [0-9]+ `
     268   echo "Disk space check on backup partition \
     269   $MOUNT_POINT $DISK_FULL_PERCENT% full."
     270   if [ $DISK_FULL_PERCENT -gt 90 ]; then
     271     echo "Warning: Disk is greater than 90% full."
     272   fi
     273   if [ $DISK_FULL_PERCENT -gt 98 ]; then
     274     echo "Error: Disk is full! Giving up."
     275       if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
     276         # Before we exit, unmount the mount point if necessary.
     277         cd; sudo umount $MOUNT_POINT &&
     278         echo "Unmounted $MOUNT_POINT again. Giving up."
     279       fi
     280     exit $E_UNMOUNTED
     281   fi
     282
     283
     284  # Create an extra backup.
     285  # If this copy fails, give up.
     286  if [ -n "$BACKUP_JUSTINCASE" ]; then
     287    if ! /bin/cp -al $BACKUP_DEST_DIR/backup.0 \
     288       $BACKUP_DEST_DIR/$BACKUP_JUSTINCASE
     289    then
     290      echo "ERROR: Failed to create extra copy \
     291      $BACKUP_DEST_DIR/$BACKUP_JUSTINCASE"
     292      if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
     293        # Before we exit, unmount the mount point if necessary.
     294        cd ;sudo umount $MOUNT_POINT &&
     295        echo "Unmounted $MOUNT_POINT again. Giving up."
     296      fi
     297      exit $E_UNMOUNTED
     298    fi
     299  fi
     300
     301
     302  # At start of month, rotate the oldest 8.
     303  if [ "$MONTHSTART" == "true" ]; then
     304    echo -e "\nStart of month. \
     305    Removing oldest backup: $BACKUP_DEST_DIR/backup.15"  &&
     306    /bin/rm -rf  $BACKUP_DEST_DIR/backup.15  &&
     307    echo "Rotating monthly,weekly backups: \
     308    $BACKUP_DEST_DIR/backup.[8-14] -> $BACKUP_DEST_DIR/backup.[9-15]"  &&
     309      /bin/mv $BACKUP_DEST_DIR/backup.14 $BACKUP_DEST_DIR/backup.15  &&
     310      /bin/mv $BACKUP_DEST_DIR/backup.13 $BACKUP_DEST_DIR/backup.14  &&
     311      /bin/mv $BACKUP_DEST_DIR/backup.12 $BACKUP_DEST_DIR/backup.13  &&
     312      /bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12  &&
     313      /bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11  &&
     314      /bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10  &&
     315      /bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9
     316
     317  # At start of week, rotate the second-oldest 4.
     318  elif [ "$WEEKSTART" == "true" ]; then
     319    echo -e "\nStart of week. \
     320    Removing oldest weekly backup: $BACKUP_DEST_DIR/backup.12"  &&
     321    /bin/rm -rf  $BACKUP_DEST_DIR/backup.12  &&
     322
     323    echo "Rotating weekly backups: \
     324    $BACKUP_DEST_DIR/backup.[8-11] -> $BACKUP_DEST_DIR/backup.[9-12]"  &&
     325      /bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12  &&
     326      /bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11  &&
     327      /bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10  &&
     328      /bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9
     329
     330  else
     331    echo -e "\nRemoving oldest daily backup: $BACKUP_DEST_DIR/backup.8"  &&
     332      /bin/rm -rf  $BACKUP_DEST_DIR/backup.8
     333
     334  fi  &&
     335
     336  # Every day, rotate the newest 8.
     337  echo "Rotating daily backups: \
     338  $BACKUP_DEST_DIR/backup.[1-7] -> $BACKUP_DEST_DIR/backup.[2-8]"  &&
     339      /bin/mv $BACKUP_DEST_DIR/backup.7 $BACKUP_DEST_DIR/backup.8  &&
     340      /bin/mv $BACKUP_DEST_DIR/backup.6 $BACKUP_DEST_DIR/backup.7  &&
     341      /bin/mv $BACKUP_DEST_DIR/backup.5 $BACKUP_DEST_DIR/backup.6  &&
     342      /bin/mv $BACKUP_DEST_DIR/backup.4 $BACKUP_DEST_DIR/backup.5  &&
     343      /bin/mv $BACKUP_DEST_DIR/backup.3 $BACKUP_DEST_DIR/backup.4  &&
     344      /bin/mv $BACKUP_DEST_DIR/backup.2 $BACKUP_DEST_DIR/backup.3  &&
     345      /bin/mv $BACKUP_DEST_DIR/backup.1 $BACKUP_DEST_DIR/backup.2  &&
     346      /bin/mv $BACKUP_DEST_DIR/backup.0 $BACKUP_DEST_DIR/backup.1  &&
     347
     348  SUCCESS=true
     349
     350
     351 if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
     352   # Unmount the mount point if it wasn't mounted to begin with.
     353   cd ; sudo umount $MOUNT_POINT && echo "Unmounted $MOUNT_POINT again."
     354 fi
     355
     356
     357 if [ "$SUCCESS" == "true" ]; then
     358   echo 'SUCCESS!'
     359   exit 0
     360 fi
     361
     362 # Should have already exited if backup worked.
     363 echo 'BACKUP FAILED! Is this just a dry run? Is the disk full?) '
     364 exit $E_BACKUP

Example A-33. An expanded cd command

       1 ###########################################################################
       2 #
       3 #       cdll
       4 #       by Phil Braham
       5 #
       6 #       ############################################
       7 #       Latest version of this script available from
       8 #       http://freshmeat.net/projects/cd/
       9 #       ############################################
      10 #
      11 #       .cd_new
      12 #
      13 #       An enhancement of the Unix cd command
      14 #
      15 #       There are unlimited stack entries and special entries. The stack
      16 #       entries keep the last cd_maxhistory
      17 #       directories that have been used. The special entries can be
      18 #       assigned to commonly used directories.
      19 #
      20 #       The special entries may be pre-assigned by setting the environment
      21 #       variables CDSn or by using the -u or -U command.
      22 #
      23 #       The following is a suggestion for the .profile file:
      24 #
      25 #               . cdll              #  Set up the cd command
      26 #       alias cd='cd_new'           #  Replace the cd command
      27 #               cd -U               #  Upload pre-assigned entries for
      28 #                                   #+ the stack and special entries
      29 #               cd -D               #  Set non-default mode
      30 #               alias @="cd_new @"  #  Allow @ to be used to get history
      31 #
      32 #       For help type:
      33 #
      34 #               cd -h or
      35 #               cd -H
      36 #
      37 #
      38 ###########################################################################
      39 #
      40 #       Version 1.2.1
      41 #
      42 #       Written by Phil Braham - Realtime Software Pty Ltd
      43 #       (realtime@mpx.com.au)
      44 #       Please send any suggestions or enhancements to the author (also at
      45 #       phil@braham.net)
      46 #
      47 ############################################################################
      48
      49 cd_hm ()
      50 {
      51         ${PRINTF} "%s" "cd [dir] [0-9] [@[s|h] [-g [<dir>]] [-d] \
      52 [-D] [-r<n>] [dir|0-9] [-R<n>] [<dir>|0-9]
      53    [-s<n>] [-S<n>] [-u] [-U] [-f] [-F] [-h] [-H] [-v]
      54     <dir> Go to directory
      55     0-n         Go to previous directory (0 is previous, 1 is last but 1 etc)
      56                 n is up to max history (default is 50)
      57     @           List history and special entries
      58     @h          List history entries
      59     @s          List special entries
      60     -g [<dir>]  Go to literal name (bypass special names)
      61                 This is to allow access to dirs called '0','1','-h' etc
      62     -d          Change default action - verbose. (See note)
      63     -D          Change default action - silent. (See note)
      64     -s<n> Go to the special entry <n>*
      65     -S<n> Go to the special entry <n>
      66                 and replace it with the current dir*
      67     -r<n> [<dir>] Go to directory <dir>
      68                               and then put it on special entry <n>*
      69     -R<n> [<dir>] Go to directory <dir>
      70                               and put current dir on special entry <n>*
      71     -a<n>       Alternative suggested directory. See note below.
      72     -f [<file>] File entries to <file>.
      73     -u [<file>] Update entries from <file>.
      74                 If no filename supplied then default file
      75                 (${CDPath}${2:-"$CDFile"}) is used
      76                 -F and -U are silent versions
      77     -v          Print version number
      78     -h          Help
      79     -H          Detailed help
      80
      81     *The special entries (0 - 9) are held until log off, replaced by another
      82      entry or updated with the -u command
      83
      84     Alternative suggested directories:
      85     If a directory is not found then CD will suggest any
      86     possibilities. These are directories starting with the same letters
      87     and if any are found they are listed prefixed with -a<n>
      88     where <n> is a number.
      89     It's possible to go to the directory by entering cd -a<n>
      90     on the command line.
      91
      92     The directory for -r<n> or -R<n> may be a number.
      93     For example:
      94         $ cd -r3 4  Go to history entry 4 and put it on special entry 3
      95         $ cd -R3 4  Put current dir on the special entry 3
      96                     and go to history entry 4
      97         $ cd -s3    Go to special entry 3
      98
      99     Note that commands R,r,S and s may be used without a number
     100     and refer to 0:
     101         $ cd -s     Go to special entry 0
     102         $ cd -S     Go to special entry 0 and make special
     103                     entry 0 current dir
     104         $ cd -r 1   Go to history entry 1 and put it on special entry 0
     105         $ cd -r     Go to history entry 0 and put it on special entry 0
     106     "
     107         if ${TEST} "$CD_MODE" = "PREV"
     108         then
     109                 ${PRINTF} "$cd_mnset"
     110         else
     111                 ${PRINTF} "$cd_mset"
     112         fi
     113 }
     114
     115 cd_Hm ()
     116 {
     117         cd_hm
     118         ${PRINTF} "%s" "
     119         The previous directories (0-$cd_maxhistory) are stored in the
     120         environment variables CD[0] - CD[$cd_maxhistory]
     121         Similarly the special directories S0 - $cd_maxspecial are in
     122         the environment variable CDS[0] - CDS[$cd_maxspecial]
     123         and may be accessed from the command line
     124
     125         The default pathname for the -f and -u commands is $CDPath
     126         The default filename for the -f and -u commands is $CDFile
     127
     128         Set the following environment variables:
     129             CDL_PROMPTLEN  - Set to the length of prompt you require.
     130                 Prompt string is set to the right characters of the
     131                 current directory.
     132                 If not set then prompt is left unchanged
     133             CDL_PROMPT_PRE - Set to the string to prefix the prompt.
     134                 Default is:
     135                     non-root:  \"\\[\\e[01;34m\\]\"  (sets colour to blue).
     136                     root:      \"\\[\\e[01;31m\\]\"  (sets colour to red).
     137             CDL_PROMPT_POST    - Set to the string to suffix the prompt.
     138                 Default is:
     139                     non-root:  \"\\[\\e[00m\\]$\"
     140                                 (resets colour and displays $).
     141                     root:      \"\\[\\e[00m\\]#\"
     142                                 (resets colour and displays #).
     143             CDPath - Set the default path for the -f & -u options.
     144                      Default is home directory
     145             CDFile - Set the default filename for the -f & -u options.
     146                      Default is cdfile
     147
     148 "
     149     cd_version
     150
     151 }
     152
     153 cd_version ()
     154 {
     155  printf "Version: ${VERSION_MAJOR}.${VERSION_MINOR} Date: ${VERSION_DATE}\n"
     156 }
     157
     158 #
     159 # Truncate right.
     160 #
     161 # params:
     162 #   p1 - string
     163 #   p2 - length to truncate to
     164 #
     165 # returns string in tcd
     166 #
     167 cd_right_trunc ()
     168 {
     169     local tlen=${2}
     170     local plen=${#1}
     171     local str="${1}"
     172     local diff
     173     local filler="<--"
     174     if ${TEST} ${plen} -le ${tlen}
     175     then
     176         tcd="${str}"
     177     else
     178         let diff=${plen}-${tlen}
     179         elen=3
     180         if ${TEST} ${diff} -le 2
     181         then
     182             let elen=${diff}
     183         fi
     184         tlen=-${tlen}
     185         let tlen=${tlen}+${elen}
     186         tcd=${filler:0:elen}${str:tlen}
     187     fi
     188 }
     189
     190 #
     191 # Three versions of do history:
     192 #    cd_dohistory  - packs history and specials side by side
     193 #    cd_dohistoryH - Shows only hstory
     194 #    cd_dohistoryS - Shows only specials
     195 #
     196 cd_dohistory ()
     197 {
     198     cd_getrc
     199         ${PRINTF} "History:\n"
     200     local -i count=${cd_histcount}
     201     while ${TEST} ${count} -ge 0
     202     do
     203         cd_right_trunc "${CD[count]}" ${cd_lchar}
     204             ${PRINTF} "%2d %-${cd_lchar}.${cd_lchar}s " ${count} "${tcd}"
     205
     206         cd_right_trunc "${CDS[count]}" ${cd_rchar}
     207             ${PRINTF} "S%d %-${cd_rchar}.${cd_rchar}s\n" ${count} "${tcd}"
     208         count=${count}-1
     209     done
     210 }
     211
     212 cd_dohistoryH ()
     213 {
     214     cd_getrc
     215         ${PRINTF} "History:\n"
     216         local -i count=${cd_maxhistory}
     217         while ${TEST} ${count} -ge 0
     218         do
     219           ${PRINTF} "${count} %-${cd_flchar}.${cd_flchar}s\n" ${CD[$count]}
     220           count=${count}-1
     221         done
     222 }
     223
     224 cd_dohistoryS ()
     225 {
     226     cd_getrc
     227         ${PRINTF} "Specials:\n"
     228         local -i count=${cd_maxspecial}
     229         while ${TEST} ${count} -ge 0
     230         do
     231           ${PRINTF} "S${count} %-${cd_flchar}.${cd_flchar}s\n" ${CDS[$count]}
     232           count=${count}-1
     233         done
     234 }
     235
     236 cd_getrc ()
     237 {
     238     cd_flchar=$(stty -a | awk -F \;
     239     '/rows/ { print $2 $3 }' | awk -F \  '{ print $4 }')
     240     if ${TEST} ${cd_flchar} -ne 0
     241     then
     242         cd_lchar=${cd_flchar}/2-5
     243         cd_rchar=${cd_flchar}/2-5
     244             cd_flchar=${cd_flchar}-5
     245     else
     246             cd_flchar=${FLCHAR:=75}
     247 	    # cd_flchar is used for for the @s & @h history
     248             cd_lchar=${LCHAR:=35}
     249             cd_rchar=${RCHAR:=35}
     250     fi
     251 }
     252
     253 cd_doselection ()
     254 {
     255         local -i nm=0
     256         cd_doflag="TRUE"
     257         if ${TEST} "${CD_MODE}" = "PREV"
     258         then
     259                 if ${TEST} -z "$cd_npwd"
     260                 then
     261                         cd_npwd=0
     262                 fi
     263         fi
     264         tm=$(echo "${cd_npwd}" | cut -b 1)
     265     if ${TEST} "${tm}" = "-"
     266     then
     267         pm=$(echo "${cd_npwd}" | cut -b 2)
     268         nm=$(echo "${cd_npwd}" | cut -d $pm -f2)
     269         case "${pm}" in
     270              a) cd_npwd=${cd_sugg[$nm]} ;;
     271              s) cd_npwd="${CDS[$nm]}" ;;
     272              S) cd_npwd="${CDS[$nm]}" ; CDS[$nm]=`pwd` ;;
     273              r) cd_npwd="$2" ; cd_specDir=$nm ; cd_doselection "$1" "$2";;
     274              R) cd_npwd="$2" ; CDS[$nm]=`pwd` ; cd_doselection "$1" "$2";;
     275         esac
     276     fi
     277
     278     if ${TEST} "${cd_npwd}" != "." -a "${cd_npwd}" \
     279 != ".." -a "${cd_npwd}" -le ${cd_maxhistory} >>/dev/null 2>&1
     280     then
     281       cd_npwd=${CD[$cd_npwd]}
     282      else
     283        case "$cd_npwd" in
     284                 @)  cd_dohistory ; cd_doflag="FALSE" ;;
     285                @h) cd_dohistoryH ; cd_doflag="FALSE" ;;
     286                @s) cd_dohistoryS ; cd_doflag="FALSE" ;;
     287                -h) cd_hm ; cd_doflag="FALSE" ;;
     288                -H) cd_Hm ; cd_doflag="FALSE" ;;
     289                -f) cd_fsave "SHOW" $2 ; cd_doflag="FALSE" ;;
     290                -u) cd_upload "SHOW" $2 ; cd_doflag="FALSE" ;;
     291                -F) cd_fsave "NOSHOW" $2 ; cd_doflag="FALSE" ;;
     292                -U) cd_upload "NOSHOW" $2 ; cd_doflag="FALSE" ;;
     293                -g) cd_npwd="$2" ;;
     294                -d) cd_chdefm 1; cd_doflag="FALSE" ;;
     295                -D) cd_chdefm 0; cd_doflag="FALSE" ;;
     296                -r) cd_npwd="$2" ; cd_specDir=0 ; cd_doselection "$1" "$2";;
     297                -R) cd_npwd="$2" ; CDS[0]=`pwd` ; cd_doselection "$1" "$2";;
     298                -s) cd_npwd="${CDS[0]}" ;;
     299                -S) cd_npwd="${CDS[0]}"  ; CDS[0]=`pwd` ;;
     300                -v) cd_version ; cd_doflag="FALSE";;
     301        esac
     302     fi
     303 }
     304
     305 cd_chdefm ()
     306 {
     307         if ${TEST} "${CD_MODE}" = "PREV"
     308         then
     309                 CD_MODE=""
     310                 if ${TEST} $1 -eq 1
     311                 then
     312                         ${PRINTF} "${cd_mset}"
     313                 fi
     314         else
     315                 CD_MODE="PREV"
     316                 if ${TEST} $1 -eq 1
     317                 then
     318                         ${PRINTF} "${cd_mnset}"
     319                 fi
     320         fi
     321 }
     322
     323 cd_fsave ()
     324 {
     325         local sfile=${CDPath}${2:-"$CDFile"}
     326         if ${TEST} "$1" = "SHOW"
     327         then
     328                 ${PRINTF} "Saved to %s\n" $sfile
     329         fi
     330         ${RM} -f ${sfile}
     331         local -i count=0
     332         while ${TEST} ${count} -le ${cd_maxhistory}
     333         do
     334                 echo "CD[$count]=\"${CD[$count]}\"" >> ${sfile}
     335                 count=${count}+1
     336         done
     337         count=0
     338         while ${TEST} ${count} -le ${cd_maxspecial}
     339         do
     340                 echo "CDS[$count]=\"${CDS[$count]}\"" >> ${sfile}
     341                 count=${count}+1
     342         done
     343 }
     344
     345 cd_upload ()
     346 {
     347         local sfile=${CDPath}${2:-"$CDFile"}
     348         if ${TEST} "${1}" = "SHOW"
     349         then
     350                 ${PRINTF} "Loading from %s\n" ${sfile}
     351         fi
     352         . ${sfile}
     353 }
     354
     355 cd_new ()
     356 {
     357     local -i count
     358     local -i choose=0
     359
     360         cd_npwd="${1}"
     361         cd_specDir=-1
     362         cd_doselection "${1}" "${2}"
     363
     364         if ${TEST} ${cd_doflag} = "TRUE"
     365         then
     366                 if ${TEST} "${CD[0]}" != "`pwd`"
     367                 then
     368                         count=$cd_maxhistory
     369                         while ${TEST} $count -gt 0
     370                         do
     371                                 CD[$count]=${CD[$count-1]}
     372                                 count=${count}-1
     373                         done
     374                         CD[0]=`pwd`
     375                 fi
     376                 command cd "${cd_npwd}" 2>/dev/null
     377         if ${TEST} $? -eq 1
     378         then
     379             ${PRINTF} "Unknown dir: %s\n" "${cd_npwd}"
     380             local -i ftflag=0
     381             for i in "${cd_npwd}"*
     382             do
     383                 if ${TEST} -d "${i}"
     384                 then
     385                     if ${TEST} ${ftflag} -eq 0
     386                     then
     387                         ${PRINTF} "Suggest:\n"
     388                         ftflag=1
     389                 fi
     390                     ${PRINTF} "\t-a${choose} %s\n" "$i"
     391                                         cd_sugg[$choose]="${i}"
     392                     choose=${choose}+1
     393         fi
     394             done
     395         fi
     396         fi
     397
     398         if ${TEST} ${cd_specDir} -ne -1
     399         then
     400                 CDS[${cd_specDir}]=`pwd`
     401         fi
     402
     403         if ${TEST} ! -z "${CDL_PROMPTLEN}"
     404         then
     405         cd_right_trunc "${PWD}" ${CDL_PROMPTLEN}
     406             cd_rp=${CDL_PROMPT_PRE}${tcd}${CDL_PROMPT_POST}
     407                 export PS1="$(echo -ne ${cd_rp})"
     408         fi
     409 }
     410 #########################################################################
     411 #                                                                       #
     412 #                        Initialisation here                            #
     413 #                                                                       #
     414 #########################################################################
     415 #
     416 VERSION_MAJOR="1"
     417 VERSION_MINOR="2.1"
     418 VERSION_DATE="24-MAY-2003"
     419 #
     420 alias cd=cd_new
     421 #
     422 # Set up commands
     423 RM=/bin/rm
     424 TEST=test
     425 PRINTF=printf              # Use builtin printf
     426
     427 #########################################################################
     428 #                                                                       #
     429 # Change this to modify the default pre- and post prompt strings.       #
     430 # These only come into effect if CDL_PROMPTLEN is set.                  #
     431 #                                                                       #
     432 #########################################################################
     433 if ${TEST} ${EUID} -eq 0
     434 then
     435 #   CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="$HOSTNAME@"}
     436     CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="\\[\\e[01;31m\\]"}  # Root is in red
     437     CDL_PROMPT_POST=${CDL_PROMPT_POST:="\\[\\e[00m\\]#"}
     438 else
     439     CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="\\[\\e[01;34m\\]"}  # Users in blue
     440     CDL_PROMPT_POST=${CDL_PROMPT_POST:="\\[\\e[00m\\]$"}
     441 fi
     442 #########################################################################
     443 #
     444 # cd_maxhistory defines the max number of history entries allowed.
     445 typeset -i cd_maxhistory=50
     446
     447 #########################################################################
     448 #
     449 # cd_maxspecial defines the number of special entries.
     450 typeset -i cd_maxspecial=9
     451 #
     452 #
     453 #########################################################################
     454 #
     455 #  cd_histcount defines the number of entries displayed in
     456 #+ the history command.
     457 typeset -i cd_histcount=9
     458 #
     459 #########################################################################
     460 export CDPath=${HOME}/
     461 #  Change these to use a different                                      #
     462 #+ default path and filename                                            #
     463 export CDFile=${CDFILE:=cdfile}           # for the -u and -f commands  #
     464 #
     465 #########################################################################
     466                                                                         #
     467 typeset -i cd_lchar cd_rchar cd_flchar
     468                         #  This is the number of chars to allow for the #
     469 cd_flchar=${FLCHAR:=75} #+ cd_flchar is used for for the @s & @h history#
     470
     471 typeset -ax CD CDS
     472 #
     473 cd_mset="\n\tDefault mode is now set - entering cd with no parameters \
     474 has the default action\n\tUse cd -d or -D for cd to go to \
     475 previous directory with no parameters\n"
     476 cd_mnset="\n\tNon-default mode is now set - entering cd with no \
     477 parameters is the same as entering cd 0\n\tUse cd -d or \
     478 -D to change default cd action\n"
     479
     480 # ==================================================================== #
     481
     482
     483
     484 : <<DOCUMENTATION
     485
     486 Written by Phil Braham. Realtime Software Pty Ltd.
     487 Released under GNU license. Free to use. Please pass any modifications
     488 or comments to the author Phil Braham:
     489
     490 realtime@mpx.com.au
     491 =======================================================================
     492
     493 cdll is a replacement for cd and incorporates similar functionality to
     494 the bash pushd and popd commands but is independent of them.
     495
     496 This version of cdll has been tested on Linux using Bash. It will work
     497 on most Linux versions but will probably not work on other shells without
     498 modification.
     499
     500 Introduction
     501 ============
     502
     503 cdll allows easy moving about between directories. When changing to a new
     504 directory the current one is automatically put onto a stack. By default
     505 50 entries are kept, but this is configurable. Special directories can be
     506 kept for easy access - by default up to 10, but this is configurable. The
     507 most recent stack entries and the special entries can be easily viewed.
     508
     509 The directory stack and special entries can be saved to, and loaded from,
     510 a file. This allows them to be set up on login, saved before logging out
     511 or changed when moving project to project.
     512
     513 In addition, cdll provides a flexible command prompt facility that allows,
     514 for example, a directory name in colour that is truncated from the left
     515 if it gets too long.
     516
     517
     518 Setting up cdll
     519 ===============
     520
     521 Copy cdll to either your local home directory or a central directory
     522 such as /usr/bin (this will require root access).
     523
     524 Copy the file cdfile to your home directory. It will require read and
     525 write access. This a default file that contains a directory stack and
     526 special entries.
     527
     528 To replace the cd command you must add commands to your login script.
     529 The login script is one or more of:
     530
     531     /etc/profile
     532     ~/.bash_profile
     533     ~/.bash_login
     534     ~/.profile
     535     ~/.bashrc
     536     /etc/bash.bashrc.local
     537
     538 To setup your login, ~/.bashrc is recommended, for global (and root) setup
     539 add the commands to /etc/bash.bashrc.local
     540
     541 To set up on login, add the command:
     542     . <dir>/cdll
     543 For example if cdll is in your local home directory:
     544     . ~/cdll
     545 If in /usr/bin then:
     546     . /usr/bin/cdll
     547
     548 If you want to use this instead of the buitin cd command then add:
     549     alias cd='cd_new'
     550 We would also recommend the following commands:
     551     alias @='cd_new @'
     552     cd -U
     553     cd -D
     554
     555 If you want to use cdll's prompt facilty then add the following:
     556     CDL_PROMPTLEN=nn
     557 Where nn is a number described below. Initially 99 would be suitable
     558 number.
     559
     560 Thus the script looks something like this:
     561
     562     ######################################################################
     563     # CD Setup
     564     ######################################################################
     565     CDL_PROMPTLEN=21        # Allow a prompt length of up to 21 characters
     566     . /usr/bin/cdll         # Initialise cdll
     567     alias cd='cd_new'       # Replace the built in cd command
     568     alias @='cd_new @'      # Allow @ at the prompt to display history
     569     cd -U                   # Upload directories
     570     cd -D                   # Set default action to non-posix
     571     ######################################################################
     572
     573 The full meaning of these commands will become clear later.
     574
     575 There are a couple of caveats. If another program changes the directory
     576 without calling cdll, then the directory won't be put on the stack and
     577 also if the prompt facility is used then this will not be updated. Two
     578 programs that can do this are pushd and popd. To update the prompt and
     579 stack simply enter:
     580
     581     cd .
     582
     583 Note that if the previous entry on the stack is the current directory
     584 then the stack is not updated.
     585
     586 Usage
     587 =====
     588 cd [dir] [0-9] [@[s|h] [-g <dir>] [-d] [-D] [-r<n>]
     589    [dir|0-9] [-R<n>] [<dir>|0-9] [-s<n>] [-S<n>]
     590    [-u] [-U] [-f] [-F] [-h] [-H] [-v]
     591
     592     <dir>       Go to directory
     593     0-n         Goto previous directory (0 is previous,
     594                 1 is last but 1, etc.)
     595                 n is up to max history (default is 50)
     596     @           List history and special entries (Usually available as $ @)
     597     @h          List history entries
     598     @s          List special entries
     599     -g [<dir>]  Go to literal name (bypass special names)
     600                 This is to allow access to dirs called '0','1','-h' etc
     601     -d          Change default action - verbose. (See note)
     602     -D          Change default action - silent. (See note)
     603     -s<n>       Go to the special entry <n>
     604     -S<n>       Go to the special entry <n>
     605                       and replace it with the current dir
     606     -r<n> [<dir>] Go to directory <dir>
     607                               and then put it on special entry <n>
     608     -R<n> [<dir>] Go to directory <dir>
     609                               and put current dir on special entry <n>
     610     -a<n>       Alternative suggested directory. See note below.
     611     -f [<file>] File entries to <file>.
     612     -u [<file>] Update entries from <file>.
     613                 If no filename supplied then default file (~/cdfile) is used
     614                 -F and -U are silent versions
     615     -v          Print version number
     616     -h          Help
     617     -H          Detailed help
     618
     619
     620
     621 Examples
     622 ========
     623
     624 These examples assume non-default mode is set (that is, cd with no
     625 parameters will go to the most recent stack directory), that aliases
     626 have been set up for cd and @ as described above and that cd's prompt
     627 facility is active and the prompt length is 21 characters.
     628
     629     /home/phil$ @
     630     # List the entries with the @
     631     History:
     632     # Output of the @ command
     633     .....
     634     # Skipped these entries for brevity
     635     1 /home/phil/ummdev               S1 /home/phil/perl
     636     # Most recent two history entries
     637     0 /home/phil/perl/eg              S0 /home/phil/umm/ummdev
     638     # and two special entries are shown
     639
     640     /home/phil$ cd /home/phil/utils/Cdll
     641     # Now change directories
     642     /home/phil/utils/Cdll$ @
     643     # Prompt reflects the directory.
     644     History:
     645     # New history
     646     .....
     647     1 /home/phil/perl/eg              S1 /home/phil/perl
     648     # History entry 0 has moved to 1
     649     0 /home/phil                      S0 /home/phil/umm/ummdev
     650     # and the most recent has entered
     651
     652 To go to a history entry:
     653
     654     /home/phil/utils/Cdll$ cd 1
     655     # Go to history entry 1.
     656     /home/phil/perl/eg$
     657     # Current directory is now what was 1
     658
     659 To go to a special entry:
     660
     661     /home/phil/perl/eg$ cd -s1
     662     # Go to special entry 1
     663     /home/phil/umm/ummdev$
     664     # Current directory is S1
     665
     666 To go to a directory called, for example, 1:
     667
     668     /home/phil$ cd -g 1
     669     # -g ignores the special meaning of 1
     670     /home/phil/1$
     671
     672 To put current directory on the special list as S1:
     673     cd -r1 .        #  OR
     674     cd -R1 .        #  These have the same effect if the directory is
     675                     #+ . (the current directory)
     676
     677 To go to a directory and add it as a special
     678   The directory for -r<n> or -R<n> may be a number.
     679   For example:
     680         $ cd -r3 4  Go to history entry 4 and put it on special entry 3
     681         $ cd -R3 4  Put current dir on the special entry 3 and go to
     682                     history entry 4
     683         $ cd -s3    Go to special entry 3
     684
     685     Note that commands R,r,S and s may be used without a number and
     686     refer to 0:
     687         $ cd -s     Go to special entry 0
     688         $ cd -S     Go to special entry 0 and make special entry 0
     689                     current dir
     690         $ cd -r 1   Go to history entry 1 and put it on special entry 0
     691         $ cd -r     Go to history entry 0 and put it on special entry 0
     692
     693
     694     Alternative suggested directories:
     695
     696     If a directory is not found, then CD will suggest any
     697     possibilities. These are directories starting with the same letters
     698     and if any are found they are listed prefixed with -a<n>
     699     where <n> is a number. It's possible to go to the directory
     700     by entering cd -a<n> on the command line.
     701
     702         Use cd -d or -D to change default cd action. cd -H will show
     703         current action.
     704
     705         The history entries (0-n) are stored in the environment variables
     706         CD[0] - CD[n]
     707         Similarly the special directories S0 - 9 are in the environment
     708         variable CDS[0] - CDS[9]
     709         and may be accessed from the command line, for example:
     710
     711             ls -l ${CDS[3]}
     712             cat ${CD[8]}/file.txt
     713
     714         The default pathname for the -f and -u commands is ~
     715         The default filename for the -f and -u commands is cdfile
     716
     717
     718 Configuration
     719 =============
     720
     721     The following environment variables can be set:
     722
     723         CDL_PROMPTLEN  - Set to the length of prompt you require.
     724             Prompt string is set to the right characters of the current
     725             directory. If not set, then prompt is left unchanged. Note
     726             that this is the number of characters that the directory is
     727             shortened to, not the total characters in the prompt.
     728
     729             CDL_PROMPT_PRE - Set to the string to prefix the prompt.
     730                 Default is:
     731                     non-root:  "\\[\\e[01;34m\\]"  (sets colour to blue).
     732                     root:      "\\[\\e[01;31m\\]"  (sets colour to red).
     733
     734             CDL_PROMPT_POST    - Set to the string to suffix the prompt.
     735                 Default is:
     736                     non-root:  "\\[\\e[00m\\]$"
     737                                (resets colour and displays $).
     738                     root:      "\\[\\e[00m\\]#"
     739                                (resets colour and displays #).
     740
     741         Note:
     742             CDL_PROMPT_PRE & _POST only t
     743
     744         CDPath - Set the default path for the -f & -u options.
     745                  Default is home directory
     746         CDFile - Set the default filename for the -f & -u options.
     747                  Default is cdfile
     748
     749
     750     There are three variables defined in the file cdll which control the
     751     number of entries stored or displayed. They are in the section labeled
     752     'Initialisation here' towards the end of the file.
     753
     754         cd_maxhistory       - The number of history entries stored.
     755                               Default is 50.
     756         cd_maxspecial       - The number of special entries allowed.
     757                               Default is 9.
     758         cd_histcount        - The number of history and special entries
     759                               displayed. Default is 9.
     760
     761     Note that cd_maxspecial should be >= cd_histcount to avoid displaying
     762     special entries that can't be set.
     763
     764
     765 Version: 1.2.1 Date: 24-MAY-2003
     766
     767 DOCUMENTATION

Example A-34. A soundcard setup script

       1 #!/bin/bash
       2 # soundcard-on.sh
       3
       4 #  Script author: Mkarcher
       5 #  http://www.thinkwiki.org/wiki  ...
       6 #  /Script_for_configuring_the_CS4239_sound_chip_in_PnP_mode
       7 #  ABS Guide author made minor changes and added comments.
       8 #  Couldn't contact script author to ask for permission to use, but ...
       9 #+ the script was released under the FDL,
      10 #+ so its use here should be both legal and ethical.
      11
      12 #  Sound-via-pnp-script for Thinkpad 600E
      13 #+ and possibly other computers with onboard CS4239/CS4610
      14 #+ that do not work with the PCI driver
      15 #+ and are not recognized by the PnP code of snd-cs4236.
      16 #  Also for some 770-series Thinkpads, such as the 770x.
      17 #  Run as root user, of course.
      18 #
      19 #  These are old and very obsolete laptop computers,
      20 #+ but this particular script is very instructive,
      21 #+ as it shows how to set up and hack device files.
      22
      23
      24
      25 #  Search for sound card pnp device:
      26
      27 for dev in /sys/bus/pnp/devices/*
      28 do
      29   grep CSC0100 $dev/id > /dev/null && WSSDEV=$dev
      30   grep CSC0110 $dev/id > /dev/null && CTLDEV=$dev
      31 done
      32 # On 770x:
      33 # WSSDEV = /sys/bus/pnp/devices/00:07
      34 # CTLDEV = /sys/bus/pnp/devices/00:06
      35 # These are symbolic links to /sys/devices/pnp0/ ...
      36
      37
      38 #  Activate devices:
      39 #  Thinkpad boots with devices disabled unless "fast boot" is turned off
      40 #+ (in BIOS).
      41
      42 echo activate > $WSSDEV/resources
      43 echo activate > $CTLDEV/resources
      44
      45
      46 # Parse resource settings.
      47
      48 { read # Discard "state = active" (see below).
      49   read bla port1
      50   read bla port2
      51   read bla port3
      52   read bla irq
      53   read bla dma1
      54   read bla dma2
      55  # The "bla's" are labels in the first field: "io," "state," etc.
      56  # These are discarded.
      57
      58  #  Hack: with PnPBIOS: ports are: port1: WSS, port2:
      59  #+ OPL, port3: sb (unneeded)
      60  #       with ACPI-PnP:ports are: port1: OPL, port2: sb, port3: WSS
      61  #  (ACPI bios seems to be wrong here, the PnP-card-code in snd-cs4236.c
      62  #+  uses the PnPBIOS port order)
      63  #  Detect port order using the fixed OPL port as reference.
      64   if [ ${port2%%-*} = 0x388 ]
      65  #            ^^^^  Strip out everything following hyphen in port address.
      66  #                  So, if port1 is 0x530-0x537
      67  #+                 we're left with 0x530 -- the start address of the port.
      68  then
      69    # PnPBIOS: usual order
      70    port=${port1%%-*}
      71    oplport=${port2%%-*}
      72  else
      73    # ACPI: mixed-up order
      74    port=${port3%%-*}
      75    oplport=${port1%%-*}
      76  fi
      77  } < $WSSDEV/resources
      78 # To see what's going on here:
      79 # ---------------------------
      80 #   cat /sys/devices/pnp0/00:07/resources
      81 #
      82 #   state = active
      83 #   io 0x530-0x537
      84 #   io 0x388-0x38b
      85 #   io 0x220-0x233
      86 #   irq 5
      87 #   dma 1
      88 #   dma 0
      89 #   ^^^   "bla" labels in first field (discarded).
      90
      91
      92 { read # Discard first line, as above.
      93   read bla port1
      94   cport=${port1%%-*}
      95   #            ^^^^
      96   # Just want _start_ address of port.
      97 } < $CTLDEV/resources
      98
      99
     100 # Load the module:
     101
     102 modprobe --ignore-install snd-cs4236 port=$port cport=$cport\
     103 fm_port=$oplport irq=$irq dma1=$dma1 dma2=$dma2 isapnp=0 index=0
     104 # See the modprobe manpage.
     105
     106 exit $?

Example A-35. Locating split paragraphs in a text file

       1 #!/bin/bash
       2 # find-splitpara.sh
       3 #  Finds split paragraphs in a text file,
       4 #+ and tags the line numbers.
       5
       6
       7 ARGCOUNT=1       # Expect one arg.
       8 OFF=0            # Flag states.
       9 ON=1
      10 E_WRONGARGS=85
      11
      12 file="$1"        # Target filename.
      13 lineno=1         # Line number. Start at 1.
      14 Flag=$OFF        # Blank line flag.
      15
      16 if [ $# -ne "$ARGCOUNT" ]
      17 then
      18   echo "Usage: `basename $0` FILENAME"
      19   exit $E_WRONGARGS
      20 fi
      21
      22 file_read ()     # Scan file for pattern, then print line.
      23 {
      24 while read line
      25 do
      26
      27   if [[ "$line" =~ ^[a-z] && $Flag -eq $ON ]]
      28      then  # Line begins with lowercase character, following blank line.
      29      echo -n "$lineno::   "
      30      echo "$line"
      31   fi
      32
      33
      34   if [[ "$line" =~ ^$ ]]
      35      then       #  If blank line,
      36      Flag=$ON   #+ set flag.
      37   else
      38      Flag=$OFF
      39   fi
      40
      41   ((lineno++))
      42
      43 done
      44 } < $file  # Redirect file into function's stdin.
      45
      46 file_read
      47
      48
      49 exit $?
      50
      51
      52 # ----------------------------------------------------------------
      53 This is line one of an example paragraph, bla, bla, bla.
      54 This is line two, and line three should follow on next line, but
      55
      56 there is a blank line separating the two parts of the paragraph.
      57 # ----------------------------------------------------------------
      58
      59 Running this script on a file containing the above paragraph
      60 yields:
      61
      62 4::   there is a blank line separating the two parts of the paragraph.
      63
      64
      65 There will be additional output for all the other split paragraphs
      66 in the target file.

Example A-36. Insertion sort

       1 #!/bin/bash
       2 # insertion-sort.bash: Insertion sort implementation in Bash
       3 #                      Heavy use of Bash array features:
       4 #+                     (string) slicing, merging, etc
       5 # URL: http://www.lugmen.org.ar/~jjo/jjotip/insertion-sort.bash.d
       6 #+          /insertion-sort.bash.sh
       7 #
       8 # Author: JuanJo Ciarlante <jjo@irrigacion.gov.ar>
       9 # Lightly reformatted by ABS Guide author.
      10 # License: GPLv2
      11 # Used in ABS Guide with author's permission (thanks!).
      12 #
      13 # Test with:   ./insertion-sort.bash -t
      14 # Or:          bash insertion-sort.bash -t
      15 # The following *doesn't* work:
      16 #              sh insertion-sort.bash -t
      17 #  Why not? Hint: which Bash-specific features are disabled
      18 #+ when running a script by 'sh script.sh'?
      19 #
      20 : ${DEBUG:=0}  # Debug, override with:  DEBUG=1 ./scriptname . . .
      21 # Parameter substitution -- set DEBUG to 0 if not previously set.
      22
      23 # Global array: "list"
      24 typeset -a list
      25 # Load whitespace-separated numbers from stdin.
      26 if [ "$1" = "-t" ]; then
      27 DEBUG=1
      28         read -a list < <( od -Ad -w24 -t u2 /dev/urandom ) # Random list.
      29 #                    ^ ^  process substition
      30 else
      31         read -a list
      32 fi
      33 numelem=${#list[*]}
      34
      35 #  Shows the list, marking the element whose index is $1
      36 #+ by surrounding it with the two chars passed as $2.
      37 #  Whole line prefixed with $3.
      38 showlist()
      39   {
      40   echo "$3"${list[@]:0:$1} ${2:0:1}${list[$1]}${2:1:1} ${list[@]:$1+1};
      41   }
      42
      43 # Loop _pivot_ -- from second element to end of list.
      44 for(( i=1; i<numelem; i++ )) do
      45         ((DEBUG))&&showlist i "[]" " "
      46         # From current _pivot_, back to first element.
      47         for(( j=i; j; j-- )) do
      48                 # Search for the 1st elem. less than current "pivot" . . .
      49                 [[ "${list[j-1]}" -le "${list[i]}" ]] && break
      50         done
      51 	(( i==j )) && continue ## No insertion was needed for this element.
      52 	# . . . Move list[i] (pivot) to the left of list[j]:
      53         list=(${list[@]:0:j} ${list[i]} ${list[j]}\
      54 	#         {0,j-1}        {i}       {j}
      55               ${list[@]:j+1:i-(j+1)} ${list[@]:i+1})
      56 	#         {j+1,i-1}              {i+1,last}
      57 	((DEBUG))&&showlist j "<>" "*"
      58 done
      59
      60
      61 echo
      62 echo  "------"
      63 echo $'Result:\n'${list[@]}
      64
      65 exit $?

Example A-37. Standard Deviation

       1 #!/bin/bash
       2 # sd.sh: Standard Deviation
       3
       4 #  The Standard Deviation indicates how consistent a set of data is.
       5 #  It shows to what extent the individual data points deviate from the
       6 #+ arithmetic mean, i.e., how much they "bounce around" (or cluster).
       7 #  It is essentially the average deviation-distance of the
       8 #+ data points from the mean.
       9
      10 # =========================================================== #
      11 #    To calculate the Standard Deviation:
      12 #
      13 # 1  Find the arithmetic mean (average) of all the data points.
      14 # 2  Subtract each data point from the arithmetic mean,
      15 #    and square that difference.
      16 # 3  Add all of the individual difference-squares in # 2.
      17 # 4  Divide the sum in # 3 by the number of data points.
      18 #    This is known as the "variance."
      19 # 5  The square root of # 4 gives the Standard Deviation.
      20 # =========================================================== #
      21
      22 count=0         # Number of data points; global.
      23 SC=9            # Scale to be used by bc. Nine decimal places.
      24 E_DATAFILE=90   # Data file error.
      25
      26 # ----------------- Set data file ---------------------
      27 if [ ! -z "$1" ]  # Specify filename as cmd-line arg?
      28 then
      29   datafile="$1" #  ASCII text file,
      30 else            #+ one (numerical) data point per line!
      31   datafile=sample.dat
      32 fi              #  See example data file, below.
      33
      34 if [ ! -e "$datafile" ]
      35 then
      36   echo "\""$datafile"\" does not exist!"
      37   exit $E_DATAFILE
      38 fi
      39 # -----------------------------------------------------
      40
      41
      42 arith_mean ()
      43 {
      44   local rt=0         # Running total.
      45   local am=0         # Arithmetic mean.
      46   local ct=0         # Number of data points.
      47
      48   while read value   # Read one data point at a time.
      49   do
      50     rt=$(echo "scale=$SC; $rt + $value" | bc)
      51     (( ct++ ))
      52   done
      53
      54   am=$(echo "scale=$SC; $rt / $ct" | bc)
      55
      56   echo $am; return $ct   # This function "returns" TWO values!
      57   #  Caution: This little trick will not work if $ct > 255!
      58   #  To handle a larger number of data points,
      59   #+ simply comment out the "return $ct" above.
      60 } <"$datafile"   # Feed in data file.
      61
      62 sd ()
      63 {
      64   mean1=$1  # Arithmetic mean (passed to function).
      65   n=$2      # How many data points.
      66   sum2=0    # Sum of squared differences ("variance").
      67   avg2=0    # Average of $sum2.
      68   sdev=0    # Standard Deviation.
      69
      70   while read value   # Read one line at a time.
      71   do
      72     diff=$(echo "scale=$SC; $mean1 - $value" | bc)
      73     # Difference between arith. mean and data point.
      74     dif2=$(echo "scale=$SC; $diff * $diff" | bc) # Squared.
      75     sum2=$(echo "scale=$SC; $sum2 + $dif2" | bc) # Sum of squares.
      76   done
      77
      78     avg2=$(echo "scale=$SC; $sum2 / $n" | bc)  # Avg. of sum of squares.
      79     sdev=$(echo "scale=$SC; sqrt($avg2)" | bc) # Square root =
      80     echo $sdev                                 # Standard Deviation.
      81
      82 } <"$datafile"   # Rewinds data file.
      83
      84
      85 # ======================================================= #
      86 mean=$(arith_mean); count=$?   # Two returns from function!
      87 std_dev=$(sd $mean $count)
      88
      89 echo
      90 echo "Number of data points in \""$datafile"\" = $count"
      91 echo "Arithmetic mean (average) = $mean"
      92 echo "Standard Deviation = $std_dev"
      93 echo
      94 # ======================================================= #
      95
      96 exit
      97
      98 #  This script could stand some drastic streamlining,
      99 #+ but not at the cost of reduced legibility, please.
     100
     101
     102 # ++++++++++++++++++++++++++++++++++++++++ #
     103 # A sample data file (sample1.dat):
     104
     105 # 18.35
     106 # 19.0
     107 # 18.88
     108 # 18.91
     109 # 18.64
     110
     111
     112 # $ sh sd.sh sample1.dat
     113
     114 # Number of data points in "sample1.dat" = 5
     115 # Arithmetic mean (average) = 18.756000000
     116 # Standard Deviation = .235338054
     117 # ++++++++++++++++++++++++++++++++++++++++ #

Example A-38. A pad file generator for shareware authors

       1 #!/bin/bash
       2 # pad.sh
       3
       4 #######################################################
       5 #               PAD (xml) file creator
       6 #+ Written by Mendel Cooper <thegrendel.abs@gmail.com>.
       7 #+ Released to the Public Domain.
       8 #
       9 #  Generates a "PAD" descriptor file for shareware
      10 #+ packages, according to the specifications
      11 #+ of the ASP.
      12 #  http://www.asp-shareware.org/pad
      13 #######################################################
      14
      15
      16 # Accepts (optional) save filename as a command-line argument.
      17 if [ -n "$1" ]
      18 then
      19   savefile=$1
      20 else
      21   savefile=save_file.xml               # Default save_file name.
      22 fi
      23
      24
      25 # ===== PAD file headers =====
      26 HDR1="<?xml version=\"1.0\" encoding=\"Windows-1252\" ?>"
      27 HDR2="<XML_DIZ_INFO>"
      28 HDR3="<MASTER_PAD_VERSION_INFO>"
      29 HDR4="\t<MASTER_PAD_VERSION>1.15</MASTER_PAD_VERSION>"
      30 HDR5="\t<MASTER_PAD_INFO>Portable Application Description, or PAD
      31 for short, is a data set that is used by shareware authors to
      32 disseminate information to anyone interested in their software products.
      33 To find out more go to http://www.asp-shareware.org/pad</MASTER_PAD_INFO>"
      34 HDR6="</MASTER_PAD_VERSION_INFO>"
      35 # ============================
      36
      37
      38 fill_in ()
      39 {
      40   if [ -z "$2" ]
      41   then
      42     echo -n "$1? "     # Get user input.
      43   else
      44     echo -n "$1 $2? "  # Additional query?
      45   fi
      46
      47   read var             # May paste to fill in field.
      48                        # This shows how flexible "read" can be.
      49
      50   if [ -z "$var" ]
      51   then
      52     echo -e "\t\t<$1 />" >>$savefile    # Indent with 2 tabs.
      53     return
      54   else
      55     echo -e "\t\t<$1>$var</$1>" >>$savefile
      56     return ${#var}     # Return length of input string.
      57   fi
      58 }
      59
      60 check_field_length ()  # Check length of program description fields.
      61 {
      62   # $1 = maximum field length
      63   # $2 = actual field length
      64   if [ "$2" -gt "$1" ]
      65   then
      66     echo "Warning: Maximum field length of $1 characters exceeded!"
      67   fi
      68 }
      69
      70 clear                  # Clear screen.
      71 echo "PAD File Creator"
      72 echo "--- ---- -------"
      73 echo
      74
      75 # Write File Headers to file.
      76 echo $HDR1 >$savefile
      77 echo $HDR2 >>$savefile
      78 echo $HDR3 >>$savefile
      79 echo -e $HDR4 >>$savefile
      80 echo -e $HDR5 >>$savefile
      81 echo $HDR6 >>$savefile
      82
      83
      84 # Company_Info
      85 echo "COMPANY INFO"
      86 CO_HDR="Company_Info"
      87 echo "<$CO_HDR>" >>$savefile
      88
      89 fill_in Company_Name
      90 fill_in Address_1
      91 fill_in Address_2
      92 fill_in City_Town
      93 fill_in State_Province
      94 fill_in Zip_Postal_Code
      95 fill_in Country
      96
      97 # If applicable:
      98 # fill_in ASP_Member "[Y/N]"
      99 # fill_in ASP_Member_Number
     100 # fill_in ESC_Member "[Y/N]"
     101
     102 fill_in Company_WebSite_URL
     103
     104 clear   # Clear screen between sections.
     105
     106    # Contact_Info
     107 echo "CONTACT INFO"
     108 CONTACT_HDR="Contact_Info"
     109 echo "<$CONTACT_HDR>" >>$savefile
     110 fill_in Author_First_Name
     111 fill_in Author_Last_Name
     112 fill_in Author_Email
     113 fill_in Contact_First_Name
     114 fill_in Contact_Last_Name
     115 fill_in Contact_Email
     116 echo -e "\t</$CONTACT_HDR>" >>$savefile
     117    # END Contact_Info
     118
     119 clear
     120
     121    # Support_Info
     122 echo "SUPPORT INFO"
     123 SUPPORT_HDR="Support_Info"
     124 echo "<$SUPPORT_HDR>" >>$savefile
     125 fill_in Sales_Email
     126 fill_in Support_Email
     127 fill_in General_Email
     128 fill_in Sales_Phone
     129 fill_in Support_Phone
     130 fill_in General_Phone
     131 fill_in Fax_Phone
     132 echo -e "\t</$SUPPORT_HDR>" >>$savefile
     133    # END Support_Info
     134
     135 echo "</$CO_HDR>" >>$savefile
     136 # END Company_Info
     137
     138 clear
     139
     140 # Program_Info
     141 echo "PROGRAM INFO"
     142 PROGRAM_HDR="Program_Info"
     143 echo "<$PROGRAM_HDR>" >>$savefile
     144 fill_in Program_Name
     145 fill_in Program_Version
     146 fill_in Program_Release_Month
     147 fill_in Program_Release_Day
     148 fill_in Program_Release_Year
     149 fill_in Program_Cost_Dollars
     150 fill_in Program_Cost_Other
     151 fill_in Program_Type "[Shareware/Freeware/GPL]"
     152 fill_in Program_Release_Status "[Beta, Major Upgrade, etc.]"
     153 fill_in Program_Install_Support
     154 fill_in Program_OS_Support "[Win9x/Win2k/Linux/etc.]"
     155 fill_in Program_Language "[English/Spanish/etc.]"
     156
     157 echo; echo
     158
     159   # File_Info
     160 echo "FILE INFO"
     161 FILEINFO_HDR="File_Info"
     162 echo "<$FILEINFO_HDR>" >>$savefile
     163 fill_in Filename_Versioned
     164 fill_in Filename_Previous
     165 fill_in Filename_Generic
     166 fill_in Filename_Long
     167 fill_in File_Size_Bytes
     168 fill_in File_Size_K
     169 fill_in File_Size_MB
     170 echo -e "\t</$FILEINFO_HDR>" >>$savefile
     171   # END File_Info
     172
     173 clear
     174
     175   # Expire_Info
     176 echo "EXPIRE INFO"
     177 EXPIRE_HDR="Expire_Info"
     178 echo "<$EXPIRE_HDR>" >>$savefile
     179 fill_in Has_Expire_Info "Y/N"
     180 fill_in Expire_Count
     181 fill_in Expire_Based_On
     182 fill_in Expire_Other_Info
     183 fill_in Expire_Month
     184 fill_in Expire_Day
     185 fill_in Expire_Year
     186 echo -e "\t</$EXPIRE_HDR>" >>$savefile
     187   # END Expire_Info
     188
     189 clear
     190
     191   # More Program_Info
     192 echo "ADDITIONAL PROGRAM INFO"
     193 fill_in Program_Change_Info
     194 fill_in Program_Specific_Category
     195 fill_in Program_Categories
     196 fill_in Includes_JAVA_VM "[Y/N]"
     197 fill_in Includes_VB_Runtime "[Y/N]"
     198 fill_in Includes_DirectX "[Y/N]"
     199   # END More Program_Info
     200
     201 echo "</$PROGRAM_HDR>" >>$savefile
     202 # END Program_Info
     203
     204 clear
     205
     206 # Program Description
     207 echo "PROGRAM DESCRIPTIONS"
     208 PROGDESC_HDR="Program_Descriptions"
     209 echo "<$PROGDESC_HDR>" >>$savefile
     210
     211 LANG="English"
     212 echo "<$LANG>" >>$savefile
     213
     214 fill_in Keywords "[comma + space separated]"
     215 echo
     216 echo "45, 80, 250, 450, 2000 word program descriptions"
     217 echo "(may cut and paste into field)"
     218 #  It would be highly appropriate to compose the following
     219 #+ "Char_Desc" fields with a text editor,
     220 #+ then cut-and-paste the text into the answer fields.
     221 echo
     222 echo "              |---------------45 characters---------------|"
     223 fill_in Char_Desc_45
     224 check_field_length 45 "$?"
     225 echo
     226 fill_in Char_Desc_80
     227 check_field_length 80 "$?"
     228
     229 fill_in Char_Desc_250
     230 check_field_length 250 "$?"
     231
     232 fill_in Char_Desc_450
     233 fill_in Char_Desc_2000
     234
     235 echo "</$LANG>" >>$savefile
     236 echo "</$PROGDESC_HDR>" >>$savefile
     237 # END Program Description
     238
     239 clear
     240 echo "Done."; echo; echo
     241 echo "Save file is:  \""$savefile"\""
     242
     243 exit 0

Example A-39. A man page editor

       1 #!/bin/bash
       2 # maned.sh
       3 # A rudimentary man page editor
       4
       5 # Version: 0.1 (Alpha, probably buggy)
       6 # Author: Mendel Cooper <thegrendel.abs@gmail.com>
       7 # Reldate: 16 June 2008
       8 # License: GPL3
       9
      10
      11 savefile=      # Global, used in multiple functions.
      12 E_NOINPUT=90   # User input missing (error). May or may not be critical.
      13
      14 # =========== Markup Tags ============ #
      15 TopHeader=".TH"
      16 NameHeader=".SH NAME"
      17 SyntaxHeader=".SH SYNTAX"
      18 SynopsisHeader=".SH SYNOPSIS"
      19 InstallationHeader=".SH INSTALLATION"
      20 DescHeader=".SH DESCRIPTION"
      21 OptHeader=".SH OPTIONS"
      22 FilesHeader=".SH FILES"
      23 EnvHeader=".SH ENVIRONMENT"
      24 AuthHeader=".SH AUTHOR"
      25 BugsHeader=".SH BUGS"
      26 SeeAlsoHeader=".SH SEE ALSO"
      27 BOLD=".B"
      28 # Add more tags, as needed.
      29 # See groff docs for markup meanings.
      30 # ==================================== #
      31
      32 start ()
      33 {
      34 clear                  # Clear screen.
      35 echo "ManEd"
      36 echo "-----"
      37 echo
      38 echo "Simple man page creator"
      39 echo "Author: Mendel Cooper"
      40 echo "License: GPL3"
      41 echo; echo; echo
      42 }
      43
      44 progname ()
      45 {
      46   echo -n "Program name? "
      47   read name
      48
      49   echo -n "Manpage section? [Hit RETURN for default (\"1\") ]  "
      50   read section
      51   if [ -z "$section" ]
      52   then
      53     section=1   # Most man pages are in section 1.
      54   fi
      55
      56   if [ -n "$name" ]
      57   then
      58     savefile=""$name"."$section""       #  Filename suffix = section.
      59     echo -n "$1 " >>$savefile
      60     name1=$(echo "$name" | tr a-z A-Z)  #  Change to uppercase,
      61                                         #+ per man page convention.
      62     echo -n "$name1" >>$savefile
      63   else
      64     echo "Error! No input."             # Mandatory input.
      65     exit $E_NOINPUT                     # Critical!
      66     #  Exercise: The script-abort if no filename input is a bit clumsy.
      67     #            Rewrite this section so a default filename is used
      68     #+           if no input.
      69   fi
      70
      71   echo -n "  \"$section\"">>$savefile   # Append, always append.
      72
      73   echo -n "Version? "
      74   read ver
      75   echo -n " \"Version $ver \"">>$savefile
      76   echo >>$savefile
      77
      78   echo -n "Short description [0 - 5 words]? "
      79   read sdesc
      80   echo "$NameHeader">>$savefile
      81   echo ""$BOLD" "$name"">>$savefile
      82   echo "\- "$sdesc"">>$savefile
      83
      84 }
      85
      86 fill_in ()
      87 { # This function more or less copied from "pad.sh" script.
      88   echo -n "$2? "       # Get user input.
      89   read var             # May paste (a single line only!) to fill in field.
      90
      91   if [ -n "$var" ]
      92   then
      93     echo "$1 " >>$savefile
      94     echo -n "$var" >>$savefile
      95   else                 # Don't append empty field to file.
      96     return $E_NOINPUT  # Not critical here.
      97   fi
      98
      99   echo >>$savefile
     100
     101 }
     102
     103
     104 end ()
     105 {
     106 clear
     107 echo -n "Would you like to view the saved man page (y/n)? "
     108 read ans
     109 if [ "$ans" = "n" -o "$ans" = "N" ]; then exit; fi
     110 exec less "$savefile"  #  Exit script and hand off control to "less" ...
     111                        #+ ... which formats for viewing man page source.
     112 }
     113
     114
     115 # ---------------------------------------- #
     116 start
     117 progname "$TopHeader"
     118 fill_in "$SynopsisHeader" "Synopsis"
     119 fill_in "$DescHeader" "Long description"
     120 # May paste in *single line* of text.
     121 fill_in "$OptHeader" "Options"
     122 fill_in "$FilesHeader" "Files"
     123 fill_in "$AuthHeader" "Author"
     124 fill_in "$BugsHeader" "Bugs"
     125 fill_in "$SeeAlsoHeader" "See also"
     126 # fill_in "$OtherHeader" ... as necessary.
     127 end    # ... exit not needed.
     128 # ---------------------------------------- #
     129
     130 #  Note that the generated man page will usually
     131 #+ require manual fine-tuning with a text editor.
     132 #  However, it's a distinct improvement upon
     133 #+ writing man source from scratch
     134 #+ or even editing a blank man page template.
     135
     136 #  The main deficiency of the script is that it permits
     137 #+ pasting only a single text line into the input fields.
     138 #  This may be a long, cobbled-together line, which groff
     139 #  will automatically wrap and hyphenate.
     140 #  However, if you want multiple (newline-separated) paragraphs,
     141 #+ these must be inserted by manual text editing on the
     142 #+ script-generated man page.
     143 #  Exercise (difficult): Fix this!
     144
     145 #  This script is not nearly as elaborate as the
     146 #+ full-featured "manedit" package
     147 #+ http://freshmeat.net/projects/manedit/
     148 #+ but it's much easier to use.

Example A-40. Petals Around the Rose

       1 #!/bin/bash -i
       2 # petals.sh
       3
       4 #########################################################################
       5 # Petals Around the Rose                                                #
       6 #                                                                       #
       7 # Version 0.1 Created by Serghey Rodin                                  #
       8 # Version 0.2 Modded by ABS Guide Author                                #
       9 #                                                                       #
      10 # License: GPL3                                                         #
      11 # Used in ABS Guide with permission.                                    #
      12 # ##################################################################### #
      13
      14 hits=0      # Correct guesses.
      15 WIN=6       # Mastered the game.
      16 ALMOST=5    # One short of mastery.
      17 EXIT=exit   # Give up early?
      18
      19 RANDOM=$$   # Seeds the random number generator from PID of script.
      20
      21
      22 # Bones (ASCII graphics for dice)
      23 bone1[1]="|         |"
      24 bone1[2]="|       o |"
      25 bone1[3]="|       o |"
      26 bone1[4]="| o     o |"
      27 bone1[5]="| o     o |"
      28 bone1[6]="| o     o |"
      29 bone2[1]="|    o    |"
      30 bone2[2]="|         |"
      31 bone2[3]="|    o    |"
      32 bone2[4]="|         |"
      33 bone2[5]="|    o    |"
      34 bone2[6]="| o     o |"
      35 bone3[1]="|         |"
      36 bone3[2]="| o       |"
      37 bone3[3]="| o       |"
      38 bone3[4]="| o     o |"
      39 bone3[5]="| o     o |"
      40 bone3[6]="| o     o |"
      41 bone="+---------+"
      42
      43
      44
      45 # Functions
      46
      47 instructions () {
      48
      49   clear
      50   echo -n "Do you need instructions? (y/n) "; read ans
      51   if [ "$ans" = "y" -o "$ans" = "Y" ]; then
      52     clear
      53     echo -e '\E[34;47m'  # Blue type.
      54
      55 #  "cat document"
      56     cat <<INSTRUCTIONSZZZ
      57 The name of the game is Petals Around the Rose,
      58 and that name is significant.
      59 Five dice will roll and you must guess the "answer" for each roll.
      60 It will be zero or an even number.
      61 After your guess, you will be told the answer for the roll, but . . .
      62 that's ALL the information you will get.
      63
      64 Six consecutive correct guesses admits you to the
      65 Fellowship of the Rose.
      66 INSTRUCTIONSZZZ
      67
      68     echo -e "\033[0m"    # Turn off blue.
      69     else clear
      70   fi
      71
      72 }
      73
      74
      75 fortune ()
      76 {
      77   RANGE=7
      78   FLOOR=0
      79   number=0
      80   while [ "$number" -le $FLOOR ]
      81   do
      82     number=$RANDOM
      83     let "number %= $RANGE"   # 1 - 6.
      84   done
      85
      86   return $number
      87 }
      88
      89
      90
      91 throw () { # Calculate each individual die.
      92   fortune; B1=$?
      93   fortune; B2=$?
      94   fortune; B3=$?
      95   fortune; B4=$?
      96   fortune; B5=$?
      97
      98   calc () { # Function embedded within a function!
      99     case "$1" in
     100        3   ) rose=2;;
     101        5   ) rose=4;;
     102        *   ) rose=0;;
     103     esac    # Simplified algorithm.
     104             # Doesn't really get to the heart of the matter.
     105     return $rose
     106   }
     107
     108   answer=0
     109   calc "$B1"; answer=$(expr $answer + $(echo $?))
     110   calc "$B2"; answer=$(expr $answer + $(echo $?))
     111   calc "$B3"; answer=$(expr $answer + $(echo $?))
     112   calc "$B4"; answer=$(expr $answer + $(echo $?))
     113   calc "$B5"; answer=$(expr $answer + $(echo $?))
     114 }
     115
     116
     117
     118 game ()
     119 { # Generate graphic display of dice throw.
     120   throw
     121     echo -e "\033[1m"    # Bold.
     122   echo -e "\n"
     123   echo -e "$bone\t$bone\t$bone\t$bone\t$bone"
     124   echo -e \
     125  "${bone1[$B1]}\t${bone1[$B2]}\t${bone1[$B3]}\t${bone1[$B4]}\t${bone1[$B5]}"
     126   echo -e \
     127  "${bone2[$B1]}\t${bone2[$B2]}\t${bone2[$B3]}\t${bone2[$B4]}\t${bone2[$B5]}"
     128   echo -e \
     129  "${bone3[$B1]}\t${bone3[$B2]}\t${bone3[$B3]}\t${bone3[$B4]}\t${bone3[$B5]}"
     130   echo -e "$bone\t$bone\t$bone\t$bone\t$bone"
     131   echo -e "\n\n\t\t"
     132     echo -e "\033[0m"    # Turn off bold.
     133   echo -n "There are how many petals around the rose? "
     134 }
     135
     136
     137
     138 # ============================================================== #
     139
     140 instructions
     141
     142 while [ "$petal" != "$EXIT" ]    # Main loop.
     143 do
     144   game
     145   read petal
     146   echo "$petal" | grep [0-9] >/dev/null  # Filter response for digit.
     147                                          # Otherwise just roll dice again.
     148   if [ "$?" -eq 0 ]   # If-loop #1.
     149   then
     150     if [ "$petal" == "$answer" ]; then    # If-loop #2.
     151     	echo -e "\nCorrect. There are $petal petals around the rose.\n"
     152         (( hits++ ))
     153
     154         if [ "$hits" -eq "$WIN" ]; then   # If-loop #3.
     155           echo -e '\E[31;47m'  # Red type.
     156           echo -e "\033[1m"    # Bold.
     157           echo "You have unraveled the mystery of the Rose Petals!"
     158           echo "Welcome to the Fellowship of the Rose!!!"
     159           echo "(You are herewith sworn to secrecy.)"; echo
     160           echo -e "\033[0m"    # Turn off red & bold.
     161           break                # Exit!
     162         else echo "You have $hits correct so far."; echo
     163
     164         if [ "$hits" -eq "$ALMOST" ]; then
     165           echo "Just one more gets you to the heart of the mystery!"; echo
     166         fi
     167
     168       fi                                  # Close if-loop #3.
     169
     170     else
     171       echo -e "\nWrong. There are $answer petals around the rose.\n"
     172       hits=0   # Reset number of correct guesses.
     173     fi                                    # Close if-loop #2.
     174
     175     echo -n "Hit ENTER for the next roll, or type \"exit\" to end. "
     176     read
     177     if [ "$REPLY" = "$EXIT" ]; then exit
     178     fi
     179
     180   fi                  # Close if-loop #1.
     181
     182   clear
     183 done                  # End of main (while) loop.
     184
     185 ###
     186
     187 exit $?
     188
     189 # Resources:
     190 # ---------
     191 # 1) http://en.wikipedia.org/wiki/Petals_Around_the_Rose
     192 #    (Wikipedia entry.)
     193 # 2) http://www.borrett.id.au/computing/petals-bg.htm
     194 #    (How Bill Gates coped with the Petals Around the Rose challenge.)

Example A-41. Quacky: a Perquackey-type word game

       1 #!/bin/bash
       2 # qky.sh
       3
       4 ##############################################################
       5 # QUACKEY: a somewhat simplified version of Perquackey [TM]. #
       6 #                                                            #
       7 # Author: Mendel Cooper  <thegrendel.abs@gmail.com>          #
       8 # version 0.1.02      03 May, 2008                           #
       9 # License: GPL3                                              #
      10 ##############################################################
      11
      12 WLIST=/usr/share/dict/word.lst
      13 #                     ^^^^^^^^  Word list file found here.
      14 #  ASCII word list, one word per line, UNIX format.
      15 #  A suggested list is the script author's "yawl" word list package.
      16 #  http://bash.deta.in/yawl-0.3.2.tar.gz
      17 #    or
      18 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
      19
      20 NONCONS=0     # Word not constructable from letter set.
      21 CONS=1        # Constructable.
      22 SUCCESS=0
      23 NG=1
      24 FAILURE=''
      25 NULL=0        # Zero out value of letter (if found).
      26 MINWLEN=3     # Minimum word length.
      27 MAXCAT=5      # Maximum number of words in a given category.
      28 PENALTY=200   # General-purpose penalty for unacceptable words.
      29 total=
      30 E_DUP=70      # Duplicate word error.
      31
      32 TIMEOUT=10    # Time for word input.
      33
      34 NVLET=10      # 10 letters for non-vulnerable.
      35 VULET=13      # 13 letters for vulnerable (not yet implemented!).
      36
      37 declare -a Words
      38 declare -a Status
      39 declare -a Score=( 0 0 0 0 0 0 0 0 0 0 0 )
      40
      41
      42 letters=( a n s r t m l k p r b c i d s i d z e w u e t f
      43 e y e r e f e g t g h h i t r s c i t i d i j a t a o l a
      44 m n a n o v n w o s e l n o s p a q e e r a b r s a o d s
      45 t g t i t l u e u v n e o x y m r k )
      46 #  Letter distribution table shamelessly borrowed from "Wordy" game,
      47 #+ ca. 1992, written by a certain fine fellow named Mendel Cooper.
      48
      49 declare -a LS
      50
      51 numelements=${#letters[@]}
      52 randseed="$1"
      53
      54 instructions ()
      55 {
      56   clear
      57   echo "Welcome to QUACKEY, the anagramming word construction game."; echo
      58   echo -n "Do you need instructions? (y/n) "; read ans
      59
      60    if [ "$ans" = "y" -o "$ans" = "Y" ]; then
      61      clear
      62      echo -e '\E[31;47m'  # Red foreground. '\E[34;47m' for blue.
      63      cat <<INSTRUCTION1
      64
      65 QUACKEY is a variant of Perquackey [TM].
      66 The rules are the same, but the scoring is simplified
      67 and plurals of previously played words are allowed.
      68 "Vulnerable" play is not yet implemented,
      69 but it is otherwise feature-complete.
      70
      71 As the game begins, the player gets 10 letters.
      72 The object is to construct valid dictionary words
      73 of at least 3-letter length from the letterset.
      74 Each word-length category
      75 -- 3-letter, 4-letter, 5-letter, ... --
      76 fills up with the fifth word entered,
      77 and no further words in that category are accepted.
      78
      79 The penalty for too-short (two-letter), duplicate, unconstructable,
      80 and invalid (not in dictionary) words is -200. The same penalty applies
      81 to attempts to enter a word in a filled-up category.
      82
      83 INSTRUCTION1
      84
      85   echo -n "Hit ENTER for next page of instructions. "; read az1
      86
      87      cat <<INSTRUCTION2
      88
      89 The scoring mostly corresponds to classic Perquackey:
      90 The first 3-letter word scores    60, plus   10 for each additional one.
      91 The first 4-letter word scores   120, plus   20 for each additional one.
      92 The first 5-letter word scores   200, plus   50 for each additional one.
      93 The first 6-letter word scores   300, plus  100 for each additional one.
      94 The first 7-letter word scores   500, plus  150 for each additional one.
      95 The first 8-letter word scores   750, plus  250 for each additional one.
      96 The first 9-letter word scores  1000, plus  500 for each additional one.
      97 The first 10-letter word scores 2000, plus 2000 for each additional one.
      98
      99 Category completion bonuses are:
     100 3-letter words   100
     101 4-letter words   200
     102 5-letter words   400
     103 6-letter words   800
     104 7-letter words  2000
     105 8-letter words 10000
     106 This is a simplification of the absurdly baroque Perquackey bonus
     107 scoring system.
     108
     109 INSTRUCTION2
     110
     111   echo -n "Hit ENTER for final page of instructions. "; read az1
     112
     113      cat <<INSTRUCTION3
     114
     115
     116 Hitting just ENTER for a word entry ends the game.
     117
     118 Individual word entry is timed to a maximum of 10 seconds.
     119 *** Timing out on an entry ends the game. ***
     120 Aside from that, the game is untimed.
     121
     122 --------------------------------------------------
     123 Game statistics are automatically saved to a file.
     124 --------------------------------------------------
     125
     126 For competitive ("duplicate") play, a previous letterset
     127 may be duplicated by repeating the script's random seed,
     128 command-line parameter \$1.
     129 For example, "qky 7633" specifies the letterset
     130 c a d i f r h u s k ...
     131 INSTRUCTION3
     132
     133   echo; echo -n "Hit ENTER to begin game. "; read az1
     134
     135        echo -e "\033[0m"    # Turn off red.
     136      else clear
     137   fi
     138
     139   clear
     140
     141 }
     142
     143
     144
     145 seed_random ()
     146 {                         #  Seed random number generator.
     147   if [ -n "$randseed" ]   #  Can specify random seed.
     148   then                    #+ for play in competitive mode.
     149 #   RANDOM="$randseed"
     150     echo "RANDOM seed set to "$randseed""
     151   else
     152     randseed="$$"         # Or get random seed from process ID.
     153     echo "RANDOM seed not specified, set to Process ID of script ($$)."
     154   fi
     155
     156   RANDOM="$randseed"
     157
     158   echo
     159 }
     160
     161
     162 get_letset ()
     163 {
     164   element=0
     165   echo -n "Letterset:"
     166
     167   for lset in $(seq $NVLET)
     168   do  # Pick random letters to fill out letterset.
     169     LS[element]="${letters[$((RANDOM%numelements))]}"
     170     ((element++))
     171   done
     172
     173   echo
     174   echo "${LS[@]}"
     175
     176 }
     177
     178
     179 add_word ()
     180 {
     181   wrd="$1"
     182   local idx=0
     183
     184   Status[0]=""
     185   Status[3]=""
     186   Status[4]=""
     187
     188   while [ "${Words[idx]}" != '' ]
     189   do
     190     if [ "${Words[idx]}" = "$wrd" ]
     191     then
     192       Status[3]="Duplicate-word-PENALTY"
     193       let "Score[0]= 0 - $PENALTY"
     194       let "Score[1]-=$PENALTY"
     195       return $E_DUP
     196     fi
     197
     198     ((idx++))
     199   done
     200
     201   Words[idx]="$wrd"
     202   get_score
     203
     204 }
     205
     206 get_score()
     207 {
     208   local wlen=0
     209   local score=0
     210   local bonus=0
     211   local first_word=0
     212   local add_word=0
     213   local numwords=0
     214
     215   wlen=${#wrd}
     216   numwords=${Score[wlen]}
     217   Score[2]=0
     218   Status[4]=""   # Initialize "bonus" to 0.
     219
     220   case "$wlen" in
     221     3) first_word=60
     222        add_word=10;;
     223     4) first_word=120
     224        add_word=20;;
     225     5) first_word=200
     226        add_word=50;;
     227     6) first_word=300
     228        add_word=100;;
     229     7) first_word=500
     230        add_word=150;;
     231     8) first_word=750
     232        add_word=250;;
     233     9) first_word=1000
     234        add_word=500;;
     235    10) first_word=2000
     236        add_word=2000;;   # This category modified from original rules!
     237       esac
     238
     239   ((Score[wlen]++))
     240   if [ ${Score[wlen]} -eq $MAXCAT ]
     241   then   # Category completion bonus scoring simplified!
     242     case $wlen in
     243       3 ) bonus=100;;
     244       4 ) bonus=200;;
     245       5 ) bonus=400;;
     246       6 ) bonus=800;;
     247       7 ) bonus=2000;;
     248       8 ) bonus=10000;;
     249     esac  # Needn't worry about 9's and 10's.
     250     Status[4]="Category-$wlen-completion***BONUS***"
     251     Score[2]=$bonus
     252   else
     253     Status[4]=""   # Erase it.
     254   fi
     255
     256
     257     let "score =  $first_word +   $add_word * $numwords"
     258     if [ "$numwords" -eq 0 ]
     259     then
     260       Score[0]=$score
     261     else
     262       Score[0]=$add_word
     263     fi   #  All this to distinguish last-word score
     264          #+ from total running score.
     265   let "Score[1] += ${Score[0]}"
     266   let "Score[1] += ${Score[2]}"
     267
     268 }
     269
     270
     271
     272 get_word ()
     273 {
     274   local wrd=''
     275   read -t $TIMEOUT wrd   # Timed read.
     276   echo $wrd
     277 }
     278
     279 is_constructable ()
     280 { # This is the most complex and difficult-to-write function.
     281   local -a local_LS=( "${LS[@]}" )  # Local copy of letter set.
     282   local is_found=0
     283   local idx=0
     284   local pos
     285   local strlen
     286   local local_word=( "$1" )
     287   strlen=${#local_word}
     288
     289   while [ "$idx" -lt "$strlen" ]
     290   do
     291     is_found=$(expr index "${local_LS[*]}" "${local_word:idx:1}")
     292     if [ "$is_found" -eq "$NONCONS" ] # Not constructable!
     293     then
     294       echo "$FAILURE"; return
     295     else
     296       ((pos = ($is_found - 1) / 2))   # Compensate for spaces betw. letters!
     297       local_LS[pos]=$NULL             # Zero out used letters.
     298       ((idx++))                       # Bump index.
     299     fi
     300   done
     301
     302   echo "$SUCCESS"
     303   return
     304 }
     305
     306 is_valid ()
     307 { # Surprisingly easy to check if word in dictionary ...
     308   fgrep -qw "$1" "$WLIST"   # ... courtesy of 'grep' ...
     309   echo $?
     310 }
     311
     312 check_word ()
     313 {
     314   if [ -z "$1" ]
     315   then
     316     return
     317   fi
     318
     319   Status[1]=""
     320   Status[2]=""
     321   Status[3]=""
     322   Status[4]=""
     323
     324   iscons=$(is_constructable "$1")
     325   if [ "$iscons" ]
     326   then
     327     Status[1]="constructable"
     328     v=$(is_valid "$1")
     329     if [ "$v" -eq "$SUCCESS" ]
     330     then
     331       Status[2]="valid"
     332       strlen=${#1}
     333
     334       if [ ${Score[strlen]} -eq "$MAXCAT" ]   # Category full!
     335       then
     336         Status[3]="Category-$strlen-overflow-PENALTY"
     337         return $NG
     338       fi
     339
     340       case "$strlen" in
     341         1 | 2 )
     342         Status[3]="Two-letter-word-PENALTY"
     343         return $NG;;
     344         * )
     345 	Status[3]=""
     346 	return $SUCCESS;;
     347       esac
     348     else
     349       Status[3]="Not-valid-PENALTY"
     350       return $NG
     351     fi
     352   else
     353     Status[3]="Not-constructable-PENALTY"
     354       return $NG
     355   fi
     356
     357   ### FIXME: Streamline the above code block.
     358
     359 }
     360
     361
     362 display_words ()
     363 {
     364   local idx=0
     365   local wlen0
     366
     367   clear
     368   echo "Letterset:   ${LS[@]}"
     369   echo "Threes:    Fours:    Fives:     Sixes:    Sevens:    Eights:"
     370   echo "------------------------------------------------------------"
     371
     372
     373
     374   while [ "${Words[idx]}" != '' ]
     375   do
     376    wlen0=${#Words[idx]}
     377    case "$wlen0" in
     378      3) ;;
     379      4) echo -n "           " ;;
     380      5) echo -n "                     " ;;
     381      6) echo -n "                                " ;;
     382      7) echo -n "                                          " ;;
     383      8) echo -n "                                                     " ;;
     384    esac
     385    echo "${Words[idx]}"
     386    ((idx++))
     387   done
     388
     389   ### FIXME: The word display is pretty crude.
     390 }
     391
     392
     393 play ()
     394 {
     395   word="Start game"   # Dummy word, to start ...
     396
     397   while [ "$word" ]   #  If player just hits return (null word),
     398   do                  #+ then game ends.
     399     echo "$word: "${Status[@]}""
     400     echo -n "Last score: [${Score[0]}]   TOTAL score: [${Score[1]}]:     Next word: "
     401     total=${Score[1]}
     402     word=$(get_word)
     403     check_word "$word"
     404
     405     if [ "$?" -eq "$SUCCESS" ]
     406     then
     407       add_word "$word"
     408     else
     409       let "Score[0]= 0 - $PENALTY"
     410       let "Score[1]-=$PENALTY"
     411     fi
     412
     413   display_words
     414   done   # Exit game.
     415
     416   ### FIXME: The play () function calls too many other functions.
     417   ### This verges on "spaghetti code" !!!
     418 }
     419
     420 end_of_game ()
     421 { # Save and display stats.
     422
     423   #######################Autosave##########################
     424   savefile=qky.save.$$
     425   #                 ^^ PID of script
     426   echo `date` >> $savefile
     427   echo "Letterset # $randseed  (random seed) ">> $savefile
     428   echo -n "Letterset: " >> $savefile
     429   echo "${LS[@]}" >> $savefile
     430   echo "---------" >> $savefile
     431   echo "Words constructed:" >> $savefile
     432   echo "${Words[@]}" >> $savefile
     433   echo >> $savefile
     434   echo "Score: $total" >> $savefile
     435
     436   echo "Statistics for this round saved in \""$savefile"\""
     437   #########################################################
     438
     439   echo "Score for this round: $total"
     440   echo "Words:  ${Words[@]}"
     441 }
     442
     443 # ---------#
     444 instructions
     445 seed_random
     446 get_letset
     447 play
     448 end_of_game
     449 # ---------#
     450
     451 exit $?
     452
     453 # TODO:
     454 #
     455 # 1) Clean up code!
     456 # 2) Prettify the display_words () function (maybe with widgets?).
     457 # 3) Improve the time-out ... maybe change to untimed entry,
     458 #+   but with a time limit for the overall round.
     459 # 4) An on-screen countdown timer would be nice.
     460 # 5) Implement "vulnerable" mode of play for compatibility with classic
     461 #+   version of the game.
     462 # 6) Improve save-to-file capability (and maybe make it optional).
     463 # 7) Fix bugs!!!
     464
     465 # For more info, reference:
     466 # http://bash.deta.in/qky.README.html

Example A-42. Nim

       1 #!/bin/bash
       2 # nim.sh: Game of Nim
       3
       4 # Author: Mendel Cooper
       5 # Reldate: 15 July 2008
       6 # License: GPL3
       7
       8 ROWS=5     # Five rows of pegs (or matchsticks).
       9 WON=91     # Exit codes to keep track of wins/losses.
      10 LOST=92    # Possibly useful if running in batch mode.
      11 QUIT=99
      12 peg_msg=   # Peg/Pegs?
      13 Rows=( 0 5 4 3 2 1 )   # Array holding play info.
      14 # ${Rows[0]} holds total number of pegs, updated after each turn.
      15 # Other array elements hold number of pegs in corresponding row.
      16
      17 instructions ()
      18 {
      19   clear
      20   tput bold
      21   echo "Welcome to the game of Nim."; echo
      22   echo -n "Do you need instructions? (y/n) "; read ans
      23
      24    if [ "$ans" = "y" -o "$ans" = "Y" ]; then
      25      clear
      26      echo -e '\E[33;41m'  # Yellow fg., over red bg.; bold.
      27      cat <<INSTRUCTIONS
      28
      29 Nim is a game with roots in the distant past.
      30 This particular variant starts with five rows of pegs.
      31
      32 1:    | | | | |
      33 2:     | | | |
      34 3:      | | |
      35 4:       | |
      36 5:        |
      37
      38 The number at the left identifies the row.
      39
      40 The human player moves first, and alternates turns with the bot.
      41 A turn consists of removing at least one peg from a single row.
      42 It is permissable to remove ALL the pegs from a row.
      43 For example, in row 2, above, the player can remove 1, 2, 3, or 4 pegs.
      44 The player who removes the last peg loses.
      45
      46 The strategy consists of trying to be the one who removes
      47 the next-to-last peg(s), leaving the loser with the final peg.
      48
      49 To exit the game early, hit ENTER during your turn.
      50 INSTRUCTIONS
      51
      52 echo; echo -n "Hit ENTER to begin game. "; read azx
      53
      54       echo -e "\033[0m"    # Restore display.
      55       else tput sgr0; clear
      56   fi
      57
      58 clear
      59
      60 }
      61
      62
      63 tally_up ()
      64 {
      65   let "Rows[0] = ${Rows[1]} + ${Rows[2]} + ${Rows[3]} + ${Rows[4]} + \
      66   ${Rows[5]}"    # Add up how many pegs remaining.
      67 }
      68
      69
      70 display ()
      71 {
      72   index=1   # Start with top row.
      73   echo
      74
      75   while [ "$index" -le "$ROWS" ]
      76   do
      77     p=${Rows[index]}
      78     echo -n "$index:   "          # Show row number.
      79
      80   # ------------------------------------------------
      81   # Two concurrent inner loops.
      82
      83       indent=$index
      84       while [ "$indent" -gt 0 ]
      85       do
      86         echo -n " "               # Staggered rows.
      87         ((indent--))              # Spacing between pegs.
      88       done
      89
      90     while [ "$p" -gt 0 ]
      91     do
      92       echo -n "| "
      93       ((p--))
      94     done
      95   # -----------------------------------------------
      96
      97   echo
      98   ((index++))
      99   done
     100
     101   tally_up
     102
     103   rp=${Rows[0]}
     104
     105   if [ "$rp" -eq 1 ]
     106   then
     107     peg_msg=peg
     108     final_msg="Game over."
     109   else             # Game not yet over . . .
     110     peg_msg=pegs
     111     final_msg=""   # . . . So "final message" is blank.
     112   fi
     113
     114   echo "      $rp $peg_msg remaining."
     115   echo "      "$final_msg""
     116
     117
     118   echo
     119 }
     120
     121 player_move ()
     122 {
     123
     124   echo "Your move:"
     125
     126   echo -n "Which row? "
     127   while read idx
     128   do                   # Validity check, etc.
     129
     130     if [ -z "$idx" ]   # Hitting return quits.
     131     then
     132         echo "Premature exit."; echo
     133         tput sgr0      # Restore display.
     134         exit $QUIT
     135     fi
     136
     137     if [ "$idx" -gt "$ROWS" -o "$idx" -lt 1 ]   # Bounds check.
     138     then
     139       echo "Invalid row number!"
     140       echo -n "Which row? "
     141     else
     142       break
     143     fi
     144     # TODO:
     145     # Add check for non-numeric input.
     146     # Also, script crashes on input outside of range of long double.
     147     # Fix this.
     148
     149   done
     150
     151   echo -n "Remove how many? "
     152   while read num
     153   do                   # Validity check.
     154
     155   if [ -z "$num" ]
     156   then
     157     echo "Premature exit."; echo
     158     tput sgr0          # Restore display.
     159     exit $QUIT
     160   fi
     161
     162     if [ "$num" -gt ${Rows[idx]} -o "$num" -lt 1 ]
     163     then
     164       echo "Cannot remove $num!"
     165       echo -n "Remove how many? "
     166     else
     167       break
     168     fi
     169   done
     170   # TODO:
     171   # Add check for non-numeric input.
     172   # Also, script crashes on input outside of range of long double.
     173   # Fix this.
     174
     175   let "Rows[idx] -= $num"
     176
     177   display
     178   tally_up
     179
     180   if [ ${Rows[0]} -eq 1 ]
     181   then
     182    echo "      Human wins!"
     183    echo "      Congratulations!"
     184    tput sgr0   # Restore display.
     185    echo
     186    exit $WON
     187   fi
     188
     189   if [ ${Rows[0]} -eq 0 ]
     190   then          # Snatching defeat from the jaws of victory . . .
     191     echo "      Fool!"
     192     echo "      You just removed the last peg!"
     193     echo "      Bot wins!"
     194     tput sgr0   # Restore display.
     195     echo
     196     exit $LOST
     197   fi
     198 }
     199
     200
     201 bot_move ()
     202 {
     203
     204   row_b=0
     205   while [[ $row_b -eq 0 || ${Rows[row_b]} -eq 0 ]]
     206   do
     207     row_b=$RANDOM          # Choose random row.
     208     let "row_b %= $ROWS"
     209   done
     210
     211
     212   num_b=0
     213   r0=${Rows[row_b]}
     214
     215   if [ "$r0" -eq 1 ]
     216   then
     217     num_b=1
     218   else
     219     let "num_b = $r0 - 1"
     220          #  Leave only a single peg in the row.
     221   fi     #  Not a very strong strategy,
     222          #+ but probably a bit better than totally random.
     223
     224   let "Rows[row_b] -= $num_b"
     225   echo -n "Bot:  "
     226   echo "Removing from row $row_b ... "
     227
     228   if [ "$num_b" -eq 1 ]
     229   then
     230     peg_msg=peg
     231   else
     232     peg_msg=pegs
     233   fi
     234
     235   echo "      $num_b $peg_msg."
     236
     237   display
     238   tally_up
     239
     240   if [ ${Rows[0]} -eq 1 ]
     241   then
     242    echo "      Bot wins!"
     243    tput sgr0   # Restore display.
     244    exit $WON
     245   fi
     246
     247 }
     248
     249
     250 # ================================================== #
     251 instructions     # If human player needs them . . .
     252 tput bold        # Bold characters for easier viewing.
     253 display          # Show game board.
     254
     255 while [ true ]   # Main loop.
     256 do               # Alternate human and bot turns.
     257   player_move
     258   bot_move
     259 done
     260 # ================================================== #
     261
     262 # Exercise:
     263 # --------
     264 # Improve the bot's strategy.
     265 # There is, in fact, a Nim strategy that can force a win.
     266 # See the Wikipedia article on Nim:  http://en.wikipedia.org/wiki/Nim
     267 # Recode the bot to use this strategy (rather difficult).
     268
     269 #  Curiosities:
     270 #  -----------
     271 #  Nim played a prominent role in Alain Resnais' 1961 New Wave film,
     272 #+ Last Year at Marienbad.
     273 #
     274 #  In 1978, Leo Christopherson wrote an animated version of Nim,
     275 #+ Android Nim, for the TRS-80 Model I.

Example A-43. A command-line stopwatch

       1 #!/bin/sh
       2 # sw.sh
       3 # A command-line Stopwatch
       4
       5 # Author: Pádraig Brady
       6 #    http://www.pixelbeat.org/scripts/sw
       7 #    (Minor reformatting by ABS Guide author.)
       8 #    Used in ABS Guide with script author's permission.
       9 # Notes:
      10 #    This script starts a few processes per lap, in addition to
      11 #    the shell loop processing, so the assumption is made that
      12 #    this takes an insignificant amount of time compared to
      13 #    the response time of humans (~.1s) (or the keyboard
      14 #    interrupt rate (~.05s)).
      15 #    '?' for splits must be entered twice if characters
      16 #    (erroneously) entered before it (on the same line).
      17 #    '?' since not generating a signal may be slightly delayed
      18 #    on heavily loaded systems.
      19 #    Lap timings on ubuntu may be slightly delayed due to:
      20 #    https://bugs.launchpad.net/bugs/62511
      21 # Changes:
      22 #    V1.0, 23 Aug 2005, Initial release
      23 #    V1.1, 26 Jul 2007, Allow both splits and laps from single invocation.
      24 #                       Only start timer after a key is pressed.
      25 #                       Indicate lap number
      26 #                       Cache programs at startup so there is less error
      27 #                       due to startup delays.
      28 #    V1.2, 01 Aug 2007, Work around `date` commands that don't have
      29 #                       nanoseconds.
      30 #                       Use stty to change interrupt keys to space for
      31 #                       laps etc.
      32 #                       Ignore other input as it causes problems.
      33 #    V1.3, 01 Aug 2007, Testing release.
      34 #    V1.4, 02 Aug 2007, Various tweaks to get working under ubuntu
      35 #                       and Mac OS X.
      36 #    V1.5, 27 Jun 2008, set LANG=C as got vague bug report about it.
      37
      38 export LANG=C
      39
      40 ulimit -c 0   # No coredumps from SIGQUIT.
      41 trap '' TSTP  # Ignore Ctrl-Z just in case.
      42 save_tty=`stty -g` && trap "stty $save_tty" EXIT  # Restore tty on exit.
      43 stty quit ' ' # Space for laps rather than Ctrl-\.
      44 stty eof  '?' # ? for splits rather than Ctrl-D.
      45 stty -echo    # Don't echo input.
      46
      47 cache_progs() {
      48     stty > /dev/null
      49     date > /dev/null
      50     grep . < /dev/null
      51     (echo "import time" | python) 2> /dev/null
      52     bc < /dev/null
      53     sed '' < /dev/null
      54     printf '1' > /dev/null
      55     /usr/bin/time false 2> /dev/null
      56     cat < /dev/null
      57 }
      58 cache_progs   # To minimise startup delay.
      59
      60 date +%s.%N | grep -qF 'N' && use_python=1 # If `date` lacks nanoseconds.
      61 now() {
      62     if [ "$use_python" ]; then
      63         echo "import time; print time.time()" 2>/dev/null | python
      64     else
      65         printf "%.2f" `date +%s.%N`
      66     fi
      67 }
      68
      69 fmt_seconds() {
      70     seconds=$1
      71     mins=`echo $seconds/60 | bc`
      72     if [ "$mins" != "0" ]; then
      73         seconds=`echo "$seconds - ($mins*60)" | bc`
      74         echo "$mins:$seconds"
      75     else
      76         echo "$seconds"
      77     fi
      78 }
      79
      80 total() {
      81     end=`now`
      82     total=`echo "$end - $start" | bc`
      83     fmt_seconds $total
      84 }
      85
      86 stop() {
      87     [ "$lapped" ] && lap "$laptime" "display"
      88     total
      89     exit
      90 }
      91
      92 lap() {
      93     laptime=`echo "$1" | sed -n 's/.*real[^0-9.]*\(.*\)/\1/p'`
      94     [ ! "$laptime" -o "$laptime" = "0.00" ] && return
      95     # Signals too frequent.
      96     laptotal=`echo $laptime+0$laptotal | bc`
      97     if [ "$2" = "display" ]; then
      98         lapcount=`echo 0$lapcount+1 | bc`
      99         laptime=`fmt_seconds $laptotal`
     100         echo $laptime "($lapcount)"
     101         lapped="true"
     102         laptotal="0"
     103     fi
     104 }
     105
     106 echo -n "Space for lap | ? for split | Ctrl-C to stop | Space to start...">&2
     107
     108 while true; do
     109     trap true INT QUIT  # Set signal handlers.
     110     laptime=`/usr/bin/time -p 2>&1 cat >/dev/null`
     111     ret=$?
     112     trap '' INT QUIT    # Ignore signals within this script.
     113     if [ $ret -eq 1 -o $ret -eq 2 -o $ret -eq 130 ]; then # SIGINT = stop
     114         [ ! "$start" ] && { echo >&2; exit; }
     115         stop
     116     elif [ $ret -eq 3 -o $ret -eq 131 ]; then             # SIGQUIT = lap
     117         if [ ! "$start" ]; then
     118             start=`now` || exit 1
     119             echo >&2
     120             continue
     121         fi
     122         lap "$laptime" "display"
     123     else                # eof = split
     124         [ ! "$start" ] && continue
     125         total
     126         lap "$laptime"  # Update laptotal.
     127     fi
     128 done
     129
     130 exit $?

Example A-44. An all-purpose shell scripting homework assignment solution

       1 #!/bin/bash
       2 #  homework.sh: All-purpose homework assignment solution.
       3 #  Author: M. Leo Cooper
       4 #  If you substitute your own name as author, then it is plagiarism,
       5 #+ possibly a lesser sin than cheating on your homework!
       6 #  License: Public Domain
       7
       8 #  This script may be turned in to your instructor
       9 #+ in fulfillment of ALL shell scripting homework assignments.
      10 #  It's sparsely commented, but you, the student, can easily remedy that.
      11 #  The script author repudiates all responsibility!
      12
      13 DLA=1
      14 P1=2
      15 P2=4
      16 P3=7
      17 PP1=0
      18 PP2=8
      19 MAXL=9
      20 E_LZY=99
      21
      22 declare -a L
      23 L[0]="3 4 0 17 29 8 13 18 19 17 20 2 19 14 17 28"
      24 L[1]="8 29 12 14 18 19 29 4 12 15 7 0 19 8 2 0 11 11 24 29 17 4 6 17 4 19"
      25 L[2]="29 19 7 0 19 29 8 29 7 0 21 4 29 13 4 6 11 4 2 19 4 3"
      26 L[3]="19 14 29 2 14 12 15 11 4 19 4 29 19 7 8 18 29"
      27 L[4]="18 2 7 14 14 11 22 14 17 10 29 0 18 18 8 6 13 12 4 13 19 26"
      28 L[5]="15 11 4 0 18 4 29 0 2 2 4 15 19 29 12 24 29 7 20 12 1 11 4 29"
      29 L[6]="4 23 2 20 18 4 29 14 5 29 4 6 17 4 6 8 14 20 18 29"
      30 L[7]="11 0 25 8 13 4 18 18 27"
      31 L[8]="0 13 3 29 6 17 0 3 4 29 12 4 29 0 2 2 14 17 3 8 13 6 11 24 26"
      32 L[9]="19 7 0 13 10 29 24 14 20 26"
      33
      34 declare -a \
      35 alph=( A B C D E F G H I J K L M N O P Q R S T U V W X Y Z . , : ' ' )
      36
      37
      38 pt_lt ()
      39 {
      40   echo -n "${alph[$1]}"
      41   echo -n -e "\a"
      42   sleep $DLA
      43 }
      44
      45 b_r ()
      46 {
      47  echo -e '\E[31;48m\033[1m'
      48 }
      49
      50 cr ()
      51 {
      52  echo -e "\a"
      53  sleep $DLA
      54 }
      55
      56 restore ()
      57 {
      58   echo -e '\033[0m'            # Bold off.
      59   tput sgr0                    # Normal.
      60 }
      61
      62
      63 p_l ()
      64 {
      65   for ltr in $1
      66   do
      67     pt_lt "$ltr"
      68   done
      69 }
      70
      71 # ----------------------
      72 b_r
      73
      74 for i in $(seq 0 $MAXL)
      75 do
      76   p_l "${L[i]}"
      77   if [[ "$i" -eq "$P1" || "$i" -eq "$P2" || "$i" -eq "$P3" ]]
      78   then
      79     cr
      80   elif [[ "$i" -eq "$PP1" || "$i" -eq "$PP2" ]]
      81   then
      82     cr; cr
      83   fi
      84 done
      85
      86 restore
      87 # ----------------------
      88
      89 echo
      90
      91 exit $E_LZY
      92
      93 #  A typical example of an obfuscated script that is difficult
      94 #+ to understand, and frustrating to maintain.
      95 #  In your career as a sysadmin, you'll run into these critters
      96 #+ all too often.

Example A-45. The Knight's Tour

       1 #!/bin/bash
       2 # ktour.sh
       3
       4 # author: mendel cooper
       5 # reldate: 12 Jan 2009
       6 # license: public domain
       7 # (Not much sense GPLing something that's pretty much in the common
       8 #+ domain anyhow.)
       9
      10 ###################################################################
      11 #             The Knight's Tour, a classic problem.               #
      12 #             =====================================               #
      13 #  The knight must move onto every square of the chess board,     #
      14 #  but cannot revisit any square he has already visited.          #
      15 #                                                                 #
      16 #  And just why is Sir Knight unwelcome for a return visit?       #
      17 #  Could it be that he has a habit of partying into the wee hours #
      18 #+ of the morning?                                                #
      19 #  Possibly he leaves pizza crusts in the bed, empty beer bottles #
      20 #+ all over the floor, and clogs the plumbing. . . .              #
      21 #                                                                 #
      22 #  -------------------------------------------------------------  #
      23 #                                                                 #
      24 #  Usage: ktour.sh [start-square] [stupid]                        #
      25 #                                                                 #
      26 #  Note that start-square can be a square number                  #
      27 #+ in the range 0 - 63 ... or                                     #
      28 #  a square designator in conventional chess notation,            #
      29 #  such as a1, f5, h3, etc.                                       #
      30 #                                                                 #
      31 #  If start-square-number not supplied,                           #
      32 #+ then starts on a random square somewhere on the board.         #
      33 #                                                                 #
      34 # "stupid" as second parameter sets the stupid strategy.          #
      35 #                                                                 #
      36 #  Examples:                                                      #
      37 #  ktour.sh 23          starts on square #23 (h3)                 #
      38 #  ktour.sh g6 stupid   starts on square #46,                     #
      39 #                       using "stupid" (non-Warnsdorff) strategy. #
      40 ###################################################################
      41
      42 DEBUG=      # Set this to echo debugging info to stdout.
      43 SUCCESS=0
      44 FAIL=99
      45 BADMOVE=-999
      46 FAILURE=1
      47 LINELEN=21  # How many moves to display per line.
      48 # ---------------------------------------- #
      49 # Board array params
      50 ROWS=8   # 8 x 8 board.
      51 COLS=8
      52 let "SQUARES = $ROWS * $COLS"
      53 let "MAX = $SQUARES - 1"
      54 MIN=0
      55 # 64 squares on board, indexed from 0 to 63.
      56
      57 VISITED=1
      58 UNVISITED=-1
      59 UNVSYM="##"
      60 # ---------------------------------------- #
      61 # Global variables.
      62 startpos=    # Starting position (square #, 0 - 63).
      63 currpos=     # Current position.
      64 movenum=     # Move number.
      65 CRITPOS=37   # Have to patch for f5 starting position!
      66
      67 declare -i board
      68 # Use a one-dimensional array to simulate a two-dimensional one.
      69 # This can make life difficult and result in ugly kludges; see below.
      70 declare -i moves  # Offsets from current knight position.
      71
      72
      73 initialize_board ()
      74 {
      75   local idx
      76
      77   for idx in {0..63}
      78   do
      79     board[$idx]=$UNVISITED
      80   done
      81 }
      82
      83
      84
      85 print_board ()
      86 {
      87   local idx
      88
      89   echo "    _____________________________________"
      90   for row in {7..0}               #  Reverse order of rows ...
      91   do                              #+ so it prints in chessboard order.
      92     let "rownum = $row + 1"       #  Start numbering rows at 1.
      93     echo -n "$rownum  |"          #  Mark board edge with border and
      94     for column in {0..7}          #+ "algebraic notation."
      95     do
      96       let "idx = $ROWS*$row + $column"
      97       if [ ${board[idx]} -eq $UNVISITED ]
      98       then
      99         echo -n "$UNVSYM   "      ##
     100       else                        # Mark square with move number.
     101         printf "%02d " "${board[idx]}"; echo -n "  "
     102       fi
     103     done
     104     echo -e -n "\b\b\b|"  # \b is a backspace.
     105     echo                  # -e enables echoing escaped chars.
     106   done
     107
     108   echo "    -------------------------------------"
     109   echo "     a    b    c    d    e    f    g    h"
     110 }
     111
     112
     113
     114 failure()
     115 { # Whine, then bail out.
     116   echo
     117   print_board
     118   echo
     119   echo    "   Waah!!! Ran out of squares to move to!"
     120   echo -n "   Knight's Tour attempt ended"
     121   echo    " on $(to_algebraic $currpos) [square #$currpos]"
     122   echo    "   after just $movenum moves!"
     123   echo
     124   exit $FAIL
     125 }
     126
     127
     128
     129 xlat_coords ()   #  Translate x/y coordinates to board position
     130 {                #+ (board-array element #).
     131   #  For user input of starting board position as x/y coords.
     132   #  This function not used in initial release of ktour.sh.
     133   #  May be used in an updated version, for compatibility with
     134   #+ standard implementation of the Knight's Tour in C, Python, etc.
     135   if [ -z "$1" -o -z "$2" ]
     136   then
     137     return $FAIL
     138   fi
     139
     140   local xc=$1
     141   local yc=$2
     142
     143   let "board_index = $xc * $ROWS + yc"
     144
     145   if [ $board_index -lt $MIN -o $board_index -gt $MAX ]
     146   then
     147     return $FAIL    # Strayed off the board!
     148   else
     149     return $board_index
     150   fi
     151 }
     152
     153
     154
     155 to_algebraic ()   #  Translate board position (board-array element #)
     156 {                 #+ to standard algebraic notation used by chess players.
     157   if [ -z "$1" ]
     158   then
     159     return $FAIL
     160   fi
     161
     162   local element_no=$1   # Numerical board position.
     163   local col_arr=( a b c d e f g h )
     164   local row_arr=( 1 2 3 4 5 6 7 8 )
     165
     166   let "row_no = $element_no / $ROWS"
     167   let "col_no = $element_no % $ROWS"
     168   t1=${col_arr[col_no]}; t2=${row_arr[row_no]}
     169   local apos=$t1$t2   # Concatenate.
     170   echo $apos
     171 }
     172
     173
     174
     175 from_algebraic ()   #  Translate standard algebraic chess notation
     176 {                   #+ to numerical board position (board-array element #).
     177                     #  Or recognize numerical input & return it unchanged.
     178   if [ -z "$1" ]
     179   then
     180     return $FAIL
     181   fi   # If no command-line arg, then will default to random start pos.
     182
     183   local ix
     184   local ix_count=0
     185   local b_index     # Board index [0-63]
     186   local alpos="$1"
     187
     188   arow=${alpos:0:1} # position = 0, length = 1
     189   acol=${alpos:1:1}
     190
     191   if [[ $arow =~ [[:digit:]] ]]   #  Numerical input?
     192   then       #  POSIX char class
     193     if [[ $acol =~ [[:alpha:]] ]] # Number followed by a letter? Illegal!
     194       then return $FAIL
     195     else if [ $alpos -gt $MAX ]   # Off board?
     196       then return $FAIL
     197     else return $alpos            #  Return digit(s) unchanged . . .
     198       fi                          #+ if within range.
     199     fi
     200   fi
     201
     202   if [[ $acol -eq $MIN || $acol -gt $ROWS ]]
     203   then        # Outside of range 1 - 8?
     204     return $FAIL
     205   fi
     206
     207   for ix in a b c d e f g h
     208   do  # Convert column letter to column number.
     209    if [ "$arow" = "$ix" ]
     210    then
     211      break
     212    fi
     213   ((ix_count++))    # Find index count.
     214   done
     215
     216   ((acol--))        # Decrementing converts to zero-based array.
     217   let "b_index = $ix_count + $acol * $ROWS"
     218
     219   if [ $b_index -gt $MAX ]   # Off board?
     220   then
     221     return $FAIL
     222   fi
     223
     224   return $b_index
     225
     226 }
     227
     228
     229 generate_moves ()   #  Calculate all valid knight moves,
     230 {                   #+ relative to current position ($1),
     231                     #+ and store in ${moves} array.
     232   local kt_hop=1    #  One square  :: short leg of knight move.
     233   local kt_skip=2   #  Two squares :: long leg  of knight move.
     234   local valmov=0    #  Valid moves.
     235   local row_pos; let "row_pos = $1 % $COLS"
     236
     237
     238   let "move1 = -$kt_skip + $ROWS"           # 2 sideways to-the-left,  1 up
     239     if [[ `expr $row_pos - $kt_skip` -lt $MIN ]]   # An ugly, ugly kludge!
     240     then                                           # Can't move off board.
     241       move1=$BADMOVE                               # Not even temporarily.
     242     else
     243       ((valmov++))
     244     fi
     245   let "move2 = -$kt_hop + $kt_skip * $ROWS" # 1 sideways to-the-left,  2 up
     246     if [[ `expr $row_pos - $kt_hop` -lt $MIN ]]    # Kludge continued ...
     247     then
     248       move2=$BADMOVE
     249     else
     250       ((valmov++))
     251     fi
     252   let "move3 =  $kt_hop + $kt_skip * $ROWS" # 1 sideways to-the-right, 2 up
     253     if [[ `expr $row_pos + $kt_hop` -ge $COLS ]]
     254     then
     255       move3=$BADMOVE
     256     else
     257       ((valmov++))
     258     fi
     259   let "move4 =  $kt_skip + $ROWS"           # 2 sideways to-the-right, 1 up
     260     if [[ `expr $row_pos + $kt_skip` -ge $COLS ]]
     261     then
     262       move4=$BADMOVE
     263     else
     264       ((valmov++))
     265     fi
     266   let "move5 =  $kt_skip - $ROWS"           # 2 sideways to-the-right, 1 dn
     267     if [[ `expr $row_pos + $kt_skip` -ge $COLS ]]
     268     then
     269       move5=$BADMOVE
     270     else
     271       ((valmov++))
     272     fi
     273   let "move6 =  $kt_hop - $kt_skip * $ROWS" # 1 sideways to-the-right, 2 dn
     274     if [[ `expr $row_pos + $kt_hop` -ge $COLS ]]
     275     then
     276       move6=$BADMOVE
     277     else
     278       ((valmov++))
     279     fi
     280   let "move7 = -$kt_hop - $kt_skip * $ROWS" # 1 sideways to-the-left,  2 dn
     281     if [[ `expr $row_pos - $kt_hop` -lt $MIN ]]
     282     then
     283       move7=$BADMOVE
     284     else
     285       ((valmov++))
     286     fi
     287   let "move8 = -$kt_skip - $ROWS"           # 2 sideways to-the-left,  1 dn
     288     if [[ `expr $row_pos - $kt_skip` -lt $MIN ]]
     289     then
     290       move8=$BADMOVE
     291     else
     292       ((valmov++))
     293     fi   # There must be a better way to do this.
     294
     295   local m=( $valmov $move1 $move2 $move3 $move4 $move5 $move6 $move7 $move8 )
     296   # ${moves[0]} = number of valid moves.
     297   # ${moves[1]} ... ${moves[8]} = possible moves.
     298   echo "${m[*]}"    # Elements of array to stdout for capture in a var.
     299
     300 }
     301
     302
     303
     304 is_on_board ()  # Is position actually on the board?
     305 {
     306   if [[ "$1" -lt "$MIN" || "$1" -gt "$MAX" ]]
     307   then
     308     return $FAILURE
     309   else
     310     return $SUCCESS
     311   fi
     312 }
     313
     314
     315
     316 do_move ()      # Move the knight!
     317 {
     318   local valid_moves=0
     319   local aapos
     320   currposl="$1"
     321   lmin=$ROWS
     322   iex=0
     323   squarel=
     324   mpm=
     325   mov=
     326   declare -a p_moves
     327
     328   ########################## DECIDE-MOVE #############################
     329   if [ $startpos -ne $CRITPOS ]
     330   then   # CRITPOS = square #37
     331     decide_move
     332   else                     # Needs a special patch for startpos=37 !!!
     333     decide_move_patched    # Why this particular move and no other ???
     334   fi
     335   ####################################################################
     336
     337   (( ++movenum ))          # Increment move count.
     338   let "square = $currposl + ${moves[iex]}"
     339
     340   ##################    DEBUG    ###############
     341   if [ "$DEBUG" ]
     342     then debug   # Echo debugging information.
     343   fi
     344   ##############################################
     345
     346   if [[ "$square" -gt $MAX || "$square" -lt $MIN ||
     347         ${board[square]} -ne $UNVISITED ]]
     348   then
     349     (( --movenum ))              #  Decrement move count,
     350     echo "RAN OUT OF SQUARES!!!" #+ since previous one was invalid.
     351     return $FAIL
     352   fi
     353
     354   board[square]=$movenum
     355   currpos=$square       # Update current position.
     356   ((valid_moves++));    # moves[0]=$valid_moves
     357   aapos=$(to_algebraic $square)
     358   echo -n "$aapos "
     359   test $(( $Moves % $LINELEN )) -eq 0 && echo
     360   # Print LINELEN=21 moves per line. A valid tour shows 3 complete lines.
     361   return $valid_moves   # Found a square to move to!
     362 }
     363
     364
     365
     366 do_move_stupid()   #  Dingbat algorithm,
     367 {                  #+ courtesy of script author, *not* Warnsdorff.
     368   local valid_moves=0
     369   local movloc
     370   local squareloc
     371   local aapos
     372   local cposloc="$1"
     373
     374   for movloc in {1..8}
     375   do       # Move to first-found unvisited square.
     376     let "squareloc = $cposloc + ${moves[movloc]}"
     377     is_on_board $squareloc
     378     if [ $? -eq $SUCCESS ] && [ ${board[squareloc]} -eq $UNVISITED ]
     379     then   # Add conditions to above if-test to improve algorithm.
     380       (( ++movenum ))
     381       board[squareloc]=$movenum
     382       currpos=$squareloc     # Update current position.
     383       ((valid_moves++));     # moves[0]=$valid_moves
     384       aapos=$(to_algebraic $squareloc)
     385       echo -n "$aapos "
     386       test $(( $Moves % $LINELEN )) -eq 0 && echo   # Print 21 moves/line.
     387       return $valid_moves    # Found a square to move to!
     388     fi
     389   done
     390
     391   return $FAIL
     392   #  If no square found in all 8 loop iterations,
     393   #+ then Knight's Tour attempt ends in failure.
     394
     395   #  Dingbat algorithm will typically fail after about 30 - 40 moves,
     396   #+ but executes _much_ faster than Warnsdorff's in do_move() function.
     397 }
     398
     399
     400
     401 decide_move ()         #  Which move will we make?
     402 {                      #  But, fails on startpos=37 !!!
     403   for mov in {1..8}
     404   do
     405     let "squarel = $currposl + ${moves[mov]}"
     406     is_on_board $squarel
     407     if [[ $? -eq $SUCCESS && ${board[squarel]} -eq $UNVISITED ]]
     408     then   #  Find accessible square with least possible future moves.
     409            #  This is Warnsdorff's algorithm.
     410            #  What happens is that the knight wanders toward the outer edge
     411            #+ of the board, then pretty much spirals inward.
     412            #  Given two or more possible moves with same value of
     413            #+ least-possible-future-moves, this implementation chooses
     414            #+ the _first_ of those moves.
     415            #  This means that there is not necessarily a unique solution
     416            #+ for any given starting position.
     417
     418       possible_moves $squarel
     419       mpm=$?
     420       p_moves[mov]=$mpm
     421
     422       if [ $mpm -lt $lmin ]  # If less than previous minimum ...
     423       then #     ^^
     424         lmin=$mpm            # Update minimum.
     425         iex=$mov             # Save index.
     426       fi
     427
     428     fi
     429   done
     430 }
     431
     432
     433
     434 decide_move_patched ()         #  Decide which move to make,
     435 {  #        ^^^^^^^            #+ but only if startpos=37 !!!
     436   for mov in {1..8}
     437   do
     438     let "squarel = $currposl + ${moves[mov]}"
     439     is_on_board $squarel
     440     if [[ $? -eq $SUCCESS && ${board[squarel]} -eq $UNVISITED ]]
     441     then
     442       possible_moves $squarel
     443       mpm=$?
     444       p_moves[mov]=$mpm
     445
     446       if [ $mpm -le $lmin ]  # If less-than-or equal to prev. minimum!
     447       then #     ^^
     448         lmin=$mpm
     449         iex=$mov
     450       fi
     451
     452     fi
     453   done                       # There has to be a better way to do this.
     454 }
     455
     456
     457
     458 possible_moves ()            #  Calculate number of possible moves,
     459 {                            #+ given the current position.
     460
     461   if [ -z "$1" ]
     462   then
     463     return $FAIL
     464   fi
     465
     466   local curr_pos=$1
     467   local valid_movl=0
     468   local icx=0
     469   local movl
     470   local sq
     471   declare -a movesloc
     472
     473   movesloc=( $(generate_moves $curr_pos) )
     474
     475   for movl in {1..8}
     476   do
     477     let "sq = $curr_pos + ${movesloc[movl]}"
     478     is_on_board $sq
     479     if [ $? -eq $SUCCESS ] && [ ${board[sq]} -eq $UNVISITED ]
     480     then
     481       ((valid_movl++));
     482     fi
     483   done
     484
     485   return $valid_movl         # Found a square to move to!
     486 }
     487
     488
     489 strategy ()
     490 {
     491   echo
     492
     493   if [ -n "$STUPID" ]
     494   then
     495     for Moves in {1..63}
     496     do
     497       cposl=$1
     498       moves=( $(generate_moves $currpos) )
     499       do_move_stupid "$currpos"
     500       if [ $? -eq $FAIL ]
     501       then
     502         failure
     503       fi
     504       done
     505   fi
     506
     507   #  Don't need an "else" clause here,
     508   #+ because Stupid Strategy will always fail and exit!
     509   for Moves in {1..63}
     510   do
     511     cposl=$1
     512     moves=( $(generate_moves $currpos) )
     513     do_move "$currpos"
     514     if [ $? -eq $FAIL ]
     515     then
     516       failure
     517     fi
     518
     519   done
     520         #  Could have condensed above two do-loops into a single one,
     521   echo  #+ but this would have slowed execution.
     522
     523   print_board
     524   echo
     525   echo "Knight's Tour ends on $(to_algebraic $currpos) [square #$currpos]."
     526   return $SUCCESS
     527 }
     528
     529 debug ()
     530 {       # Enable this by setting DEBUG=1 near beginning of script.
     531   local n
     532
     533   echo "================================="
     534   echo "  At move number  $movenum:"
     535   echo " *** possible moves = $mpm ***"
     536 # echo "### square = $square ###"
     537   echo "lmin = $lmin"
     538   echo "${moves[@]}"
     539
     540   for n in {1..8}
     541   do
     542     echo -n "($n):${p_moves[n]} "
     543   done
     544
     545   echo
     546   echo "iex = $iex :: moves[iex] = ${moves[iex]}"
     547   echo "square = $square"
     548   echo "================================="
     549   echo
     550 } # Gives pretty complete status after ea. move.
     551
     552
     553
     554 # =============================================================== #
     555 # int main () {
     556 from_algebraic "$1"
     557 startpos=$?
     558 if [ "$startpos" -eq "$FAIL" ]          # Okay even if no $1.
     559 then   #         ^^^^^^^^^^^              Okay even if input -lt 0.
     560   echo "No starting square specified (or illegal input)."
     561   let "startpos = $RANDOM % $SQUARES"   # 0 - 63 permissable range.
     562 fi
     563
     564
     565 if [ "$2" = "stupid" ]
     566 then
     567   STUPID=1
     568   echo -n "     ### Stupid Strategy ###"
     569 else
     570   STUPID=''
     571   echo -n "  *** Warnsdorff's Algorithm ***"
     572 fi
     573
     574
     575 initialize_board
     576
     577 movenum=0
     578 board[startpos]=$movenum   # Mark each board square with move number.
     579 currpos=$startpos
     580 algpos=$(to_algebraic $startpos)
     581
     582 echo; echo "Starting from $algpos [square #$startpos] ..."; echo
     583 echo -n "Moves:"
     584
     585 strategy "$currpos"
     586
     587 echo
     588
     589 exit 0   # return 0;
     590
     591 # }      # End of main() pseudo-function.
     592 # =============================================================== #
     593
     594
     595 # Exercises:
     596 # ---------
     597 #
     598 # 1) Extend this example to a 10 x 10 board or larger.
     599 # 2) Improve the "stupid strategy" by modifying the
     600 #    do_move_stupid function.
     601 #    Hint: Prevent straying into corner squares in early moves
     602 #          (the exact opposite of Warnsdorff's algorithm!).
     603 # 3) This script could stand considerable improvement and
     604 #    streamlining, especially in the poorly-written
     605 #    generate_moves() function
     606 #    and in the DECIDE-MOVE patch in the do_move() function.
     607 #    Must figure out why standard algorithm fails for startpos=37 ...
     608 #+   but _not_ on any other, including symmetrical startpos=26.
     609 #    Possibly, when calculating possible moves, counts the move back
     610 #+   to the originating square. If so, it might be a relatively easy fix.

Example A-46. Magic Squares

       1 #!/bin/bash
       2 # msquare.sh
       3 # Magic Square generator (odd-order squares only!)
       4
       5 # Author: mendel cooper
       6 # reldate: 19 Jan. 2009
       7 # License: Public Domain
       8 # A C-program by the very talented Kwon Young Shin inspired this script.
       9 #     http://user.chollian.net/~brainstm/MagicSquare.htm
      10
      11 # Definition: A "magic square" is a two-dimensional array
      12 #             of integers in which all the rows, columns,
      13 #             and *long* diagonals add up to the same number.
      14 #             Being "square," the array has the same number
      15 #             of rows and columns. That number is the "order."
      16 # An example of a magic square of order 3 is:
      17 #   8  1  6
      18 #   3  5  7
      19 #   4  9  2
      20 # All the rows, columns, and the two long diagonals add up to 15.
      21
      22
      23 # Globals
      24 EVEN=2
      25 MAXSIZE=31   # 31 rows x 31 cols.
      26 E_usage=90   # Invocation error.
      27 dimension=
      28 declare -i square
      29
      30 usage_message ()
      31 {
      32   echo "Usage: $0 order"
      33   echo "   ... where \"order\" (square size) is an ODD integer"
      34   echo "       in the range 3 - 31."
      35   #  Actually works for squares up to order 159,
      36   #+ but large squares will not display pretty-printed in a term window.
      37   #  Try increasing MAXSIZE, above.
      38   exit $E_usage
      39 }
      40
      41
      42 calculate ()       # Here's where the actual work gets done.
      43 {
      44   local row col index dimadj j k cell_val=1
      45   dimension=$1
      46
      47   let "dimadj = $dimension * 3"; let "dimadj /= 2"   # x 1.5, then truncate.
      48
      49   for ((j=0; j < dimension; j++))
      50   do
      51     for ((k=0; k < dimension; k++))
      52     do  # Calculate indices, then convert to 1-dim. array index.
      53         # Bash doesn't support multidimensional arrays. Pity.
      54       let "col = $k - $j + $dimadj"; let "col %= $dimension"
      55       let "row = $j * 2 - $k + $dimension"; let "row %= $dimension"
      56       let "index = $row*($dimension) + $col"
      57       square[$index]=cell_val; ((cell_val++))
      58     done
      59   done
      60 }     # Plain math, visualization not required.
      61
      62
      63 print_square ()               # Output square, one row at a time.
      64 {
      65   local row col idx d1
      66   let "d1 = $dimension - 1"   # Adjust for zero-indexed array.
      67
      68   for row in $(seq 0 $d1)
      69   do
      70
      71     for col in $(seq 0 $d1)
      72     do
      73       let "idx = $row * $dimension + $col"
      74       printf "%3d " "${square[idx]}"; echo -n "  "
      75     done   # Displays up to 13th order neatly in 80-column term window.
      76
      77     echo   # Newline after each row.
      78   done
      79 }
      80
      81
      82 #################################################
      83 if [[ -z "$1" ]] || [[ "$1" -gt $MAXSIZE ]]
      84 then
      85   usage_message
      86 fi
      87
      88 let "test_even = $1 % $EVEN"
      89 if [ $test_even -eq 0 ]
      90 then           # Can't handle even-order squares.
      91   usage_message
      92 fi
      93
      94 calculate $1
      95 print_square   # echo "${square[@]}"   # DEBUG
      96
      97 exit $?
      98 #################################################
      99
     100
     101 # Exercises:
     102 # ---------
     103 # 1) Add a function to calculate the sum of each row, column,
     104 #    and *long* diagonal. The sums must match.
     105 #    This is the "magic constant" of that particular order square.
     106 # 2) Have the print_square function auto-calculate how much space
     107 #    to allot between square elements for optimized display.
     108 #    This might require parameterizing the "printf" line.
     109 # 3) Add appropriate functions for generating magic squares
     110 #    with an *even* number of rows/columns.
     111 #    This is non-trivial(!).
     112 #    See the URL for Kwon Young Shin, above, for help.

Example A-47. Fifteen Puzzle

       1 #!/bin/bash
       2 # fifteen.sh
       3
       4 # Classic "Fifteen Puzzle"
       5 # Author: Antonio Macchi
       6 # Lightly edited and commented by ABS Guide author.
       7 # Used in ABS Guide with permission. (Thanks!)
       8
       9 #  The invention of the Fifteen Puzzle is attributed to either
      10 #+ Sam Loyd or Noyes Palmer Chapman.
      11 #  The puzzle was wildly popular in the late 19th-century.
      12
      13 #  Object: Rearrange the numbers so they read in order,
      14 #+ from 1 - 15:   ________________
      15 #                |  1   2   3   4 |
      16 #                |  5   6   7   8 |
      17 #                |  9  10  11  12 |
      18 #                | 13  14  15     |
      19 #                 ----------------
      20
      21
      22 #######################
      23 # Constants           #
      24   SQUARES=16          #
      25   FAIL=70             #
      26   E_PREMATURE_EXIT=80 #
      27 #######################
      28
      29
      30 ########
      31 # Data #
      32 ########
      33
      34 Puzzle=( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 " " )
      35
      36
      37 #############
      38 # Functions #
      39 #############
      40
      41 function swap
      42 {
      43   local tmp
      44
      45   tmp=${Puzzle[$1]}
      46   Puzzle[$1]=${Puzzle[$2]}
      47   Puzzle[$2]=$tmp
      48 }
      49
      50
      51 function Jumble
      52 { # Scramble the pieces at beginning of round.
      53   local i pos1 pos2
      54
      55   for i in {1..100}
      56   do
      57     pos1=$(( $RANDOM % $SQUARES))
      58     pos2=$(( $RANDOM % $SQUARES ))
      59     swap $pos1 $pos2
      60   done
      61 }
      62
      63
      64 function PrintPuzzle
      65 {
      66   local i1 i2 puzpos
      67   puzpos=0
      68
      69   clear
      70   echo "Enter  quit  to exit."; echo   # Better that than Ctl-C.
      71
      72   echo ",----.----.----.----."   # Top border.
      73   for i1 in {1..4}
      74   do
      75     for i2 in {1..4}
      76     do
      77       printf "| %2s " "${Puzzle[$puzpos]}"
      78       (( puzpos++ ))
      79     done
      80     echo "|"                     # Right-side border.
      81     test $i1 = 4 || echo "+----+----+----+----+"
      82   done
      83   echo "'----'----'----'----'"   # Bottom border.
      84 }
      85
      86
      87 function GetNum
      88 { # Test for valid input.
      89   local puznum garbage
      90
      91   while true
      92   do
      93 	  echo "Moves: $moves" # Also counts invalid moves.
      94     read -p "Number to move: " puznum garbage
      95       if [ "$puznum" = "quit" ]; then echo; exit $E_PREMATURE_EXIT; fi
      96     test -z "$puznum" -o -n "${puznum//[0-9]/}" && continue
      97     test $puznum -gt 0 -a $puznum -lt $SQUARES && break
      98   done
      99   return $puznum
     100 }
     101
     102
     103 function GetPosFromNum
     104 { # $1 = puzzle-number
     105   local puzpos
     106
     107   for puzpos in {0..15}
     108   do
     109     test "${Puzzle[$puzpos]}" = "$1" && break
     110   done
     111   return $puzpos
     112 }
     113
     114
     115 function Move
     116 { # $1=Puzzle-pos
     117   test $1 -gt 3 && test "${Puzzle[$(( $1 - 4 ))]}" = " "\
     118        && swap $1 $(( $1 - 4 )) && return 0
     119   test $(( $1%4 )) -ne 3 && test "${Puzzle[$(( $1 + 1 ))]}" = " "\
     120        && swap $1 $(( $1 + 1 )) && return 0
     121   test $1 -lt 12 && test "${Puzzle[$(( $1 + 4 ))]}" = " "\
     122        && swap $1 $(( $1 + 4 )) && return 0
     123   test $(( $1%4 )) -ne 0 && test "${Puzzle[$(( $1 - 1 ))]}" = " " &&\
     124        swap $1 $(( $1 - 1 )) && return 0
     125   return 1
     126 }
     127
     128
     129 function Solved
     130 {
     131   local pos
     132
     133   for pos in {0..14}
     134   do
     135     test "${Puzzle[$pos]}" = $(( $pos + 1 )) || return $FAIL
     136     # Check whether number in each square = square number.
     137   done
     138   return 0   # Successful solution.
     139 }
     140
     141
     142 ################### MAIN () #######################{
     143 moves=0
     144 Jumble
     145
     146 while true   # Loop continuously until puzzle solved.
     147 do
     148   echo; echo
     149   PrintPuzzle
     150   echo
     151   while true
     152   do
     153     GetNum
     154     puznum=$?
     155     GetPosFromNum $puznum
     156     puzpos=$?
     157     ((moves++))
     158     Move $puzpos && break
     159   done
     160   Solved && break
     161 done
     162
     163 echo;echo
     164 PrintPuzzle
     165 echo; echo "BRAVO!"; echo
     166
     167 exit 0
     168 ###################################################}
     169
     170 #  Exercise:
     171 #  --------
     172 #  Rewrite the script to display the letters A - O,
     173 #+ rather than the numbers 1 - 15.

Example A-48. The Towers of Hanoi, graphic version

       1 #! /bin/bash
       2 # The Towers Of Hanoi
       3 # Original script (hanoi.bash) copyright (C) 2000 Amit Singh.
       4 # All Rights Reserved.
       5 # http://hanoi.kernelthread.com
       6
       7 #  hanoi2.bash
       8 #  Version 2.00: modded for ASCII-graphic display.
       9 #  Version 2.01: fixed no command-line param bug.
      10 #  Uses code contributed by Antonio Macchi,
      11 #+ with heavy editing by ABS Guide author.
      12 #  This variant falls under the original copyright, see above.
      13 #  Used in ABS Guide with Amit Singh's permission (thanks!).
      14
      15
      16 ###   Variables && sanity check   ###
      17
      18 E_NOPARAM=86
      19 E_BADPARAM=87            # Illegal no. of disks passed to script.
      20 E_NOEXIT=88
      21
      22 DISKS=${1:-$E_NOPARAM}   # Must specify how many disks.
      23 Moves=0
      24
      25 MWIDTH=7
      26 MARGIN=2
      27 # Arbitrary "magic" constants; work okay for relatively small # of disks.
      28 # BASEWIDTH=51   # Original code.
      29 let "basewidth = $MWIDTH * $DISKS + $MARGIN"       # "Base" beneath rods.
      30 # Above "algorithm" could likely stand improvement.
      31
      32 ###   Display variables   ###
      33 let "disks1 = $DISKS - 1"
      34 let "spaces1 = $DISKS"
      35 let "spaces2 = 2 * $DISKS"
      36
      37 let "lastmove_t = $DISKS - 1"                      # Final move?
      38
      39
      40 declare -a Rod1 Rod2 Rod3
      41
      42 ###   #########################   ###
      43
      44
      45 function repeat  {  # $1=char $2=number of repetitions
      46   local n           # Repeat-print a character.
      47
      48   for (( n=0; n<$2; n++ )); do
      49     echo -n "$1"
      50   done
      51 }
      52
      53 function FromRod  {
      54   local rod summit weight sequence
      55
      56   while true; do
      57     rod=$1
      58     test ${rod/[^123]/} || continue
      59
      60     sequence=$(echo $(seq 0 $disks1 | tac))
      61     for summit in $sequence; do
      62       eval weight=\${Rod${rod}[$summit]}
      63       test $weight -ne 0 &&
      64            { echo "$rod $summit $weight"; return; }
      65     done
      66   done
      67 }
      68
      69
      70 function ToRod  { # $1=previous (FromRod) weight
      71   local rod firstfree weight sequence
      72
      73   while true; do
      74     rod=$2
      75     test ${rod/[^123]} || continue
      76
      77     sequence=$(echo $(seq 0 $disks1 | tac))
      78     for firstfree in $sequence; do
      79       eval weight=\${Rod${rod}[$firstfree]}
      80       test $weight -gt 0 && { (( firstfree++ )); break; }
      81     done
      82     test $weight -gt $1 -o $firstfree = 0 &&
      83          { echo "$rod $firstfree"; return; }
      84   done
      85 }
      86
      87
      88 function PrintRods  {
      89   local disk rod empty fill sp sequence
      90
      91
      92   repeat " " $spaces1
      93   echo -n "|"
      94   repeat " " $spaces2
      95   echo -n "|"
      96   repeat " " $spaces2
      97   echo "|"
      98
      99   sequence=$(echo $(seq 0 $disks1 | tac))
     100   for disk in $sequence; do
     101     for rod in {1..3}; do
     102       eval empty=$(( $DISKS - (Rod${rod}[$disk] / 2) ))
     103       eval fill=\${Rod${rod}[$disk]}
     104       repeat " " $empty
     105       test $fill -gt 0 && repeat "*" $fill || echo -n "|"
     106       repeat " " $empty
     107     done
     108     echo
     109   done
     110   repeat "=" $basewidth   # Print "base" beneath rods.
     111   echo
     112 }
     113
     114
     115 display ()
     116 {
     117   echo
     118   PrintRods
     119
     120   # Get rod-number, summit and weight
     121   first=( `FromRod $1` )
     122   eval Rod${first[0]}[${first[1]}]=0
     123
     124   # Get rod-number and first-free position
     125   second=( `ToRod ${first[2]} $2` )
     126   eval Rod${second[0]}[${second[1]}]=${first[2]}
     127
     128
     129 echo; echo; echo
     130 if [ "${Rod3[lastmove_t]}" = 1 ]
     131 then   # Last move? If yes, then display final position.
     132     echo "+  Final Position: $Moves moves"; echo
     133     PrintRods
     134   fi
     135 }
     136
     137
     138 # From here down, almost the same as original (hanoi.bash) script.
     139
     140 dohanoi() {   # Recursive function.
     141     case $1 in
     142     0)
     143         ;;
     144     *)
     145         dohanoi "$(($1-1))" $2 $4 $3
     146 	if [ "$Moves" -ne 0 ]
     147         then
     148 	  echo "+  Position after move $Moves"
     149         fi
     150         ((Moves++))
     151         echo -n "   Next move will be:  "
     152         echo $2 "-->" $3
     153           display $2 $3
     154         dohanoi "$(($1-1))" $4 $3 $2
     155         ;;
     156     esac
     157 }
     158
     159
     160 setup_arrays ()
     161 {
     162   local dim n elem
     163
     164   let "dim1 = $1 - 1"
     165   elem=$dim1
     166
     167   for n in $(seq 0 $dim1)
     168   do
     169    let "Rod1[$elem] = 2 * $n + 1"
     170    Rod2[$n]=0
     171    Rod3[$n]=0
     172    ((elem--))
     173   done
     174 }
     175
     176
     177 ###   Main   ###
     178
     179 setup_arrays $DISKS
     180 echo; echo "+  Start Position"
     181
     182 case $# in
     183     1) case $(($1>0)) in     # Must have at least one disk.
     184        1)
     185            disks=$1
     186            dohanoi $1 1 3 2
     187 #          Total moves = 2^n - 1, where n = number of disks.
     188 	   echo
     189            exit 0;
     190            ;;
     191        *)
     192            echo "$0: Illegal value for number of disks";
     193            exit $E_BADPARAM;
     194            ;;
     195        esac
     196     ;;
     197     *)
     198        clear
     199        echo "usage: $0 N"
     200        echo "       Where \"N\" is the number of disks."
     201        exit $E_NOPARAM;
     202        ;;
     203 esac
     204
     205 exit $E_NOEXIT   # Shouldn't exit here.
     206
     207 # Note:
     208 # Redirect script output to a file, otherwise it scrolls off display.

Example A-49. The Towers of Hanoi, alternate graphic version

       1 #! /bin/bash
       2 # The Towers Of Hanoi
       3 # Original script (hanoi.bash) copyright (C) 2000 Amit Singh.
       4 # All Rights Reserved.
       5 # http://hanoi.kernelthread.com
       6
       7 #  hanoi2.bash
       8 #  Version 2: modded for ASCII-graphic display.
       9 #  Uses code contributed by Antonio Macchi,
      10 #+ with heavy editing by ABS Guide author.
      11 #  This variant also falls under the original copyright, see above.
      12 #  Used in ABS Guide with Amit Singh's permission (thanks!).
      13
      14
      15 #   Variables   #
      16 E_NOPARAM=86
      17 E_BADPARAM=87   # Illegal no. of disks passed to script.
      18 E_NOEXIT=88
      19 DELAY=2         # Interval, in seconds, between moves. Change, if desired.
      20 DISKS=$1
      21 Moves=0
      22
      23 MWIDTH=7
      24 MARGIN=2
      25 # Arbitrary "magic" constants, work okay for relatively small # of disks.
      26 # BASEWIDTH=51   # Original code.
      27 let "basewidth = $MWIDTH * $DISKS + $MARGIN" # "Base" beneath rods.
      28 # Above "algorithm" could likely stand improvement.
      29
      30 # Display variables.
      31 let "disks1 = $DISKS - 1"
      32 let "spaces1 = $DISKS"
      33 let "spaces2 = 2 * $DISKS"
      34
      35 let "lastmove_t = $DISKS - 1"                # Final move?
      36
      37
      38 declare -a Rod1 Rod2 Rod3
      39
      40 #################
      41
      42
      43 function repeat  {  # $1=char $2=number of repetitions
      44   local n           # Repeat-print a character.
      45
      46   for (( n=0; n<$2; n++ )); do
      47     echo -n "$1"
      48   done
      49 }
      50
      51 function FromRod  {
      52   local rod summit weight sequence
      53
      54   while true; do
      55     rod=$1
      56     test ${rod/[^123]/} || continue
      57
      58     sequence=$(echo $(seq 0 $disks1 | tac))
      59     for summit in $sequence; do
      60       eval weight=\${Rod${rod}[$summit]}
      61       test $weight -ne 0 &&
      62            { echo "$rod $summit $weight"; return; }
      63     done
      64   done
      65 }
      66
      67
      68 function ToRod  { # $1=previous (FromRod) weight
      69   local rod firstfree weight sequence
      70
      71   while true; do
      72     rod=$2
      73     test ${rod/[^123]} || continue
      74
      75     sequence=$(echo $(seq 0 $disks1 | tac))
      76     for firstfree in $sequence; do
      77       eval weight=\${Rod${rod}[$firstfree]}
      78       test $weight -gt 0 && { (( firstfree++ )); break; }
      79     done
      80     test $weight -gt $1 -o $firstfree = 0 &&
      81          { echo "$rod $firstfree"; return; }
      82   done
      83 }
      84
      85
      86 function PrintRods  {
      87   local disk rod empty fill sp sequence
      88
      89   tput cup 5 0
      90
      91   repeat " " $spaces1
      92   echo -n "|"
      93   repeat " " $spaces2
      94   echo -n "|"
      95   repeat " " $spaces2
      96   echo "|"
      97
      98   sequence=$(echo $(seq 0 $disks1 | tac))
      99   for disk in $sequence; do
     100     for rod in {1..3}; do
     101       eval empty=$(( $DISKS - (Rod${rod}[$disk] / 2) ))
     102       eval fill=\${Rod${rod}[$disk]}
     103       repeat " " $empty
     104       test $fill -gt 0 && repeat "*" $fill || echo -n "|"
     105       repeat " " $empty
     106     done
     107     echo
     108   done
     109   repeat "=" $basewidth   # Print "base" beneath rods.
     110   echo
     111 }
     112
     113
     114 display ()
     115 {
     116   echo
     117   PrintRods
     118
     119   # Get rod-number, summit and weight
     120   first=( `FromRod $1` )
     121   eval Rod${first[0]}[${first[1]}]=0
     122
     123   # Get rod-number and first-free position
     124   second=( `ToRod ${first[2]} $2` )
     125   eval Rod${second[0]}[${second[1]}]=${first[2]}
     126
     127
     128   if [ "${Rod3[lastmove_t]}" = 1 ]
     129   then   # Last move? If yes, then display final position.
     130     tput cup 0 0
     131     echo; echo "+  Final Position: $Moves moves"
     132     PrintRods
     133   fi
     134
     135   sleep $DELAY
     136 }
     137
     138 # From here down, almost the same as original (hanoi.bash) script.
     139
     140 dohanoi() {   # Recursive function.
     141     case $1 in
     142     0)
     143         ;;
     144     *)
     145         dohanoi "$(($1-1))" $2 $4 $3
     146 	if [ "$Moves" -ne 0 ]
     147         then
     148 	  tput cup 0 0
     149 	  echo; echo "+  Position after move $Moves"
     150         fi
     151         ((Moves++))
     152         echo -n "   Next move will be:  "
     153         echo $2 "-->" $3
     154         display $2 $3
     155         dohanoi "$(($1-1))" $4 $3 $2
     156         ;;
     157     esac
     158 }
     159
     160 setup_arrays ()
     161 {
     162   local dim n elem
     163
     164   let "dim1 = $1 - 1"
     165   elem=$dim1
     166
     167   for n in $(seq 0 $dim1)
     168   do
     169    let "Rod1[$elem] = 2 * $n + 1"
     170    Rod2[$n]=0
     171    Rod3[$n]=0
     172    ((elem--))
     173   done
     174 }
     175
     176
     177 ###   Main   ###
     178
     179 trap "tput cnorm" 0
     180 tput civis
     181 clear
     182
     183 setup_arrays $DISKS
     184
     185 tput cup 0 0
     186 echo; echo "+  Start Position"
     187
     188 case $# in
     189     1) case $(($1>0)) in     # Must have at least one disk.
     190        1)
     191            disks=$1
     192            dohanoi $1 1 3 2
     193 #          Total moves = 2^n - 1, where n = # of disks.
     194 	   echo
     195            exit 0;
     196            ;;
     197        *)
     198            echo "$0: Illegal value for number of disks";
     199            exit $E_BADPARAM;
     200            ;;
     201        esac
     202     ;;
     203     *)
     204        echo "usage: $0 N"
     205        echo "       Where \"N\" is the number of disks."
     206        exit $E_NOPARAM;
     207        ;;
     208 esac
     209
     210 exit $E_NOEXIT   # Shouldn't exit here.
     211
     212 #  Exercise:
     213 #  --------
     214 #  There is a minor bug in the script that causes the display of
     215 #+ the next-to-last move to be skipped.
     216 #+ Fix this.

Example A-50. An alternate version of the getopt-simple.sh script

       1 #!/bin/bash
       2 # UseGetOpt.sh
       3
       4 # Author: Peggy Russell <prusselltechgroup@gmail.com>
       5
       6 UseGetOpt () {
       7   declare inputOptions
       8   declare -r E_OPTERR=85
       9   declare -r ScriptName=${0##*/}
      10   declare -r ShortOpts="adf:hlt"
      11   declare -r LongOpts="aoption,debug,file:,help,log,test"
      12
      13 DoSomething () {
      14     echo "The function name is '${FUNCNAME}'"
      15     #  Recall that $FUNCNAME is an internal variable
      16     #+ holding the name of the function it is in.
      17   }
      18
      19   inputOptions=$(getopt -o "${ShortOpts}" --long \
      20               "${LongOpts}" --name "${ScriptName}" -- "${@}")
      21
      22   if [[ ($? -ne 0) || ($# -eq 0) ]]; then
      23     echo "Usage: ${ScriptName} [-dhlt] {OPTION...}"
      24     exit $E_OPTERR
      25   fi
      26
      27   eval set -- "${inputOptions}"
      28
      29   # Only for educational purposes. Can be removed.
      30   #-----------------------------------------------
      31   echo "++ Test: Number of arguments: [$#]"
      32   echo '++ Test: Looping through "$@"'
      33   for a in "$@"; do
      34     echo "  ++ [$a]"
      35   done
      36   #-----------------------------------------------
      37
      38   while true; do
      39     case "${1}" in
      40       --aoption | -a)  # Argument found.
      41         echo "Option [$1]"
      42         ;;
      43
      44       --debug | -d)    # Enable informational messages.
      45         echo "Option [$1] Debugging enabled"
      46         ;;
      47
      48       --file | -f)     #  Check for optional argument.
      49         case "$2" in   #+ Double colon is optional argument.
      50           "")          #  Not there.
      51               echo "Option [$1] Use default"
      52               shift
      53               ;;
      54
      55           *) # Got it
      56              echo "Option [$1] Using input [$2]"
      57              shift
      58              ;;
      59
      60         esac
      61         DoSomething
      62         ;;
      63
      64       --log | -l) # Enable Logging.
      65         echo "Option [$1] Logging enabled"
      66         ;;
      67
      68       --test | -t) # Enable testing.
      69         echo "Option [$1] Testing enabled"
      70         ;;
      71
      72       --help | -h)
      73         echo "Option [$1] Display help"
      74         break
      75         ;;
      76
      77       --)   # Done! $# is argument number for "--", $@ is "--"
      78         echo "Option [$1] Dash Dash"
      79         break
      80         ;;
      81
      82        *)
      83         echo "Major internal error!"
      84         exit 8
      85         ;;
      86
      87     esac
      88     echo "Number of arguments: [$#]"
      89     shift
      90   done
      91
      92   shift
      93   # Only for educational purposes. Can be removed.
      94   #----------------------------------------------------------------------
      95   echo "++ Test: Number of arguments after \"--\" is [$#] They are: [$@]"
      96   echo '++ Test: Looping through "$@"'
      97   for a in "$@"; do
      98     echo "  ++ [$a]"
      99   done
     100   #----------------------------------------------------------------------
     101
     102 }
     103
     104 ################################### M A I N ########################
     105 #  If you remove "function UseGetOpt () {" and corresponding "}",
     106 #+ you can uncomment the "exit 0" line below, and invoke this script
     107 #+ with the various options from the command-line.
     108 #-------------------------------------------------------------------
     109 # exit 0
     110
     111 echo "Test 1"
     112 UseGetOpt -f myfile one "two three" four
     113
     114 echo;echo "Test 2"
     115 UseGetOpt -h
     116
     117 echo;echo "Test 3 - Short Options"
     118 UseGetOpt -adltf myfile  anotherfile
     119
     120 echo;echo "Test 4 - Long Options"
     121 UseGetOpt --aoption --debug --log --test --file myfile anotherfile
     122
     123 exit

Example A-51. The version of the UseGetOpt.sh example used in the Tab Expansion appendix

       1 #!/bin/bash
       2
       3 #  UseGetOpt-2.sh
       4 #  Modified version of the script for illustrating tab-expansion
       5 #+ of command-line options.
       6 #  See the "Introduction to Tab Expansion" appendix.
       7
       8 #  Possible options: -a -d -f -l -t -h
       9 #+                   --aoption, --debug --file --log --test -- help --
      10
      11 #  Author of original script: Peggy Russell <prusselltechgroup@gmail.com>
      12
      13
      14 # UseGetOpt () {
      15   declare inputOptions
      16   declare -r E_OPTERR=85
      17   declare -r ScriptName=${0##*/}
      18   declare -r ShortOpts="adf:hlt"
      19   declare -r LongOpts="aoption,debug,file:,help,log,test"
      20
      21 DoSomething () {
      22     echo "The function name is '${FUNCNAME}'"
      23   }
      24
      25   inputOptions=$(getopt -o "${ShortOpts}" --long \
      26               "${LongOpts}" --name "${ScriptName}" -- "${@}")
      27
      28   if [[ ($? -ne 0) || ($# -eq 0) ]]; then
      29     echo "Usage: ${ScriptName} [-dhlt] {OPTION...}"
      30     exit $E_OPTERR
      31   fi
      32
      33   eval set -- "${inputOptions}"
      34
      35
      36   while true; do
      37     case "${1}" in
      38       --aoption | -a)  # Argument found.
      39         echo "Option [$1]"
      40         ;;
      41
      42       --debug | -d)    # Enable informational messages.
      43         echo "Option [$1] Debugging enabled"
      44         ;;
      45
      46       --file | -f)     #  Check for optional argument.
      47         case "$2" in   #+ Double colon is optional argument.
      48           "")          #  Not there.
      49               echo "Option [$1] Use default"
      50               shift
      51               ;;
      52
      53           *) # Got it
      54              echo "Option [$1] Using input [$2]"
      55              shift
      56              ;;
      57
      58         esac
      59         DoSomething
      60         ;;
      61
      62       --log | -l) # Enable Logging.
      63         echo "Option [$1] Logging enabled"
      64         ;;
      65
      66       --test | -t) # Enable testing.
      67         echo "Option [$1] Testing enabled"
      68         ;;
      69
      70       --help | -h)
      71         echo "Option [$1] Display help"
      72         break
      73         ;;
      74
      75       --)   # Done! $# is argument number for "--", $@ is "--"
      76         echo "Option [$1] Dash Dash"
      77         break
      78         ;;
      79
      80        *)
      81         echo "Major internal error!"
      82         exit 8
      83         ;;
      84
      85     esac
      86     echo "Number of arguments: [$#]"
      87     shift
      88   done
      89
      90   shift
      91
      92 #  }
      93
      94 exit

Example A-52. Cycling through all the possible color backgrounds

       1 #!/bin/bash
       2
       3 # show-all-colors.sh
       4 # Displays all 256 possible background colors, using ANSI escape sequences.
       5 # Author: Chetankumar Phulpagare
       6 # Used in ABS Guide with permission.
       7
       8 T1=8
       9 T2=6
      10 T3=36
      11 offset=0
      12
      13 for num1 in {0..7}
      14 do {
      15    for num2 in {0,1}
      16        do {
      17           shownum=`echo "$offset + $T1 * ${num2} + $num1" | bc`
      18           echo -en "\E[0;48;5;${shownum}m color ${shownum} \E[0m"
      19           }
      20        done
      21    echo
      22    }
      23 done
      24
      25 offset=16
      26 for num1 in {0..5}
      27 do {
      28    for num2 in {0..5}
      29        do {
      30           for num3 in {0..5}
      31               do {
      32                  shownum=`echo "$offset + $T2 * ${num3} \
      33                  + $num2 + $T3 * ${num1}" | bc`
      34                  echo -en "\E[0;48;5;${shownum}m color ${shownum} \E[0m"
      35                  }
      36                done
      37           echo
      38           }
      39        done
      40 }
      41 done
      42
      43 offset=232
      44 for num1 in {0..23}
      45 do {
      46    shownum=`expr $offset + $num1`
      47    echo -en "\E[0;48;5;${shownum}m ${shownum}\E[0m"
      48 }
      49 done
      50
      51 echo

Example A-53. Morse Code Practice

       1 #!/bin/bash
       2 # sam.sh, v. .01a
       3 # Still Another Morse (code training script)
       4 # With profuse apologies to Sam (F.B.) Morse.
       5 # Author: Mendel Cooper
       6 # License: GPL3
       7 # Reldate: 05/25/11
       8
       9 # Morse code training script.
      10 # Converts arguments to audible dots and dashes.
      11 # Note: lowercase input only at this time.
      12
      13
      14
      15 # Get the wav files from the source tarball:
      16 # http://bash.deta.in/abs-guide-latest.tar.bz2
      17 DOT='soundfiles/dot.wav'
      18 DASH='soundfiles/dash.wav'
      19 # Maybe move soundfiles to /usr/local/sounds?
      20
      21 LETTERSPACE=300000  # Microseconds.
      22 WORDSPACE=980000
      23 # Nice and slow, for beginners. Maybe 5 wpm?
      24
      25 EXIT_MSG="May the Morse be with you!"
      26 E_NOARGS=75         # No command-line args?
      27
      28
      29
      30 declare -A morse    # Associative array!
      31 # ======================================= #
      32 morse[a]="dot; dash"
      33 morse[b]="dash; dot; dot; dot"
      34 morse[c]="dash; dot; dash; dot"
      35 morse[d]="dash; dot; dot"
      36 morse[e]="dot"
      37 morse[f]="dot; dot; dash; dot"
      38 morse[g]="dash; dash; dot"
      39 morse[h]="dot; dot; dot; dot"
      40 morse[i]="dot; dot;"
      41 morse[j]="dot; dash; dash; dash"
      42 morse[k]="dash; dot; dash"
      43 morse[l]="dot; dash; dot; dot"
      44 morse[m]="dash; dash"
      45 morse[n]="dash; dot"
      46 morse[o]="dash; dash; dash"
      47 morse[p]="dot; dash; dash; dot"
      48 morse[q]="dash; dash; dot; dash"
      49 morse[r]="dot; dash; dot"
      50 morse[s]="dot; dot; dot"
      51 morse[t]="dash"
      52 morse[u]="dot; dot; dash"
      53 morse[v]="dot; dot; dot; dash"
      54 morse[w]="dot; dash; dash"
      55 morse[x]="dash; dot; dot; dash"
      56 morse[y]="dash; dot; dash; dash"
      57 morse[z]="dash; dash; dot; dot"
      58 morse[0]="dash; dash; dash; dash; dash"
      59 morse[1]="dot; dash; dash; dash; dash"
      60 morse[2]="dot; dot; dash; dash; dash"
      61 morse[3]="dot; dot; dot; dash; dash"
      62 morse[4]="dot; dot; dot; dot; dash"
      63 morse[5]="dot; dot; dot; dot; dot"
      64 morse[6]="dash; dot; dot; dot; dot"
      65 morse[7]="dash; dash; dot; dot; dot"
      66 morse[8]="dash; dash; dash; dot; dot"
      67 morse[9]="dash; dash; dash; dash; dot"
      68 # The following must be escaped or quoted.
      69 morse[?]="dot; dot; dash; dash; dot; dot"
      70 morse[.]="dot; dash; dot; dash; dot; dash"
      71 morse[,]="dash; dash; dot; dot; dash; dash"
      72 morse[/]="dash; dot; dot; dash; dot"
      73 morse[\@]="dot; dash; dash; dot; dash; dot"
      74 # ======================================= #
      75
      76 play_letter ()
      77 {
      78   eval ${morse[$1]}   # Play dots, dashes from appropriate sound files.
      79   # Why is 'eval' necessary here?
      80   usleep $LETTERSPACE # Pause in between letters.
      81 }
      82
      83 extract_letters ()
      84 {                     # Slice string apart, letter by letter.
      85   local pos=0         # Starting at left end of string.
      86   local len=1         # One letter at a time.
      87   strlen=${#1}
      88
      89   while [ $pos -lt $strlen ]
      90   do
      91     letter=${1:pos:len}
      92     #      ^^^^^^^^^^^^    See Chapter 10.1.
      93     play_letter $letter
      94     echo -n "*"       #    Mark letter just played.
      95     ((pos++))
      96   done
      97 }
      98
      99 ######### Play the sounds ############
     100 dot()  { aplay "$DOT" 2&>/dev/null;  }
     101 dash() { aplay "$DASH" 2&>/dev/null; }
     102 ######################################
     103
     104 no_args ()
     105 {
     106     declare -a usage
     107     usage=( $0 word1 word2 ... )
     108
     109     echo "Usage:"; echo
     110     echo ${usage[*]}
     111     for index in 0 1 2 3
     112     do
     113       extract_letters ${usage[index]}
     114       usleep $WORDSPACE
     115       echo -n " "     # Print space between words.
     116     done
     117 #   echo "Usage: $0 word1 word2 ... "
     118     echo; echo
     119 }
     120
     121
     122 # int main()
     123 # {
     124
     125 clear                 # Clear the terminal screen.
     126 echo "            SAM"
     127 echo "Still Another Morse code trainer"
     128 echo "    Author: Mendel Cooper"
     129 echo; echo;
     130
     131 if [ -z "$1" ]
     132 then
     133   no_args
     134   echo; echo; echo "$EXIT_MSG"; echo
     135   exit $E_NOARGS
     136 fi
     137
     138 echo; echo "$*"       # Print text that will be played.
     139
     140 until [ -z "$1" ]
     141 do
     142   extract_letters $1
     143   shift           # On to next word.
     144   usleep $WORDSPACE
     145   echo -n " "     # Print space between words.
     146 done
     147
     148 echo; echo; echo "$EXIT_MSG"; echo
     149
     150 exit 0
     151 # }
     152
     153 #  Exercises:
     154 #  ---------
     155 #  1) Have the script accept either lowercase or uppercase words
     156 #+    as arguments. Hint: Use 'tr' . . .
     157 #  2) Have the script optionally accept input from a text file.

Example A-54. Base64 encoding/decoding

       1 #!/bin/bash
       2 # base64.sh: Bash implementation of Base64 encoding and decoding.
       3 #
       4 # Copyright (c) 2011 vladz <vladz@devzero.fr>
       5 # Used in ABSG with permission (thanks!).
       6 #
       7 #  Encode or decode original Base64 (and also Base64url)
       8 #+ from STDIN to STDOUT.
       9 #
      10 #    Usage:
      11 #
      12 #    Encode
      13 #    $ ./base64.sh < binary-file > binary-file.base64
      14 #    Decode
      15 #    $ ./base64.sh -d < binary-file.base64 > binary-file
      16 #
      17 # Reference:
      18 #
      19 #    [1]  RFC4648 - "The Base16, Base32, and Base64 Data Encodings"
      20 #         http://tools.ietf.org/html/rfc4648#section-5
      21
      22
      23 # The base64_charset[] array contains entire base64 charset,
      24 # and additionally the character "=" ...
      25 base64_charset=( {A..Z} {a..z} {0..9} + / = )
      26                 # Nice illustration of brace expansion.
      27
      28 #  Uncomment the ### line below to use base64url encoding instead of
      29 #+ original base64.
      30 ### base64_charset=( {A..Z} {a..z} {0..9} - _ = )
      31
      32 #  Output text width when encoding
      33 #+ (64 characters, just like openssl output).
      34 text_width=64
      35
      36 function display_base64_char {
      37 #  Convert a 6-bit number (between 0 and 63) into its corresponding values
      38 #+ in Base64, then display the result with the specified text width.
      39   printf "${base64_charset[$1]}"; (( width++ ))
      40   (( width % text_width == 0 )) && printf "\n"
      41 }
      42
      43 function encode_base64 {
      44 # Encode three 8-bit hexadecimal codes into four 6-bit numbers.
      45   #    We need two local int array variables:
      46   #    c8[]: to store the codes of the 8-bit characters to encode
      47   #    c6[]: to store the corresponding encoded values on 6-bit
      48   declare -a -i c8 c6
      49
      50   #  Convert hexadecimal to decimal.
      51   c8=( $(printf "ibase=16; ${1:0:2}\n${1:2:2}\n${1:4:2}\n" | bc) )
      52
      53   #  Let's play with bitwise operators
      54   #+ (3x8-bit into 4x6-bits conversion).
      55   (( c6[0] = c8[0] >> 2 ))
      56   (( c6[1] = ((c8[0] &  3) << 4) | (c8[1] >> 4) ))
      57
      58   # The following operations depend on the c8 element number.
      59   case ${#c8[*]} in
      60     3) (( c6[2] = ((c8[1] & 15) << 2) | (c8[2] >> 6) ))
      61        (( c6[3] = c8[2] & 63 )) ;;
      62     2) (( c6[2] = (c8[1] & 15) << 2 ))
      63        (( c6[3] = 64 )) ;;
      64     1) (( c6[2] = c6[3] = 64 )) ;;
      65   esac
      66
      67   for char in ${c6[@]}; do
      68     display_base64_char ${char}
      69   done
      70 }
      71
      72 function decode_base64 {
      73 # Decode four base64 characters into three hexadecimal ASCII characters.
      74   #  c8[]: to store the codes of the 8-bit characters
      75   #  c6[]: to store the corresponding Base64 values on 6-bit
      76   declare -a -i c8 c6
      77
      78   # Find decimal value corresponding to the current base64 character.
      79   for current_char in ${1:0:1} ${1:1:1} ${1:2:1} ${1:3:1}; do
      80      [ "${current_char}" = "=" ] && break
      81
      82      position=0
      83      while [ "${current_char}" != "${base64_charset[${position}]}" ]; do
      84         (( position++ ))
      85      done
      86
      87      c6=( ${c6[*]} ${position} )
      88   done
      89
      90   #  Let's play with bitwise operators
      91   #+ (4x8-bit into 3x6-bits conversion).
      92   (( c8[0] = (c6[0] << 2) | (c6[1] >> 4) ))
      93
      94   # The next operations depends on the c6 elements number.
      95   case ${#c6[*]} in
      96     3) (( c8[1] = ( (c6[1] & 15) << 4) | (c6[2] >> 2) ))
      97        (( c8[2] = (c6[2] & 3) << 6 )); unset c8[2] ;;
      98     4) (( c8[1] = ( (c6[1] & 15) << 4) | (c6[2] >> 2) ))
      99        (( c8[2] = ( (c6[2] &  3) << 6) |  c6[3] )) ;;
     100   esac
     101
     102   for char in ${c8[*]}; do
     103      printf "\x$(printf "%x" ${char})"
     104   done
     105 }
     106
     107
     108 # main ()
     109
     110 if [ "$1" = "-d" ]; then   # decode
     111
     112   # Reformat STDIN in pseudo 4x6-bit groups.
     113   content=$(cat - | tr -d "\n" | sed -r "s/(.{4})/\1 /g")
     114
     115   for chars in ${content}; do decode_base64 ${chars}; done
     116
     117 else
     118   # Make a hexdump of stdin and reformat in 3-byte groups.
     119   content=$(cat - | xxd -ps -u | sed -r "s/(\w{6})/\1 /g" |
     120             tr -d "\n")
     121
     122   for chars in ${content}; do encode_base64 ${chars}; done
     123
     124   echo
     125
     126 fi

Example A-55. Inserting text in a file using sed

       1 #!/bin/bash
       2 #  Prepends a string at a specified line
       3 #+ in files with names ending in "sample"
       4 #+ in the current working directory.
       5 #  000000000000000000000000000000000000
       6 #  This script overwrites files!
       7 #  Be careful running it in a directory
       8 #+ where you have important files!!!
       9 #  000000000000000000000000000000000000
      10
      11 #  Create a couple of files to operate on ...
      12 #  01sample
      13 #  02sample
      14 #  ... etc.
      15 #  These files must not be empty, else the prepend will not work.
      16
      17 lineno=1            # Append at line 1 (prepend).
      18 filespec="*sample"  # Filename pattern to operate on.
      19
      20 string=$(whoami)    # Will set your username as string to insert.
      21                     # It could just as easily be any other string.
      22
      23 for file in $filespec # Specify which files to alter.
      24 do #        ^^^^^^^^^
      25  sed -i ""$lineno"i "$string"" $file
      26 #    ^^ -i option edits files in-place.
      27 #                 ^ Insert (i) command.
      28  echo ""$file" altered!"
      29 done
      30
      31 echo "Warning: files possibly clobbered!"
      32
      33 exit 0
      34
      35 # Exercise:
      36 # Add error checking to this script.
      37 # It needs it badly.

Example A-56. The Gronsfeld Cipher

       1 #!/bin/bash
       2 # gronsfeld.bash
       3
       4 # License: GPL3
       5 # Reldate 06/23/11
       6
       7 #  This is an implementation of the Gronsfeld Cipher.
       8 #  It's essentially a stripped-down variant of the
       9 #+ polyalphabetic Vigenère Tableau, but with only 10 alphabets.
      10 #  The classic Gronsfeld has a numeric sequence as the key word,
      11 #+ but here we substitute a letter string, for ease of use.
      12 #  Allegedly, this cipher was invented by the eponymous Count Gronsfeld
      13 #+ in the 17th Century. It was at one time considered to be unbreakable.
      14 #  Note that this is ###not### a secure cipher by modern standards.
      15
      16 #  Global Variables  #
      17 Enc_suffix="29379"   #  Encrypted text output with this 5-digit suffix.
      18                      #  This functions as a decryption flag,
      19                      #+ and when used to generate passwords adds security.
      20 Default_key="gronsfeldk"
      21                      #  The script uses this if key not entered below
      22                      #  (at "Keychain").
      23                      #  Change the above two values frequently
      24                      #+ for added security.
      25
      26 GROUPLEN=5           #  Output in groups of 5 letters, per tradition.
      27 alpha1=( abcdefghijklmnopqrstuvwxyz )
      28 alpha2=( {A..Z} )    #  Output in all caps, per tradition.
      29                      #  Use   alpha2=( {a..z} )   for password generator.
      30 wraplen=26           #  Wrap around if past end of alphabet.
      31 dflag=               #  Decrypt flag (set if $Enc_suffix present).
      32 E_NOARGS=76          #  Missing command-line args?
      33 DEBUG=77             #  Debugging flag.
      34 declare -a offsets   #  This array holds the numeric shift values for
      35                      #+ encryption/decryption.
      36
      37 ########Keychain#########
      38 key=  ### Put key here!!!
      39       # 10 characters!
      40 #########################
      41
      42
      43
      44 # Function
      45 : ()
      46 { # Encrypt or decrypt, depending on whether $dflag is set.
      47   # Why ": ()" as a function name? Just to prove that it can be done.
      48
      49   local idx keydx mlen off1 shft
      50   local plaintext="$1"
      51   local mlen=${#plaintext}
      52
      53 for (( idx=0; idx<$mlen; idx++ ))
      54 do
      55   let "keydx = $idx % $keylen"
      56   shft=${offsets[keydx]}
      57
      58   if [ -n "$dflag" ]
      59   then                  # Decrypt!
      60     let "off1 = $(expr index "${alpha1[*]}" ${plaintext:idx:1}) - $shft"
      61     # Shift backward to decrypt.
      62   else                  # Encrypt!
      63     let "off1 = $(expr index "${alpha1[*]}" ${plaintext:idx:1}) + $shft"
      64     # Shift forward to encrypt.
      65     test $(( $idx % $GROUPLEN)) = 0 && echo -n " "  # Groups of 5 letters.
      66     #  Comment out above line for output as a string without whitespace,
      67     #+ for example, if using the script as a password generator.
      68   fi
      69
      70   ((off1--))   # Normalize. Why is this necessary?
      71
      72       if [ $off1 -lt 0 ]
      73       then     # Catch negative indices.
      74         let "off1 += $wraplen"
      75       fi
      76
      77   ((off1 %= $wraplen))   # Wrap around if past end of alphabet.
      78
      79   echo -n "${alpha2[off1]}"
      80
      81 done
      82
      83   if [ -z "$dflag" ]
      84   then
      85     echo " $Enc_suffix"
      86 #   echo "$Enc_suffix"  # For password generator.
      87   else
      88     echo
      89   fi
      90 } # End encrypt/decrypt function.
      91
      92
      93
      94 # int main () {
      95
      96 # Check for command-line args.
      97 if [ -z "$1" ]
      98 then
      99    echo "Usage: $0 TEXT TO ENCODE/DECODE"
     100    exit $E_NOARGS
     101 fi
     102
     103 if [ ${!#} == "$Enc_suffix" ]
     104 #    ^^^^^ Final command-line arg.
     105 then
     106   dflag=ON
     107   echo -n "+"           # Flag decrypted text with a "+" for easy ID.
     108 fi
     109
     110 if [ -z "$key" ]
     111 then
     112   key="$Default_key"    # "gronsfeldk" per above.
     113 fi
     114
     115 keylen=${#key}
     116
     117 for (( idx=0; idx<$keylen; idx++ ))
     118 do  # Calculate shift values for encryption/decryption.
     119   offsets[idx]=$(expr index "${alpha1[*]}" ${key:idx:1})   # Normalize.
     120   ((offsets[idx]--))  #  Necessary because "expr index" starts at 1,
     121                       #+ whereas array count starts at 0.
     122   # Generate array of numerical offsets corresponding to the key.
     123   # There are simpler ways to accomplish this.
     124 done
     125
     126 args=$(echo "$*" | sed -e 's/ //g' | tr A-Z a-z | sed -e 's/[0-9]//g')
     127 # Remove whitespace and digits from command-line args.
     128 # Can modify to also remove punctuation characters, if desired.
     129
     130          # Debug:
     131          # echo "$args"; exit $DEBUG
     132
     133 : "$args"               # Call the function named ":".
     134 # : is a null operator, except . . . when it's a function name!
     135
     136 exit $?    # } End-of-script
     137
     138
     139 #   **************************************************************   #
     140 #   This script can function as a  password generator,
     141 #+  with several minor mods, see above.
     142 #   That would allow an easy-to-remember password, even the word
     143 #+ "password" itself, which encrypts to vrgfotvo29379
     144 #+  a fairly secure password not susceptible to a dictionary attack.
     145 #   Or, you could use your own name (surely that's easy to remember!).
     146 #   For example, Bozo Bozeman encrypts to hfnbttdppkt29379.
     147 #   **************************************************************   #

Example A-57. Bingo Number Generator

       1 #!/bin/bash
       2 # bingo.sh
       3 # Bingo number generator
       4 # Reldate 20Aug12, License: Public Domain
       5
       6 #######################################################################
       7 # This script generates bingo numbers.
       8 # Hitting a key generates a new number.
       9 # Hitting 'q' terminates the script.
      10 # In a given run of the script, there will be no duplicate numbers.
      11 # When the script terminates, it prints a log of the numbers generated.
      12 #######################################################################
      13
      14 MIN=1       # Lowest allowable bingo number.
      15 MAX=75      # Highest allowable bingo number.
      16 COLS=15     # Numbers in each column (B I N G O).
      17 SINGLE_DIGIT_MAX=9
      18
      19 declare -a Numbers
      20 Prefix=(B I N G O)
      21
      22 initialize_Numbers ()
      23 {  # Zero them out to start.
      24    # They'll be incremented if chosen.
      25    local index=0
      26    until [ "$index" -gt $MAX ]
      27    do
      28      Numbers[index]=0
      29      ((index++))
      30    done
      31
      32    Numbers[0]=1   # Flag zero, so it won't be selected.
      33 }
      34
      35
      36 generate_number ()
      37 {
      38    local number
      39
      40    while [ 1 ]
      41    do
      42      let "number = $(expr $RANDOM % $MAX)"
      43      if [ ${Numbers[number]} -eq 0 ]    # Number not yet called.
      44      then
      45        let "Numbers[number]+=1"         # Flag it in the array.
      46        break                            # And terminate loop.
      47      fi   # Else if already called, loop and generate another number.
      48    done
      49    # Exercise: Rewrite this more elegantly as an until-loop.
      50
      51    return $number
      52 }
      53
      54
      55 print_numbers_called ()
      56 {   # Print out the called number log in neat columns.
      57     # echo ${Numbers[@]}
      58
      59 local pre2=0                #  Prefix a zero, so columns will align
      60                             #+ on single-digit numbers.
      61
      62 echo "Number Stats"
      63
      64 for (( index=1; index<=MAX; index++))
      65 do
      66   count=${Numbers[index]}
      67   let "t = $index - 1"      # Normalize, since array begins with index 0.
      68   let "column = $(expr $t / $COLS)"
      69   pre=${Prefix[column]}
      70 # echo -n "${Prefix[column]} "
      71
      72 if [ $(expr $t % $COLS) -eq 0 ]
      73 then
      74   echo   # Newline at end of row.
      75 fi
      76
      77   if [ "$index" -gt $SINGLE_DIGIT_MAX ]  # Check for single-digit number.
      78   then
      79     echo -n "$pre$index#$count "
      80   else    # Prefix a zero.
      81     echo -n "$pre$pre2$index#$count "
      82   fi
      83
      84 done
      85 }
      86
      87
      88
      89 # main () {
      90 RANDOM=$$   # Seed random number generator.
      91
      92 initialize_Numbers   # Zero out the number tracking array.
      93
      94 clear
      95 echo "Bingo Number Caller"; echo
      96
      97 while [[ "$key" != "q" ]]   # Main loop.
      98 do
      99   read -s -n1 -p "Hit a key for the next number [q to exit] " key
     100   # Usually 'q' exits, but not always.
     101   # Can always hit Ctl-C if q fails.
     102   echo
     103
     104   generate_number; new_number=$?
     105
     106   let "column = $(expr $new_number / $COLS)"
     107   echo -n "${Prefix[column]} "   # B-I-N-G-O
     108
     109   echo $new_number
     110 done
     111
     112 echo; echo
     113
     114 # Game over ...
     115 print_numbers_called
     116 echo; echo "[#0 = not called . . . #1 = called]"
     117
     118 echo
     119
     120 exit 0
     121 # }
     122
     123
     124 # Certainly, this script could stand some improvement.
     125 #See also the author's Instructable:
     126 #www.instructables.com/id/Binguino-An-Arduino-based-Bingo-Number-Generato/

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

Example A-58. Basics Reviewed

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

Example A-59. Testing execution times of various commands

       1 #!/bin/bash
       2 #  test-execution-time.sh
       3 #  Example by Erik Brandsberg, for testing execution time
       4 #+ of certain operations.
       5 #  Referenced in the "Optimizations" section of "Miscellany" chapter.
       6
       7 count=50000
       8 echo "Math tests"
       9 echo "Math via \$(( ))"
      10 time for (( i=0; i< $count; i++))
      11 do
      12   result=$(( $i%2 ))
      13 done
      14
      15 echo "Math via *expr*:"
      16 time for (( i=0; i< $count; i++))
      17 do
      18   result=`expr "$i%2"`
      19 done
      20
      21 echo "Math via *let*:"
      22 time for (( i=0; i< $count; i++))
      23 do
      24   let result=$i%2
      25 done
      26
      27 echo
      28 echo "Conditional testing tests"
      29
      30 echo "Test via case:"
      31 time for (( i=0; i< $count; i++))
      32 do
      33   case $(( $i%2 )) in
      34     0) : ;;
      35     1) : ;;
      36   esac
      37 done
      38
      39 echo "Test with if [], no quotes:"
      40 time for (( i=0; i< $count; i++))
      41 do
      42   if [ $(( $i%2 )) = 0 ]; then
      43      :
      44   else
      45      :
      46   fi
      47 done
      48
      49 echo "Test with if [], quotes:"
      50 time for (( i=0; i< $count; i++))
      51 do
      52   if [ "$(( $i%2 ))" = "0" ]; then
      53      :
      54   else
      55      :
      56   fi
      57 done
      58
      59 echo "Test with if [], using -eq:"
      60 time for (( i=0; i< $count; i++))
      61 do
      62   if [ $(( $i%2 )) -eq 0 ]; then
      63      :
      64   else
      65      :
      66   fi
      67 done
      68
      69 exit $?

Example A-60. Associative arrays vs. conventional arrays (execution times)

       1 #!/bin/bash
       2 #  assoc-arr-test.sh
       3 #  Benchmark test script to compare execution times of
       4 #  numeric-indexed array vs. associative array.
       5 #     Thank you, Erik Brandsberg.
       6
       7 count=100000       # May take a while for some of the tests below.
       8 declare simple     # Can change to 20000, if desired.
       9 declare -a array1
      10 declare -A array2
      11 declare -a array3
      12 declare -A array4
      13
      14 echo "===Assignment tests==="
      15 echo
      16
      17 echo "Assigning a simple variable:"
      18 # References $i twice to equalize lookup times.
      19 time for (( i=0; i< $count; i++)); do
      20         simple=$i$i
      21 done
      22
      23 echo "---"
      24
      25 echo "Assigning a numeric index array entry:"
      26 time for (( i=0; i< $count; i++)); do
      27         array1[$i]=$i
      28 done
      29
      30 echo "---"
      31
      32 echo "Overwriting a numeric index array entry:"
      33 time for (( i=0; i< $count; i++)); do
      34         array1[$i]=$i
      35 done
      36
      37 echo "---"
      38
      39 echo "Linear reading of numeric index array:"
      40 time for (( i=0; i< $count; i++)); do
      41         simple=array1[$i]
      42 done
      43
      44 echo "---"
      45
      46 echo "Assigning an associative array entry:"
      47 time for (( i=0; i< $count; i++)); do
      48         array2[$i]=$i
      49 done
      50
      51 echo "---"
      52
      53 echo "Overwriting an associative array entry:"
      54 time for (( i=0; i< $count; i++)); do
      55         array2[$i]=$i
      56 done
      57
      58 echo "---"
      59
      60 echo "Linear reading an associative array entry:"
      61 time for (( i=0; i< $count; i++)); do
      62         simple=array2[$i]
      63 done
      64
      65 echo "---"
      66
      67 echo "Assigning a random number to a simple variable:"
      68 time for (( i=0; i< $count; i++)); do
      69         simple=$RANDOM
      70 done
      71
      72 echo "---"
      73
      74 echo "Assign a sparse numeric index array entry randomly into 64k cells:"
      75 time for (( i=0; i< $count; i++)); do
      76         array3[$RANDOM]=$i
      77 done
      78
      79 echo "---"
      80
      81 echo "Reading sparse numeric index array entry:"
      82 time for value in "${array3[@]}"i; do
      83         simple=$value
      84 done
      85
      86 echo "---"
      87
      88 echo "Assigning a sparse associative array entry randomly into 64k cells:"
      89 time for (( i=0; i< $count; i++)); do
      90         array4[$RANDOM]=$i
      91 done
      92
      93 echo "---"
      94
      95 echo "Reading sparse associative index array entry:"
      96 time for value in "${array4[@]}"; do
      97         simple=$value
      98 done
      99
     100 exit $?

Prev | Home | Next
Bibliography |  | Reference Cards