These scripts, while not fitting into the text of this document, do illustrate some interesting shell programming techniques. They are useful, too. Have fun analyzing and running them.
Example A-1. manview: Viewing formatted manpages
1 #!/bin/bash 2 # manview.sh: Formats the source of a man page for viewing. 3 4 # This script is useful when writing man page source. 5 # It lets you look at the intermediate results on the fly 6 #+ while working on it. 7 8 E_WRONGARGS=65 9 10 if [ -z "$1" ] 11 then 12 echo "Usage: `basename $0` filename" 13 exit $E_WRONGARGS 14 fi 15 16 # --------------------------- 17 groff -Tascii -man $1 | less 18 # From the man page for groff. 19 # --------------------------- 20 21 # If the man page includes tables and/or equations, 22 #+ then the above code will barf. 23 # The following line can handle such cases. 24 # 25 # gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man 26 # 27 # Thanks, S.C. 28 29 exit 0 |
Example A-2. mailformat: Formatting an e-mail message
1 #!/bin/bash 2 # mail-format.sh: Format e-mail messages. 3 4 # Gets rid of carets, tabs, also fold excessively long lines. 5 6 # ================================================================= 7 # Standard Check for Script Argument(s) 8 ARGS=1 9 E_BADARGS=65 10 E_NOFILE=66 11 12 if [ $# -ne $ARGS ] # Correct number of arguments passed to script? 13 then 14 echo "Usage: `basename $0` filename" 15 exit $E_BADARGS 16 fi 17 18 if [ -f "$1" ] # Check if file exists. 19 then 20 file_name=$1 21 else 22 echo "File \"$1\" does not exist." 23 exit $E_NOFILE 24 fi 25 # ================================================================= 26 27 MAXWIDTH=70 # Width to fold long lines to. 28 29 # Delete carets and tabs at beginning of lines, 30 #+ then fold lines to $MAXWIDTH characters. 31 sed ' 32 s/^>// 33 s/^ *>// 34 s/^ *// 35 s/ *// 36 ' $1 | fold -s --width=$MAXWIDTH 37 # -s option to "fold" breaks lines at whitespace, if possible. 38 39 # This script was inspired by an article in a well-known trade journal 40 #+ extolling a 164K Windows utility with similar functionality. 41 # 42 # An nice set of text processing utilities and an efficient 43 #+ scripting language provide an alternative to bloated executables. 44 45 exit 0 |
Example A-3. rn: A simple-minded file rename utility
This script is a modification of Example 12-18.
1 #! /bin/bash 2 # 3 # Very simpleminded filename "rename" utility (based on "lowercase.sh"). 4 # 5 # The "ren" utility, by Vladimir Lanin (lanin@csd2.nyu.edu), 6 #+ does a much better job of this. 7 8 9 ARGS=2 10 E_BADARGS=65 11 ONE=1 # For getting singular/plural right (see below). 12 13 if [ $# -ne "$ARGS" ] 14 then 15 echo "Usage: `basename $0` old-pattern new-pattern" 16 # As in "rn gif jpg", which renames all gif files in working directory to jpg. 17 exit $E_BADARGS 18 fi 19 20 number=0 # Keeps track of how many files actually renamed. 21 22 23 for filename in *$1* #Traverse all matching files in directory. 24 do 25 if [ -f "$filename" ] # If finds match... 26 then 27 fname=`basename $filename` # Strip off path. 28 n=`echo $fname | sed -e "s/$1/$2/"` # Substitute new for old in filename. 29 mv $fname $n # Rename. 30 let "number += 1" 31 fi 32 done 33 34 if [ "$number" -eq "$ONE" ] # For correct grammar. 35 then 36 echo "$number file renamed." 37 else 38 echo "$number files renamed." 39 fi 40 41 exit 0 42 43 44 # Exercises: 45 # --------- 46 # What type of files will this not work on? 47 # How can this be fixed? 48 # 49 # Rewrite this script to process all the files in a directory 50 #+ containing spaces in their names, and to rename them, 51 #+ substituting an underscore for each space. |
Example A-4. blank-rename: renames filenames containing blanks
This is an even simpler-minded version of previous script.
1 #! /bin/bash 2 # blank-rename.sh 3 # 4 # Substitutes underscores for blanks in all the filenames in a directory. 5 6 ONE=1 # For getting singular/plural right (see below). 7 number=0 # Keeps track of how many files actually renamed. 8 FOUND=0 # Successful return value. 9 10 for filename in * #Traverse all files in directory. 11 do 12 echo "$filename" | grep -q " " # Check whether filename 13 if [ $? -eq $FOUND ] #+ contains space(s). 14 then 15 fname=$filename # Strip off path. 16 n=`echo $fname | sed -e "s/ /_/g"` # Substitute underscore for blank. 17 mv "$fname" "$n" # Do the actual renaming. 18 let "number += 1" 19 fi 20 done 21 22 if [ "$number" -eq "$ONE" ] # For correct grammar. 23 then 24 echo "$number file renamed." 25 else 26 echo "$number files renamed." 27 fi 28 29 exit 0 |
Example A-5. encryptedpw: Uploading to an ftp site, using a locally encrypted password
1 #!/bin/bash 2 3 # Example "ex72.sh" modified to use encrypted password. 4 5 # Note that this is still rather insecure, 6 #+ since the decrypted password is sent in the clear. 7 # Use something like "ssh" if this is a concern. 8 9 E_BADARGS=65 10 11 if [ -z "$1" ] 12 then 13 echo "Usage: `basename $0` filename" 14 exit $E_BADARGS 15 fi 16 17 Username=bozo # Change to suit. 18 pword=/home/bozo/secret/password_encrypted.file 19 # File containing encrypted password. 20 21 Filename=`basename $1` # Strips pathname out of file name 22 23 Server="XXX" 24 Directory="YYY" # Change above to actual server name & directory. 25 26 27 Password=`cruft <$pword` # Decrypt password. 28 # Uses the author's own "cruft" file encryption package, 29 #+ based on the classic "onetime pad" algorithm, 30 #+ and obtainable from: 31 #+ Primary-site: ftp://ibiblio.org/pub/Linux/utils/file 32 #+ cruft-0.2.tar.gz [16k] 33 34 35 ftp -n $Server <<End-Of-Session 36 user $Username $Password 37 binary 38 bell 39 cd $Directory 40 put $Filename 41 bye 42 End-Of-Session 43 # -n option to "ftp" disables auto-logon. 44 # "bell" rings 'bell' after each file transfer. 45 46 exit 0 |
Example A-6. copy-cd: Copying a data CD
1 #!/bin/bash 2 # copy-cd.sh: copying a data CD 3 4 CDROM=/dev/cdrom # CD ROM device 5 OF=/home/bozo/projects/cdimage.iso # output file 6 # /xxxx/xxxxxxx/ Change to suit your system. 7 BLOCKSIZE=2048 8 SPEED=2 # May use higher speed if supported. 9 DEVICE=cdrom 10 # DEVICE="0,0" on older versions of cdrecord. 11 12 echo; echo "Insert source CD, but do *not* mount it." 13 echo "Press ENTER when ready. " 14 read ready # Wait for input, $ready not used. 15 16 echo; echo "Copying the source CD to $OF." 17 echo "This may take a while. Please be patient." 18 19 dd if=$CDROM of=$OF bs=$BLOCKSIZE # Raw device copy. 20 21 22 echo; echo "Remove data CD." 23 echo "Insert blank CDR." 24 echo "Press ENTER when ready. " 25 read ready # Wait for input, $ready not used. 26 27 echo "Copying $OF to CDR." 28 29 cdrecord -v -isosize speed=$SPEED dev=$DEVICE $OF 30 # Uses Joerg Schilling's "cdrecord" package (see its docs). 31 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html 32 33 34 echo; echo "Done copying $OF to CDR on device $CDROM." 35 36 echo "Do you want to erase the image file (y/n)? " # Probably a huge file. 37 read answer 38 39 case "$answer" in 40 [yY]) rm -f $OF 41 echo "$OF erased." 42 ;; 43 *) echo "$OF not erased.";; 44 esac 45 46 echo 47 48 # Exercise: 49 # Change the above "case" statement to also accept "yes" and "Yes" as input. 50 51 exit 0 |
Example A-7. Collatz series
1 #!/bin/bash 2 # collatz.sh 3 4 # The notorious "hailstone" or Collatz series. 5 # ------------------------------------------- 6 # 1) Get the integer "seed" from the command line. 7 # 2) NUMBER <--- seed 8 # 3) Print NUMBER. 9 # 4) If NUMBER is even, divide by 2, or 10 # 5)+ if odd, multiply by 3 and add 1. 11 # 6) NUMBER <--- result 12 # 7) Loop back to step 3 (for specified number of iterations). 13 # 14 # The theory is that every sequence, 15 #+ no matter how large the initial value, 16 #+ eventually settles down to repeating "4,2,1..." cycles, 17 #+ even after fluctuating through a wide range of values. 18 # 19 # This is an instance of an "iterate", 20 #+ an operation that feeds its output back into the input. 21 # Sometimes the result is a "chaotic" series. 22 23 24 MAX_ITERATIONS=200 25 # For large seed numbers (>32000), increase MAX_ITERATIONS. 26 27 h=${1:-$$} # Seed 28 # Use $PID as seed, 29 #+ if not specified as command-line arg. 30 31 echo 32 echo "C($h) --- $MAX_ITERATIONS Iterations" 33 echo 34 35 for ((i=1; i<=MAX_ITERATIONS; i++)) 36 do 37 38 echo -n "$h " 39 # ^^^^^ 40 # tab 41 42 let "remainder = h % 2" 43 if [ "$remainder" -eq 0 ] # Even? 44 then 45 let "h /= 2" # Divide by 2. 46 else 47 let "h = h*3 + 1" # Multiply by 3 and add 1. 48 fi 49 50 51 COLUMNS=10 # Output 10 values per line. 52 let "line_break = i % $COLUMNS" 53 if [ "$line_break" -eq 0 ] 54 then 55 echo 56 fi 57 58 done 59 60 echo 61 62 # For more information on this mathematical function, 63 #+ see "Computers, Pattern, Chaos, and Beauty", by Pickover, p. 185 ff., 64 #+ as listed in the bibliography. 65 66 exit 0 |
Example A-8. days-between: Calculate number of days between two dates
1 #!/bin/bash 2 # days-between.sh: Number of days between two dates. 3 # Usage: ./days-between.sh [M]M/[D]D/YYYY [M]M/[D]D/YYYY 4 # 5 # Note: Script modified to account for changes in Bash 2.05b 6 #+ that closed the loophole permitting large negative 7 #+ integer return values. 8 9 ARGS=2 # Two command line parameters expected. 10 E_PARAM_ERR=65 # Param error. 11 12 REFYR=1600 # Reference year. 13 CENTURY=100 14 DIY=365 15 ADJ_DIY=367 # Adjusted for leap year + fraction. 16 MIY=12 17 DIM=31 18 LEAPCYCLE=4 19 20 MAXRETVAL=255 # Largest permissable 21 #+ positive return value from a function. 22 23 diff= # Declare global variable for date difference. 24 value= # Declare global variable for absolute value. 25 day= # Declare globals for day, month, year. 26 month= 27 year= 28 29 30 Param_Error () # Command line parameters wrong. 31 { 32 echo "Usage: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY" 33 echo " (date must be after 1/3/1600)" 34 exit $E_PARAM_ERR 35 } 36 37 38 Parse_Date () # Parse date from command line params. 39 { 40 month=${1%%/**} 41 dm=${1%/**} # Day and month. 42 day=${dm#*/} 43 let "year = `basename $1`" # Not a filename, but works just the same. 44 } 45 46 47 check_date () # Checks for invalid date(s) passed. 48 { 49 [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error 50 # Exit script on bad value(s). 51 # Uses "or-list / and-list". 52 # 53 # Exercise: Implement more rigorous date checking. 54 } 55 56 57 strip_leading_zero () # Better to strip possible leading zero(s) 58 { #+ from day and/or month 59 return ${1#0} #+ since otherwise Bash will interpret them 60 } #+ as octal values (POSIX.2, sect 2.9.2.1). 61 62 63 day_index () # Gauss' Formula: 64 { # Days from Jan. 3, 1600 to date passed as param. 65 66 day=$1 67 month=$2 68 year=$3 69 70 let "month = $month - 2" 71 if [ "$month" -le 0 ] 72 then 73 let "month += 12" 74 let "year -= 1" 75 fi 76 77 let "year -= $REFYR" 78 let "indexyr = $year / $CENTURY" 79 80 81 let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM" 82 # For an in-depth explanation of this algorithm, see 83 #+ http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm 84 85 86 echo $Days 87 88 } 89 90 91 calculate_difference () # Difference between to day indices. 92 { 93 let "diff = $1 - $2" # Global variable. 94 } 95 96 97 abs () # Absolute value 98 { # Uses global "value" variable. 99 if [ "$1" -lt 0 ] # If negative 100 then #+ then 101 let "value = 0 - $1" #+ change sign, 102 else #+ else 103 let "value = $1" #+ leave it alone. 104 fi 105 } 106 107 108 109 if [ $# -ne "$ARGS" ] # Require two command line params. 110 then 111 Param_Error 112 fi 113 114 Parse_Date $1 115 check_date $day $month $year # See if valid date. 116 117 strip_leading_zero $day # Remove any leading zeroes 118 day=$? #+ on day and/or month. 119 strip_leading_zero $month 120 month=$? 121 122 let "date1 = `day_index $day $month $year`" 123 124 125 Parse_Date $2 126 check_date $day $month $year 127 128 strip_leading_zero $day 129 day=$? 130 strip_leading_zero $month 131 month=$? 132 133 date2=$(day_index $day $month $year) # Command substitution. 134 135 136 calculate_difference $date1 $date2 137 138 abs $diff # Make sure it's positive. 139 diff=$value 140 141 echo $diff 142 143 exit 0 144 # Compare this script with 145 #+ the implementation of Gauss' Formula in a C program at: 146 #+ http://buschencrew.hypermart.net/software/datedif |
Example A-9. Make a "dictionary"
1 #!/bin/bash 2 # makedict.sh [make dictionary] 3 4 # Modification of /usr/sbin/mkdict script. 5 # Original script copyright 1993, by Alec Muffett. 6 # 7 # This modified script included in this document in a manner 8 #+ consistent with the "LICENSE" document of the "Crack" package 9 #+ that the original script is a part of. 10 11 # This script processes text files to produce a sorted list 12 #+ of words found in the files. 13 # This may be useful for compiling dictionaries 14 #+ and for lexicographic research. 15 16 17 E_BADARGS=65 18 19 if [ ! -r "$1" ] # Need at least one 20 then #+ valid file argument. 21 echo "Usage: $0 files-to-process" 22 exit $E_BADARGS 23 fi 24 25 26 # SORT="sort" # No longer necessary to define options 27 #+ to sort. Changed from original script. 28 29 cat $* | # Contents of specified files to stdout. 30 tr A-Z a-z | # Convert to lowercase. 31 tr ' ' '\012' | # New: change spaces to newlines. 32 # tr -cd '\012[a-z][0-9]' | # Get rid of everything non-alphanumeric 33 #+ (original script). 34 tr -c '\012a-z' '\012' | # Rather than deleting 35 #+ now change non-alpha to newlines. 36 sort | # $SORT options unnecessary now. 37 uniq | # Remove duplicates. 38 grep -v '^#' | # Delete lines beginning with a hashmark. 39 grep -v '^$' # Delete blank lines. 40 41 exit 0 |
Example A-10. Soundex conversion
1 #!/bin/bash 2 # soundex.sh: Calculate "soundex" code for names 3 4 # ======================================================= 5 # Soundex script 6 # by 7 # Mendel Cooper 8 # thegrendel@theriver.com 9 # 23 January, 2002 10 # 11 # Placed in the Public Domain. 12 # 13 # A slightly different version of this script appeared in 14 #+ Ed Schaefer's July, 2002 "Shell Corner" column 15 #+ in "Unix Review" on-line, 16 #+ http://www.unixreview.com/documents/uni1026336632258/ 17 # ======================================================= 18 19 20 ARGCOUNT=1 # Need name as argument. 21 E_WRONGARGS=70 22 23 if [ $# -ne "$ARGCOUNT" ] 24 then 25 echo "Usage: `basename $0` name" 26 exit $E_WRONGARGS 27 fi 28 29 30 assign_value () # Assigns numerical value 31 { #+ to letters of name. 32 33 val1=bfpv # 'b,f,p,v' = 1 34 val2=cgjkqsxz # 'c,g,j,k,q,s,x,z' = 2 35 val3=dt # etc. 36 val4=l 37 val5=mn 38 val6=r 39 40 # Exceptionally clever use of 'tr' follows. 41 # Try to figure out what is going on here. 42 43 value=$( echo "$1" \ 44 | tr -d wh \ 45 | tr $val1 1 | tr $val2 2 | tr $val3 3 \ 46 | tr $val4 4 | tr $val5 5 | tr $val6 6 \ 47 | tr -s 123456 \ 48 | tr -d aeiouy ) 49 50 # Assign letter values. 51 # Remove duplicate numbers, except when separated by vowels. 52 # Ignore vowels, except as separators, so delete them last. 53 # Ignore 'w' and 'h', even as separators, so delete them first. 54 # 55 # The above command substitution lays more pipe than a plumber <g>. 56 57 } 58 59 60 input_name="$1" 61 echo 62 echo "Name = $input_name" 63 64 65 # Change all characters of name input to lowercase. 66 # ------------------------------------------------ 67 name=$( echo $input_name | tr A-Z a-z ) 68 # ------------------------------------------------ 69 # Just in case argument to script is mixed case. 70 71 72 # Prefix of soundex code: first letter of name. 73 # -------------------------------------------- 74 75 76 char_pos=0 # Initialize character position. 77 prefix0=${name:$char_pos:1} 78 prefix=`echo $prefix0 | tr a-z A-Z` 79 # Uppercase 1st letter of soundex. 80 81 let "char_pos += 1" # Bump character position to 2nd letter of name. 82 name1=${name:$char_pos} 83 84 85 # ++++++++++++++++++++++++++ Exception Patch +++++++++++++++++++++++++++++++++ 86 # Now, we run both the input name and the name shifted one char to the right 87 #+ through the value-assigning function. 88 # If we get the same value out, that means that the first two characters 89 #+ of the name have the same value assigned, and that one should cancel. 90 # However, we also need to test whether the first letter of the name 91 #+ is a vowel or 'w' or 'h', because otherwise this would bollix things up. 92 93 char1=`echo $prefix | tr A-Z a-z` # First letter of name, lowercased. 94 95 assign_value $name 96 s1=$value 97 assign_value $name1 98 s2=$value 99 assign_value $char1 100 s3=$value 101 s3=9$s3 # If first letter of name is a vowel 102 #+ or 'w' or 'h', 103 #+ then its "value" will be null (unset). 104 #+ Therefore, set it to 9, an otherwise 105 #+ unused value, which can be tested for. 106 107 108 if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]] 109 then 110 suffix=$s2 111 else 112 suffix=${s2:$char_pos} 113 fi 114 # ++++++++++++++++++++++ end Exception Patch +++++++++++++++++++++++++++++++++ 115 116 117 padding=000 # Use at most 3 zeroes to pad. 118 119 120 soun=$prefix$suffix$padding # Pad with zeroes. 121 122 MAXLEN=4 # Truncate to maximum of 4 chars. 123 soundex=${soun:0:$MAXLEN} 124 125 echo "Soundex = $soundex" 126 127 echo 128 129 # The soundex code is a method of indexing and classifying names 130 #+ by grouping together the ones that sound alike. 131 # The soundex code for a given name is the first letter of the name, 132 #+ followed by a calculated three-number code. 133 # Similar sounding names should have almost the same soundex codes. 134 135 # Examples: 136 # Smith and Smythe both have a "S-530" soundex. 137 # Harrison = H-625 138 # Hargison = H-622 139 # Harriman = H-655 140 141 # This works out fairly well in practice, but there are numerous anomalies. 142 # 143 # 144 # The U.S. Census and certain other governmental agencies use soundex, 145 # as do genealogical researchers. 146 # 147 # For more information, 148 #+ see the "National Archives and Records Administration home page", 149 #+ http://www.nara.gov/genealogy/soundex/soundex.html 150 151 152 153 # Exercise: 154 # -------- 155 # Simplify the "Exception Patch" section of this script. 156 157 exit 0 |
Example A-11. "Game of Life"
1 #!/bin/bash 2 # life.sh: "Life in the Slow Lane" 3 4 # ##################################################################### # 5 # This is the Bash script version of John Conway's "Game of Life". # 6 # "Life" is a simple implementation of cellular automata. # 7 # --------------------------------------------------------------------- # 8 # On a rectangular grid, let each "cell" be either "living" or "dead". # 9 # Designate a living cell with a dot, and a dead one with a blank space.# 10 # Begin with an arbitrarily drawn dot-and-blank grid, # 11 #+ and let this be the starting generation, "generation 0". # 12 # Determine each successive generation by the following rules: # 13 # 1) Each cell has 8 neighbors, the adjoining cells # 14 #+ left, right, top, bottom, and the 4 diagonals. # 15 # 123 # 16 # 4*5 # 17 # 678 # 18 # # 19 # 2) A living cell with either 2 or 3 living neighbors remains alive. # 20 # 3) A dead cell with 3 living neighbors becomes alive (a "birth"). # 21 SURVIVE=2 # 22 BIRTH=3 # 23 # 4) All other cases result in dead cells. # 24 # ##################################################################### # 25 26 27 startfile=gen0 # Read the starting generation from the file "gen0". 28 # Default, if no other file specified when invoking script. 29 # 30 if [ -n "$1" ] # Specify another "generation 0" file. 31 then 32 if [ -e "$1" ] # Check for existence. 33 then 34 startfile="$1" 35 fi 36 fi 37 38 39 ALIVE1=. 40 DEAD1=_ 41 # Represent living and "dead" cells in the start-up file. 42 43 # This script uses a 10 x 10 grid (may be increased, 44 #+ but a large grid will will cause very slow execution). 45 ROWS=10 46 COLS=10 47 48 GENERATIONS=10 # How many generations to cycle through. 49 # Adjust this upwards, 50 #+ if you have time on your hands. 51 52 NONE_ALIVE=80 # Exit status on premature bailout, 53 #+ if no cells left alive. 54 TRUE=0 55 FALSE=1 56 ALIVE=0 57 DEAD=1 58 59 avar= # Global; holds current generation. 60 generation=0 # Initialize generation count. 61 62 # ================================================================= 63 64 65 let "cells = $ROWS * $COLS" 66 # How many cells. 67 68 declare -a initial # Arrays containing "cells". 69 declare -a current 70 71 display () 72 { 73 74 alive=0 # How many cells "alive". 75 # Initially zero. 76 77 declare -a arr 78 arr=( `echo "$1"` ) # Convert passed arg to array. 79 80 element_count=${#arr[*]} 81 82 local i 83 local rowcheck 84 85 for ((i=0; i<$element_count; i++)) 86 do 87 88 # Insert newline at end of each row. 89 let "rowcheck = $i % ROWS" 90 if [ "$rowcheck" -eq 0 ] 91 then 92 echo # Newline. 93 echo -n " " # Indent. 94 fi 95 96 cell=${arr[i]} 97 98 if [ "$cell" = . ] 99 then 100 let "alive += 1" 101 fi 102 103 echo -n "$cell" | sed -e 's/_/ /g' 104 # Print out array and change underscores to spaces. 105 done 106 107 return 108 109 } 110 111 IsValid () # Test whether cell coordinate valid. 112 { 113 114 if [ -z "$1" -o -z "$2" ] # Mandatory arguments missing? 115 then 116 return $FALSE 117 fi 118 119 local row 120 local lower_limit=0 # Disallow negative coordinate. 121 local upper_limit 122 local left 123 local right 124 125 let "upper_limit = $ROWS * $COLS - 1" # Total number of cells. 126 127 128 if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ] 129 then 130 return $FALSE # Out of array bounds. 131 fi 132 133 row=$2 134 let "left = $row * $ROWS" # Left limit. 135 let "right = $left + $COLS - 1" # Right limit. 136 137 if [ "$1" -lt "$left" -o "$1" -gt "$right" ] 138 then 139 return $FALSE # Beyond row boundary. 140 fi 141 142 return $TRUE # Valid coordinate. 143 144 } 145 146 147 IsAlive () # Test whether cell is alive. 148 # Takes array, cell number, state of cell as arguments. 149 { 150 GetCount "$1" $2 # Get alive cell count in neighborhood. 151 local nhbd=$? 152 153 154 if [ "$nhbd" -eq "$BIRTH" ] # Alive in any case. 155 then 156 return $ALIVE 157 fi 158 159 if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ] 160 then # Alive only if previously alive. 161 return $ALIVE 162 fi 163 164 return $DEAD # Default. 165 166 } 167 168 169 GetCount () # Count live cells in passed cell's neighborhood. 170 # Two arguments needed: 171 # $1) variable holding array 172 # $2) cell number 173 { 174 local cell_number=$2 175 local array 176 local top 177 local center 178 local bottom 179 local r 180 local row 181 local i 182 local t_top 183 local t_cen 184 local t_bot 185 local count=0 186 local ROW_NHBD=3 187 188 array=( `echo "$1"` ) 189 190 let "top = $cell_number - $COLS - 1" # Set up cell neighborhood. 191 let "center = $cell_number - 1" 192 let "bottom = $cell_number + $COLS - 1" 193 let "r = $cell_number / $ROWS" 194 195 for ((i=0; i<$ROW_NHBD; i++)) # Traverse from left to right. 196 do 197 let "t_top = $top + $i" 198 let "t_cen = $center + $i" 199 let "t_bot = $bottom + $i" 200 201 202 let "row = $r" # Count center row of neighborhood. 203 IsValid $t_cen $row # Valid cell position? 204 if [ $? -eq "$TRUE" ] 205 then 206 if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive? 207 then # Yes? 208 let "count += 1" # Increment count. 209 fi 210 fi 211 212 let "row = $r - 1" # Count top row. 213 IsValid $t_top $row 214 if [ $? -eq "$TRUE" ] 215 then 216 if [ ${array[$t_top]} = "$ALIVE1" ] 217 then 218 let "count += 1" 219 fi 220 fi 221 222 let "row = $r + 1" # Count bottom row. 223 IsValid $t_bot $row 224 if [ $? -eq "$TRUE" ] 225 then 226 if [ ${array[$t_bot]} = "$ALIVE1" ] 227 then 228 let "count += 1" 229 fi 230 fi 231 232 done 233 234 235 if [ ${array[$cell_number]} = "$ALIVE1" ] 236 then 237 let "count -= 1" # Make sure value of tested cell itself 238 fi #+ is not counted. 239 240 241 return $count 242 243 } 244 245 next_gen () # Update generation array. 246 { 247 248 local array 249 local i=0 250 251 array=( `echo "$1"` ) # Convert passed arg to array. 252 253 while [ "$i" -lt "$cells" ] 254 do 255 IsAlive "$1" $i ${array[$i]} # Is cell alive? 256 if [ $? -eq "$ALIVE" ] 257 then # If alive, then 258 array[$i]=. #+ represent the cell as a period. 259 else 260 array[$i]="_" # Otherwise underscore 261 fi #+ (which will later be converted to space). 262 let "i += 1" 263 done 264 265 266 # let "generation += 1" # Increment generation count. 267 268 # Set variable to pass as parameter to "display" function. 269 avar=`echo ${array[@]}` # Convert array back to string variable. 270 display "$avar" # Display it. 271 echo; echo 272 echo "Generation $generation -- $alive alive" 273 274 if [ "$alive" -eq 0 ] 275 then 276 echo 277 echo "Premature exit: no more cells alive!" 278 exit $NONE_ALIVE # No point in continuing 279 fi #+ if no live cells. 280 281 } 282 283 284 # ========================================================= 285 286 # main () 287 288 # Load initial array with contents of startup file. 289 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\ 290 sed -e 's/\./\. /g' -e 's/_/_ /g'` ) 291 # Delete lines containing '#' comment character. 292 # Remove linefeeds and insert space between elements. 293 294 clear # Clear screen. 295 296 echo # Title 297 echo "=======================" 298 echo " $GENERATIONS generations" 299 echo " of" 300 echo "\"Life in the Slow Lane\"" 301 echo "=======================" 302 303 304 # -------- Display first generation. -------- 305 Gen0=`echo ${initial[@]}` 306 display "$Gen0" # Display only. 307 echo; echo 308 echo "Generation $generation -- $alive alive" 309 # ------------------------------------------- 310 311 312 let "generation += 1" # Increment generation count. 313 echo 314 315 # ------- Display second generation. ------- 316 Cur=`echo ${initial[@]}` 317 next_gen "$Cur" # Update & display. 318 # ------------------------------------------ 319 320 let "generation += 1" # Increment generation count. 321 322 # ------ Main loop for displaying subsequent generations ------ 323 while [ "$generation" -le "$GENERATIONS" ] 324 do 325 Cur="$avar" 326 next_gen "$Cur" 327 let "generation += 1" 328 done 329 # ============================================================== 330 331 echo 332 333 exit 0 334 335 # -------------------------------------------------------------- 336 # The grid in this script has a "boundary problem". 337 # The the top, bottom, and sides border on a void of dead cells. 338 # Exercise: Change the script to have the grid wrap around, 339 # + so that the left and right sides will "touch", 340 # + as will the top and bottom. |
Example A-12. Data file for "Game of Life"
1 # This is an example "generation 0" start-up file for "life.sh". 2 # -------------------------------------------------------------- 3 # The "gen0" file is a 10 x 10 grid using a period (.) for live cells, 4 #+ and an underscore (_) for dead ones. We cannot simply use spaces 5 #+ for dead cells in this file because of a peculiarity in Bash arrays. 6 # [Exercise for the reader: explain this.] 7 # 8 # Lines beginning with a '#' are comments, and the script ignores them. 9 __.__..___ 10 ___._.____ 11 ____.___.. 12 _._______. 13 ____._____ 14 ..__...___ 15 ____._____ 16 ___...____ 17 __.._..___ 18 _..___..__ |
+++
The following two scripts are by Mark Moraes of the University of Toronto. See the enclosed file "Moraes-COPYRIGHT" for permissions and restrictions.
Example A-13. behead: Removing mail and news message headers
1 #! /bin/sh 2 # Strips off the header from a mail/News message i.e. till the first 3 # empty line 4 # Mark Moraes, University of Toronto 5 6 # ==> These comments added by author of this document. 7 8 if [ $# -eq 0 ]; then 9 # ==> If no command line args present, then works on file redirected to stdin. 10 sed -e '1,/^$/d' -e '/^[ ]*$/d' 11 # --> Delete empty lines and all lines until 12 # --> first one beginning with white space. 13 else 14 # ==> If command line args present, then work on files named. 15 for i do 16 sed -e '1,/^$/d' -e '/^[ ]*$/d' $i 17 # --> Ditto, as above. 18 done 19 fi 20 21 # ==> Exercise: Add error checking and other options. 22 # ==> 23 # ==> Note that the small sed script repeats, except for the arg passed. 24 # ==> Does it make sense to embed it in a function? Why or why not? |
Example A-14. ftpget: Downloading files via ftp
1 #! /bin/sh 2 # $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $ 3 # Script to perform batch anonymous ftp. Essentially converts a list of 4 # of command line arguments into input to ftp. 5 # Simple, and quick - written as a companion to ftplist 6 # -h specifies the remote host (default prep.ai.mit.edu) 7 # -d specifies the remote directory to cd to - you can provide a sequence 8 # of -d options - they will be cd'ed to in turn. If the paths are relative, 9 # make sure you get the sequence right. Be careful with relative paths - 10 # there are far too many symlinks nowadays. 11 # (default is the ftp login directory) 12 # -v turns on the verbose option of ftp, and shows all responses from the 13 # ftp server. 14 # -f remotefile[:localfile] gets the remote file into localfile 15 # -m pattern does an mget with the specified pattern. Remember to quote 16 # shell characters. 17 # -c does a local cd to the specified directory 18 # For example, 19 # ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \ 20 # -d ../pub/R3/fixes -c ~/fixes -m 'fix*' 21 # will get xplaces.shar from ~ftp/contrib on expo.lcs.mit.edu, and put it in 22 # xplaces.sh in the current working directory, and get all fixes from 23 # ~ftp/pub/R3/fixes and put them in the ~/fixes directory. 24 # Obviously, the sequence of the options is important, since the equivalent 25 # commands are executed by ftp in corresponding order 26 # 27 # Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 28 # ==> Angle brackets changed to parens, so Docbook won't get indigestion. 29 # 30 31 32 # ==> These comments added by author of this document. 33 34 # PATH=/local/bin:/usr/ucb:/usr/bin:/bin 35 # export PATH 36 # ==> Above 2 lines from original script probably superfluous. 37 38 TMPFILE=/tmp/ftp.$$ 39 # ==> Creates temp file, using process id of script ($$) 40 # ==> to construct filename. 41 42 SITE=`domainname`.toronto.edu 43 # ==> 'domainname' similar to 'hostname' 44 # ==> May rewrite this to parameterize this for general use. 45 46 usage="Usage: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \ 47 [-c localdirectory] [-m filepattern] [-v]" 48 ftpflags="-i -n" 49 verbflag= 50 set -f # So we can use globbing in -m 51 set x `getopt vh:d:c:m:f: $*` 52 if [ $? != 0 ]; then 53 echo $usage 54 exit 65 55 fi 56 shift 57 trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15 58 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}" 59 # ==> Added quotes (recommended in complex echoes). 60 echo binary >> ${TMPFILE} 61 for i in $* # ==> Parse command line args. 62 do 63 case $i in 64 -v) verbflag=-v; echo hash >> ${TMPFILE}; shift;; 65 -h) remhost=$2; shift 2;; 66 -d) echo cd $2 >> ${TMPFILE}; 67 if [ x${verbflag} != x ]; then 68 echo pwd >> ${TMPFILE}; 69 fi; 70 shift 2;; 71 -c) echo lcd $2 >> ${TMPFILE}; shift 2;; 72 -m) echo mget "$2" >> ${TMPFILE}; shift 2;; 73 -f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`; 74 echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;; 75 --) shift; break;; 76 esac 77 done 78 if [ $# -ne 0 ]; then 79 echo $usage 80 exit 65 # ==> Changed from "exit 2" to conform with standard. 81 fi 82 if [ x${verbflag} != x ]; then 83 ftpflags="${ftpflags} -v" 84 fi 85 if [ x${remhost} = x ]; then 86 remhost=prep.ai.mit.edu 87 # ==> Rewrite to match your favorite ftp site. 88 fi 89 echo quit >> ${TMPFILE} 90 # ==> All commands saved in tempfile. 91 92 ftp ${ftpflags} ${remhost} < ${TMPFILE} 93 # ==> Now, tempfile batch processed by ftp. 94 95 rm -f ${TMPFILE} 96 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile). 97 98 99 # ==> Exercises: 100 # ==> --------- 101 # ==> 1) Add error checking. 102 # ==> 2) Add bells & whistles. |
+
Antek Sawicki contributed the following script, which makes very clever use of the parameter substitution operators discussed in Section 9.3.
Example A-15. password: Generating random 8-character passwords
1 #!/bin/bash 2 # May need to be invoked with #!/bin/bash2 on older machines. 3 # 4 # Random password generator for Bash 2.x by Antek Sawicki <tenox@tenox.tc>, 5 # who generously gave permission to the document author to use it here. 6 # 7 # ==> Comments added by document author ==> 8 9 10 MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 11 # ==> Password will consist of alphanumeric characters. 12 LENGTH="8" 13 # ==> May change 'LENGTH' for longer password. 14 15 16 while [ "${n:=1}" -le "$LENGTH" ] 17 # ==> Recall that := is "default substitution" operator. 18 # ==> So, if 'n' has not been initialized, set it to 1. 19 do 20 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}" 21 # ==> Very clever, but tricky. 22 23 # ==> Starting from the innermost nesting... 24 # ==> ${#MATRIX} returns length of array MATRIX. 25 26 # ==> $RANDOM%${#MATRIX} returns random number between 1 27 # ==> and [length of MATRIX] - 1. 28 29 # ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1} 30 # ==> returns expansion of MATRIX at random position, by length 1. 31 # ==> See {var:pos:len} parameter substitution in Chapter 9. 32 # ==> and the associated examples. 33 34 # ==> PASS=... simply pastes this result onto previous PASS (concatenation). 35 36 # ==> To visualize this more clearly, uncomment the following line 37 # echo "$PASS" 38 # ==> to see PASS being built up, 39 # ==> one character at a time, each iteration of the loop. 40 41 let n+=1 42 # ==> Increment 'n' for next pass. 43 done 44 45 echo "$PASS" # ==> Or, redirect to a file, as desired. 46 47 exit 0 |
+
James R. Van Zandt contributed this script, which uses named pipes and, in his words, "really exercises quoting and escaping".
Example A-16. fifo: Making daily backups, using named pipes
1 #!/bin/bash 2 # ==> Script by James R. Van Zandt, and used here with his permission. 3 4 # ==> Comments added by author of this document. 5 6 7 HERE=`uname -n` # ==> hostname 8 THERE=bilbo 9 echo "starting remote backup to $THERE at `date +%r`" 10 # ==> `date +%r` returns time in 12-hour format, i.e. "08:08:34 PM". 11 12 # make sure /pipe really is a pipe and not a plain file 13 rm -rf /pipe 14 mkfifo /pipe # ==> Create a "named pipe", named "/pipe". 15 16 # ==> 'su xyz' runs commands as user "xyz". 17 # ==> 'ssh' invokes secure shell (remote login client). 18 su xyz -c "ssh $THERE \"cat >/home/xyz/backup/${HERE}-daily.tar.gz\" < /pipe"& 19 cd / 20 tar -czf - bin boot dev etc home info lib man root sbin share usr var >/pipe 21 # ==> Uses named pipe, /pipe, to communicate between processes: 22 # ==> 'tar/gzip' writes to /pipe and 'ssh' reads from /pipe. 23 24 # ==> The end result is this backs up the main directories, from / on down. 25 26 # ==> What are the advantages of a "named pipe" in this situation, 27 # ==> as opposed to an "anonymous pipe", with |? 28 # ==> Will an anonymous pipe even work here? 29 30 31 exit 0 |
+
Stephane Chazelas contributed the following script to demonstrate that generating prime numbers does not require arrays.
Example A-17. Generating prime numbers using the modulo operator
1 #!/bin/bash 2 # primes.sh: Generate prime numbers, without using arrays. 3 # Script contributed by Stephane Chazelas. 4 5 # This does *not* use the classic "Sieve of Eratosthenes" algorithm, 6 #+ but instead uses the more intuitive method of testing each candidate number 7 #+ for factors (divisors), using the "%" modulo operator. 8 9 10 LIMIT=1000 # Primes 2 - 1000 11 12 Primes() 13 { 14 (( n = $1 + 1 )) # Bump to next integer. 15 shift # Next parameter in list. 16 # echo "_n=$n i=$i_" 17 18 if (( n == LIMIT )) 19 then echo $* 20 return 21 fi 22 23 for i; do # "i" gets set to "@", previous values of $n. 24 # echo "-n=$n i=$i-" 25 (( i * i > n )) && break # Optimization. 26 (( n % i )) && continue # Sift out non-primes using modulo operator. 27 Primes $n $@ # Recursion inside loop. 28 return 29 done 30 31 Primes $n $@ $n # Recursion outside loop. 32 # Successively accumulate positional parameters. 33 # "$@" is the accumulating list of primes. 34 } 35 36 Primes 1 37 38 exit 0 39 40 # Uncomment lines 16 and 24 to help figure out what is going on. 41 42 # Compare the speed of this algorithm for generating primes 43 #+ with the Sieve of Eratosthenes (ex68.sh). 44 45 # Exercise: Rewrite this script without recursion, for faster execution. |
+
This is Rick Boivie's revision of Jordi Sanfeliu's tree script.
Example A-18. tree: Displaying a directory tree
1 #!/bin/bash 2 # tree.sh 3 4 # Written by Rick Boivie. 5 # Used with permission. 6 # This is a revised and simplified version of a script 7 # by Jordi Sanfeliu (and patched by Ian Kjos). 8 # This script replaces the earlier version used in 9 #+ previous releases of the Advanced Bash Scripting Guide. 10 11 # ==> Comments added by the author of this document. 12 13 14 search () { 15 for dir in `echo *` 16 # ==> `echo *` lists all the files in current working directory, 17 #+ ==> without line breaks. 18 # ==> Similar effect to for dir in * 19 # ==> but "dir in `echo *`" will not handle filenames with blanks. 20 do 21 if [ -d "$dir" ] ; then # ==> If it is a directory (-d)... 22 zz=0 # ==> Temp variable, keeping track of directory level. 23 while [ $zz != $1 ] # Keep track of inner nested loop. 24 do 25 echo -n "| " # ==> Display vertical connector symbol, 26 # ==> with 2 spaces & no line feed in order to indent. 27 zz=`expr $zz + 1` # ==> Increment zz. 28 done 29 30 if [ -L "$dir" ] ; then # ==> If directory is a symbolic link... 31 echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'` 32 # ==> Display horiz. connector and list directory name, but... 33 # ==> delete date/time part of long listing. 34 else 35 echo "+---$dir" # ==> Display horizontal connector symbol... 36 # ==> and print directory name. 37 numdirs=`expr $numdirs + 1` # ==> Increment directory count. 38 if cd "$dir" ; then # ==> If can move to subdirectory... 39 search `expr $1 + 1` # with recursion ;-) 40 # ==> Function calls itself. 41 cd .. 42 fi 43 fi 44 fi 45 done 46 } 47 48 if [ $# != 0 ] ; then 49 cd $1 # move to indicated directory. 50 #else # stay in current directory 51 fi 52 53 echo "Initial directory = `pwd`" 54 numdirs=0 55 56 search 0 57 echo "Total directories = $numdirs" 58 59 exit 0 |
Noah Friedman gave permission to use his string function script, which essentially reproduces some of the C-library string manipulation functions.
Example A-19. string functions: C-like string functions
1 #!/bin/bash 2 3 # string.bash --- bash emulation of string(3) library routines 4 # Author: Noah Friedman <friedman@prep.ai.mit.edu> 5 # ==> Used with his kind permission in this document. 6 # Created: 1992-07-01 7 # Last modified: 1993-09-29 8 # Public domain 9 10 # Conversion to bash v2 syntax done by Chet Ramey 11 12 # Commentary: 13 # Code: 14 15 #:docstring strcat: 16 # Usage: strcat s1 s2 17 # 18 # Strcat appends the value of variable s2 to variable s1. 19 # 20 # Example: 21 # a="foo" 22 # b="bar" 23 # strcat a b 24 # echo $a 25 # => foobar 26 # 27 #:end docstring: 28 29 ###;;;autoload ==> Autoloading of function commented out. 30 function strcat () 31 { 32 local s1_val s2_val 33 34 s1_val=${!1} # indirect variable expansion 35 s2_val=${!2} 36 eval "$1"=\'"${s1_val}${s2_val}"\' 37 # ==> eval $1='${s1_val}${s2_val}' avoids problems, 38 # ==> if one of the variables contains a single quote. 39 } 40 41 #:docstring strncat: 42 # Usage: strncat s1 s2 $n 43 # 44 # Line strcat, but strncat appends a maximum of n characters from the value 45 # of variable s2. It copies fewer if the value of variabl s2 is shorter 46 # than n characters. Echoes result on stdout. 47 # 48 # Example: 49 # a=foo 50 # b=barbaz 51 # strncat a b 3 52 # echo $a 53 # => foobar 54 # 55 #:end docstring: 56 57 ###;;;autoload 58 function strncat () 59 { 60 local s1="$1" 61 local s2="$2" 62 local -i n="$3" 63 local s1_val s2_val 64 65 s1_val=${!s1} # ==> indirect variable expansion 66 s2_val=${!s2} 67 68 if [ ${#s2_val} -gt ${n} ]; then 69 s2_val=${s2_val:0:$n} # ==> substring extraction 70 fi 71 72 eval "$s1"=\'"${s1_val}${s2_val}"\' 73 # ==> eval $1='${s1_val}${s2_val}' avoids problems, 74 # ==> if one of the variables contains a single quote. 75 } 76 77 #:docstring strcmp: 78 # Usage: strcmp $s1 $s2 79 # 80 # Strcmp compares its arguments and returns an integer less than, equal to, 81 # or greater than zero, depending on whether string s1 is lexicographically 82 # less than, equal to, or greater than string s2. 83 #:end docstring: 84 85 ###;;;autoload 86 function strcmp () 87 { 88 [ "$1" = "$2" ] && return 0 89 90 [ "${1}" '<' "${2}" ] > /dev/null && return -1 91 92 return 1 93 } 94 95 #:docstring strncmp: 96 # Usage: strncmp $s1 $s2 $n 97 # 98 # Like strcmp, but makes the comparison by examining a maximum of n 99 # characters (n less than or equal to zero yields equality). 100 #:end docstring: 101 102 ###;;;autoload 103 function strncmp () 104 { 105 if [ -z "${3}" -o "${3}" -le "0" ]; then 106 return 0 107 fi 108 109 if [ ${3} -ge ${#1} -a ${3} -ge ${#2} ]; then 110 strcmp "$1" "$2" 111 return $? 112 else 113 s1=${1:0:$3} 114 s2=${2:0:$3} 115 strcmp $s1 $s2 116 return $? 117 fi 118 } 119 120 #:docstring strlen: 121 # Usage: strlen s 122 # 123 # Strlen returns the number of characters in string literal s. 124 #:end docstring: 125 126 ###;;;autoload 127 function strlen () 128 { 129 eval echo "\${#${1}}" 130 # ==> Returns the length of the value of the variable 131 # ==> whose name is passed as an argument. 132 } 133 134 #:docstring strspn: 135 # Usage: strspn $s1 $s2 136 # 137 # Strspn returns the length of the maximum initial segment of string s1, 138 # which consists entirely of characters from string s2. 139 #:end docstring: 140 141 ###;;;autoload 142 function strspn () 143 { 144 # Unsetting IFS allows whitespace to be handled as normal chars. 145 local IFS= 146 local result="${1%%[!${2}]*}" 147 148 echo ${#result} 149 } 150 151 #:docstring strcspn: 152 # Usage: strcspn $s1 $s2 153 # 154 # Strcspn returns the length of the maximum initial segment of string s1, 155 # which consists entirely of characters not from string s2. 156 #:end docstring: 157 158 ###;;;autoload 159 function strcspn () 160 { 161 # Unsetting IFS allows whitspace to be handled as normal chars. 162 local IFS= 163 local result="${1%%[${2}]*}" 164 165 echo ${#result} 166 } 167 168 #:docstring strstr: 169 # Usage: strstr s1 s2 170 # 171 # Strstr echoes a substring starting at the first occurrence of string s2 in 172 # string s1, or nothing if s2 does not occur in the string. If s2 points to 173 # a string of zero length, strstr echoes s1. 174 #:end docstring: 175 176 ###;;;autoload 177 function strstr () 178 { 179 # if s2 points to a string of zero length, strstr echoes s1 180 [ ${#2} -eq 0 ] && { echo "$1" ; return 0; } 181 182 # strstr echoes nothing if s2 does not occur in s1 183 case "$1" in 184 *$2*) ;; 185 *) return 1;; 186 esac 187 188 # use the pattern matching code to strip off the match and everything 189 # following it 190 first=${1/$2*/} 191 192 # then strip off the first unmatched portion of the string 193 echo "${1##$first}" 194 } 195 196 #:docstring strtok: 197 # Usage: strtok s1 s2 198 # 199 # Strtok considers the string s1 to consist of a sequence of zero or more 200 # text tokens separated by spans of one or more characters from the 201 # separator string s2. The first call (with a non-empty string s1 202 # specified) echoes a string consisting of the first token on stdout. The 203 # function keeps track of its position in the string s1 between separate 204 # calls, so that subsequent calls made with the first argument an empty 205 # string will work through the string immediately following that token. In 206 # this way subsequent calls will work through the string s1 until no tokens 207 # remain. The separator string s2 may be different from call to call. 208 # When no token remains in s1, an empty value is echoed on stdout. 209 #:end docstring: 210 211 ###;;;autoload 212 function strtok () 213 { 214 : 215 } 216 217 #:docstring strtrunc: 218 # Usage: strtrunc $n $s1 {$s2} {$...} 219 # 220 # Used by many functions like strncmp to truncate arguments for comparison. 221 # Echoes the first n characters of each string s1 s2 ... on stdout. 222 #:end docstring: 223 224 ###;;;autoload 225 function strtrunc () 226 { 227 n=$1 ; shift 228 for z; do 229 echo "${z:0:$n}" 230 done 231 } 232 233 # provide string 234 235 # string.bash ends here 236 237 238 # ========================================================================== # 239 # ==> Everything below here added by the document author. 240 241 # ==> Suggested use of this script is to delete everything below here, 242 # ==> and "source" this file into your own scripts. 243 244 # strcat 245 string0=one 246 string1=two 247 echo 248 echo "Testing \"strcat\" function:" 249 echo "Original \"string0\" = $string0" 250 echo "\"string1\" = $string1" 251 strcat string0 string1 252 echo "New \"string0\" = $string0" 253 echo 254 255 # strlen 256 echo 257 echo "Testing \"strlen\" function:" 258 str=123456789 259 echo "\"str\" = $str" 260 echo -n "Length of \"str\" = " 261 strlen str 262 echo 263 264 265 266 # Exercise: 267 # -------- 268 # Add code to test all the other string functions above. 269 270 271 exit 0 |
Michael Zick's complex array example uses the md5sum check sum command to encode directory information.
Example A-20. Directory information
1 #! /bin/bash 2 # directory-info.sh 3 # Parses and lists directory information. 4 5 # NOTE: Change lines 273 and 353 per "README" file. 6 7 # Michael Zick is the author of this script. 8 # Used here with his permission. 9 10 # Controls 11 # If overridden by command arguments, they must be in the order: 12 # Arg1: "Descriptor Directory" 13 # Arg2: "Exclude Paths" 14 # Arg3: "Exclude Directories" 15 # 16 # Environment Settings override Defaults. 17 # Command arguments override Environment Settings. 18 19 # Default location for content addressed file descriptors. 20 MD5UCFS=${1:-${MD5UCFS:-'/tmpfs/ucfs'}} 21 22 # Directory paths never to list or enter 23 declare -a \ 24 EXCLUDE_PATHS=${2:-${EXCLUDE_PATHS:-'(/proc /dev /devfs /tmpfs)'}} 25 26 # Directories never to list or enter 27 declare -a \ 28 EXCLUDE_DIRS=${3:-${EXCLUDE_DIRS:-'(ucfs lost+found tmp wtmp)'}} 29 30 # Files never to list or enter 31 declare -a \ 32 EXCLUDE_FILES=${3:-${EXCLUDE_FILES:-'(core "Name with Spaces")'}} 33 34 35 # Here document used as a comment block. 36 : << LSfieldsDoc 37 # # # # # List Filesystem Directory Information # # # # # 38 # 39 # ListDirectory "FileGlob" "Field-Array-Name" 40 # or 41 # ListDirectory -of "FileGlob" "Field-Array-Filename" 42 # '-of' meaning 'output to filename' 43 # # # # # 44 45 String format description based on: ls (GNU fileutils) version 4.0.36 46 47 Produces a line (or more) formatted: 48 inode permissions hard-links owner group ... 49 32736 -rw------- 1 mszick mszick 50 51 size day month date hh:mm:ss year path 52 2756608 Sun Apr 20 08:53:06 2003 /home/mszick/core 53 54 Unless it is formatted: 55 inode permissions hard-links owner group ... 56 266705 crw-rw---- 1 root uucp 57 58 major minor day month date hh:mm:ss year path 59 4, 68 Sun Apr 20 09:27:33 2003 /dev/ttyS4 60 NOTE: that pesky comma after the major number 61 62 NOTE: the 'path' may be multiple fields: 63 /home/mszick/core 64 /proc/982/fd/0 -> /dev/null 65 /proc/982/fd/1 -> /home/mszick/.xsession-errors 66 /proc/982/fd/13 -> /tmp/tmpfZVVOCs (deleted) 67 /proc/982/fd/7 -> /tmp/kde-mszick/ksycoca 68 /proc/982/fd/8 -> socket:[11586] 69 /proc/982/fd/9 -> pipe:[11588] 70 71 If that isn't enough to keep your parser guessing, 72 either or both of the path components may be relative: 73 ../Built-Shared -> Built-Static 74 ../linux-2.4.20.tar.bz2 -> ../../../SRCS/linux-2.4.20.tar.bz2 75 76 The first character of the 11 (10?) character permissions field: 77 's' Socket 78 'd' Directory 79 'b' Block device 80 'c' Character device 81 'l' Symbolic link 82 NOTE: Hard links not marked - test for identical inode numbers 83 on identical filesystems. 84 All information about hard linked files are shared, except 85 for the names and the name's location in the directory system. 86 NOTE: A "Hard link" is known as a "File Alias" on some systems. 87 '-' An undistingushed file 88 89 Followed by three groups of letters for: User, Group, Others 90 Character 1: '-' Not readable; 'r' Readable 91 Character 2: '-' Not writable; 'w' Writable 92 Character 3, User and Group: Combined execute and special 93 '-' Not Executable, Not Special 94 'x' Executable, Not Special 95 's' Executable, Special 96 'S' Not Executable, Special 97 Character 3, Others: Combined execute and sticky (tacky?) 98 '-' Not Executable, Not Tacky 99 'x' Executable, Not Tacky 100 't' Executable, Tacky 101 'T' Not Executable, Tacky 102 103 Followed by an access indicator 104 Haven't tested this one, it may be the eleventh character 105 or it may generate another field 106 ' ' No alternate access 107 '+' Alternate access 108 LSfieldsDoc 109 110 111 ListDirectory() 112 { 113 local -a T 114 local -i of=0 # Default return in variable 115 # OLD_IFS=$IFS # Using BASH default ' \t\n' 116 117 case "$#" in 118 3) case "$1" in 119 -of) of=1 ; shift ;; 120 * ) return 1 ;; 121 esac ;; 122 2) : ;; # Poor man's "continue" 123 *) return 1 ;; 124 esac 125 126 # NOTE: the (ls) command is NOT quoted (") 127 T=( $(ls --inode --ignore-backups --almost-all --directory \ 128 --full-time --color=none --time=status --sort=none \ 129 --format=long $1) ) 130 131 case $of in 132 # Assign T back to the array whose name was passed as $2 133 0) eval $2=\( \"\$\{T\[@\]\}\" \) ;; 134 # Write T into filename passed as $2 135 1) echo "${T[@]}" > "$2" ;; 136 esac 137 return 0 138 } 139 140 # # # # # Is that string a legal number? # # # # # 141 # 142 # IsNumber "Var" 143 # # # # # There has to be a better way, sigh... 144 145 IsNumber() 146 { 147 local -i int 148 if [ $# -eq 0 ] 149 then 150 return 1 151 else 152 (let int=$1) 2>/dev/null 153 return $? # Exit status of the let thread 154 fi 155 } 156 157 # # # # # Index Filesystem Directory Information # # # # # 158 # 159 # IndexList "Field-Array-Name" "Index-Array-Name" 160 # or 161 # IndexList -if Field-Array-Filename Index-Array-Name 162 # IndexList -of Field-Array-Name Index-Array-Filename 163 # IndexList -if -of Field-Array-Filename Index-Array-Filename 164 # # # # # 165 166 : << IndexListDoc 167 Walk an array of directory fields produced by ListDirectory 168 169 Having suppressed the line breaks in an otherwise line oriented 170 report, build an index to the array element which starts each line. 171 172 Each line gets two index entries, the first element of each line 173 (inode) and the element that holds the pathname of the file. 174 175 The first index entry pair (Line-Number==0) are informational: 176 Index-Array-Name[0] : Number of "Lines" indexed 177 Index-Array-Name[1] : "Current Line" pointer into Index-Array-Name 178 179 The following index pairs (if any) hold element indexes into 180 the Field-Array-Name per: 181 Index-Array-Name[Line-Number * 2] : The "inode" field element. 182 NOTE: This distance may be either +11 or +12 elements. 183 Index-Array-Name[(Line-Number * 2) + 1] : The "pathname" element. 184 NOTE: This distance may be a variable number of elements. 185 Next line index pair for Line-Number+1. 186 IndexListDoc 187 188 189 190 IndexList() 191 { 192 local -a LIST # Local of listname passed 193 local -a -i INDEX=( 0 0 ) # Local of index to return 194 local -i Lidx Lcnt 195 local -i if=0 of=0 # Default to variable names 196 197 case "$#" in # Simplistic option testing 198 0) return 1 ;; 199 1) return 1 ;; 200 2) : ;; # Poor man's continue 201 3) case "$1" in 202 -if) if=1 ;; 203 -of) of=1 ;; 204 * ) return 1 ;; 205 esac ; shift ;; 206 4) if=1 ; of=1 ; shift ; shift ;; 207 *) return 1 208 esac 209 210 # Make local copy of list 211 case "$if" in 212 0) eval LIST=\( \"\$\{$1\[@\]\}\" \) ;; 213 1) LIST=( $(cat $1) ) ;; 214 esac 215 216 # Grok (grope?) the array 217 Lcnt=${#LIST[@]} 218 Lidx=0 219 until (( Lidx >= Lcnt )) 220 do 221 if IsNumber ${LIST[$Lidx]} 222 then 223 local -i inode name 224 local ft 225 inode=Lidx 226 local m=${LIST[$Lidx+2]} # Hard Links field 227 ft=${LIST[$Lidx+1]:0:1} # Fast-Stat 228 case $ft in 229 b) ((Lidx+=12)) ;; # Block device 230 c) ((Lidx+=12)) ;; # Character device 231 *) ((Lidx+=11)) ;; # Anything else 232 esac 233 name=Lidx 234 case $ft in 235 -) ((Lidx+=1)) ;; # The easy one 236 b) ((Lidx+=1)) ;; # Block device 237 c) ((Lidx+=1)) ;; # Character device 238 d) ((Lidx+=1)) ;; # The other easy one 239 l) ((Lidx+=3)) ;; # At LEAST two more fields 240 # A little more elegance here would handle pipes, 241 #+ sockets, deleted files - later. 242 *) until IsNumber ${LIST[$Lidx]} || ((Lidx >= Lcnt)) 243 do 244 ((Lidx+=1)) 245 done 246 ;; # Not required 247 esac 248 INDEX[${#INDEX[*]}]=$inode 249 INDEX[${#INDEX[*]}]=$name 250 INDEX[0]=${INDEX[0]}+1 # One more "line" found 251 # echo "Line: ${INDEX[0]} Type: $ft Links: $m Inode: \ 252 # ${LIST[$inode]} Name: ${LIST[$name]}" 253 254 else 255 ((Lidx+=1)) 256 fi 257 done 258 case "$of" in 259 0) eval $2=\( \"\$\{INDEX\[@\]\}\" \) ;; 260 1) echo "${INDEX[@]}" > "$2" ;; 261 esac 262 return 0 # What could go wrong? 263 } 264 265 # # # # # Content Identify File # # # # # 266 # 267 # DigestFile Input-Array-Name Digest-Array-Name 268 # or 269 # DigestFile -if Input-FileName Digest-Array-Name 270 # # # # # 271 272 # Here document used as a comment block. 273 : <<DigestFilesDoc 274 275 The key (no pun intended) to a Unified Content File System (UCFS) 276 is to distinguish the files in the system based on their content. 277 Distinguishing files by their name is just, so, 20th Century. 278 279 The content is distinguished by computing a checksum of that content. 280 This version uses the md5sum program to generate a 128 bit checksum 281 representative of the file's contents. 282 There is a chance that two files having different content might 283 generate the same checksum using md5sum (or any checksum). Should 284 that become a problem, then the use of md5sum can be replace by a 285 cyrptographic signature. But until then... 286 287 The md5sum program is documented as outputting three fields (and it 288 does), but when read it appears as two fields (array elements). This 289 is caused by the lack of whitespace between the second and third field. 290 So this function gropes the md5sum output and returns: 291 [0] 32 character checksum in hexidecimal (UCFS filename) 292 [1] Single character: ' ' text file, '*' binary file 293 [2] Filesystem (20th Century Style) name 294 Note: That name may be the character '-' indicating STDIN read. 295 296 DigestFilesDoc 297 298 299 300 DigestFile() 301 { 302 local if=0 # Default, variable name 303 local -a T1 T2 304 305 case "$#" in 306 3) case "$1" in 307 -if) if=1 ; shift ;; 308 * ) return 1 ;; 309 esac ;; 310 2) : ;; # Poor man's "continue" 311 *) return 1 ;; 312 esac 313 314 case $if in 315 0) eval T1=\( \"\$\{$1\[@\]\}\" \) 316 T2=( $(echo ${T1[@]} | md5sum -) ) 317 ;; 318 1) T2=( $(md5sum $1) ) 319 ;; 320 esac 321 322 case ${#T2[@]} in 323 0) return 1 ;; 324 1) return 1 ;; 325 2) case ${T2[1]:0:1} in # SanScrit-2.0.5 326 \*) T2[${#T2[@]}]=${T2[1]:1} 327 T2[1]=\* 328 ;; 329 *) T2[${#T2[@]}]=${T2[1]} 330 T2[1]=" " 331 ;; 332 esac 333 ;; 334 3) : ;; # Assume it worked 335 *) return 1 ;; 336 esac 337 338 local -i len=${#T2[0]} 339 if [ $len -ne 32 ] ; then return 1 ; fi 340 eval $2=\( \"\$\{T2\[@\]\}\" \) 341 } 342 343 # # # # # Locate File # # # # # 344 # 345 # LocateFile [-l] FileName Location-Array-Name 346 # or 347 # LocateFile [-l] -of FileName Location-Array-FileName 348 # # # # # 349 350 # A file location is Filesystem-id and inode-number 351 352 # Here document used as a comment block. 353 : <<StatFieldsDoc 354 Based on stat, version 2.2 355 stat -t and stat -lt fields 356 [0] name 357 [1] Total size 358 File - number of bytes 359 Symbolic link - string length of pathname 360 [2] Number of (512 byte) blocks allocated 361 [3] File type and Access rights (hex) 362 [4] User ID of owner 363 [5] Group ID of owner 364 [6] Device number 365 [7] Inode number 366 [8] Number of hard links 367 [9] Device type (if inode device) Major 368 [10] Device type (if inode device) Minor 369 [11] Time of last access 370 May be disabled in 'mount' with noatime 371 atime of files changed by exec, read, pipe, utime, mknod (mmap?) 372 atime of directories changed by addition/deletion of files 373 [12] Time of last modification 374 mtime of files changed by write, truncate, utime, mknod 375 mtime of directories changed by addtition/deletion of files 376 [13] Time of last change 377 ctime reflects time of changed inode information (owner, group 378 permissions, link count 379 -*-*- Per: 380 Return code: 0 381 Size of array: 14 382 Contents of array 383 Element 0: /home/mszick 384 Element 1: 4096 385 Element 2: 8 386 Element 3: 41e8 387 Element 4: 500 388 Element 5: 500 389 Element 6: 303 390 Element 7: 32385 391 Element 8: 22 392 Element 9: 0 393 Element 10: 0 394 Element 11: 1051221030 395 Element 12: 1051214068 396 Element 13: 1051214068 397 398 For a link in the form of linkname -> realname 399 stat -t linkname returns the linkname (link) information 400 stat -lt linkname returns the realname information 401 402 stat -tf and stat -ltf fields 403 [0] name 404 [1] ID-0? # Maybe someday, but Linux stat structure 405 [2] ID-0? # does not have either LABEL nor UUID 406 # fields, currently information must come 407 # from file-system specific utilities 408 These will be munged into: 409 [1] UUID if possible 410 [2] Volume Label if possible 411 Note: 'mount -l' does return the label and could return the UUID 412 413 [3] Maximum length of filenames 414 [4] Filesystem type 415 [5] Total blocks in the filesystem 416 [6] Free blocks 417 [7] Free blocks for non-root user(s) 418 [8] Block size of the filesystem 419 [9] Total inodes 420 [10] Free inodes 421 422 -*-*- Per: 423 Return code: 0 424 Size of array: 11 425 Contents of array 426 Element 0: /home/mszick 427 Element 1: 0 428 Element 2: 0 429 Element 3: 255 430 Element 4: ef53 431 Element 5: 2581445 432 Element 6: 2277180 433 Element 7: 2146050 434 Element 8: 4096 435 Element 9: 1311552 436 Element 10: 1276425 437 438 StatFieldsDoc 439 440 441 # LocateFile [-l] FileName Location-Array-Name 442 # LocateFile [-l] -of FileName Location-Array-FileName 443 444 LocateFile() 445 { 446 local -a LOC LOC1 LOC2 447 local lk="" of=0 448 449 case "$#" in 450 0) return 1 ;; 451 1) return 1 ;; 452 2) : ;; 453 *) while (( "$#" > 2 )) 454 do 455 case "$1" in 456 -l) lk=-1 ;; 457 -of) of=1 ;; 458 *) return 1 ;; 459 esac 460 shift 461 done ;; 462 esac 463 464 # More Sanscrit-2.0.5 465 # LOC1=( $(stat -t $lk $1) ) 466 # LOC2=( $(stat -tf $lk $1) ) 467 # Uncomment above two lines if system has "stat" command installed. 468 LOC=( ${LOC1[@]:0:1} ${LOC1[@]:3:11} 469 ${LOC2[@]:1:2} ${LOC2[@]:4:1} ) 470 471 case "$of" in 472 0) eval $2=\( \"\$\{LOC\[@\]\}\" \) ;; 473 1) echo "${LOC[@]}" > "$2" ;; 474 esac 475 return 0 476 # Which yields (if you are lucky, and have "stat" installed) 477 # -*-*- Location Discriptor -*-*- 478 # Return code: 0 479 # Size of array: 15 480 # Contents of array 481 # Element 0: /home/mszick 20th Century name 482 # Element 1: 41e8 Type and Permissions 483 # Element 2: 500 User 484 # Element 3: 500 Group 485 # Element 4: 303 Device 486 # Element 5: 32385 inode 487 # Element 6: 22 Link count 488 # Element 7: 0 Device Major 489 # Element 8: 0 Device Minor 490 # Element 9: 1051224608 Last Access 491 # Element 10: 1051214068 Last Modify 492 # Element 11: 1051214068 Last Status 493 # Element 12: 0 UUID (to be) 494 # Element 13: 0 Volume Label (to be) 495 # Element 14: ef53 Filesystem type 496 } 497 498 499 500 # And then there was some test code 501 502 ListArray() # ListArray Name 503 { 504 local -a Ta 505 506 eval Ta=\( \"\$\{$1\[@\]\}\" \) 507 echo 508 echo "-*-*- List of Array -*-*-" 509 echo "Size of array $1: ${#Ta[*]}" 510 echo "Contents of array $1:" 511 for (( i=0 ; i<${#Ta[*]} ; i++ )) 512 do 513 echo -e "\tElement $i: ${Ta[$i]}" 514 done 515 return 0 516 } 517 518 declare -a CUR_DIR 519 # For small arrays 520 ListDirectory "${PWD}" CUR_DIR 521 ListArray CUR_DIR 522 523 declare -a DIR_DIG 524 DigestFile CUR_DIR DIR_DIG 525 echo "The new \"name\" (checksum) for ${CUR_DIR[9]} is ${DIR_DIG[0]}" 526 527 declare -a DIR_ENT 528 # BIG_DIR # For really big arrays - use a temporary file in ramdisk 529 # BIG-DIR # ListDirectory -of "${CUR_DIR[11]}/*" "/tmpfs/junk2" 530 ListDirectory "${CUR_DIR[11]}/*" DIR_ENT 531 532 declare -a DIR_IDX 533 # BIG-DIR # IndexList -if "/tmpfs/junk2" DIR_IDX 534 IndexList DIR_ENT DIR_IDX 535 536 declare -a IDX_DIG 537 # BIG-DIR # DIR_ENT=( $(cat /tmpfs/junk2) ) 538 # BIG-DIR # DigestFile -if /tmpfs/junk2 IDX_DIG 539 DigestFile DIR_ENT IDX_DIG 540 # Small (should) be able to parallize IndexList & DigestFile 541 # Large (should) be able to parallize IndexList & DigestFile & the assignment 542 echo "The \"name\" (checksum) for the contents of ${PWD} is ${IDX_DIG[0]}" 543 544 declare -a FILE_LOC 545 LocateFile ${PWD} FILE_LOC 546 ListArray FILE_LOC 547 548 exit 0 |
Stephane Chazelas demonstrates object-oriented programming in a Bash script.
Example A-21. Object-oriented database
1 #!/bin/bash 2 # obj-oriented.sh: Object-oriented programming in a shell script. 3 # Script by Stephane Chazelas. 4 5 6 person.new() # Looks almost like a class declaration in C++. 7 { 8 local obj_name=$1 name=$2 firstname=$3 birthdate=$4 9 10 eval "$obj_name.set_name() { 11 eval \"$obj_name.get_name() { 12 echo \$1 13 }\" 14 }" 15 16 eval "$obj_name.set_firstname() { 17 eval \"$obj_name.get_firstname() { 18 echo \$1 19 }\" 20 }" 21 22 eval "$obj_name.set_birthdate() { 23 eval \"$obj_name.get_birthdate() { 24 echo \$1 25 }\" 26 eval \"$obj_name.show_birthdate() { 27 echo \$(date -d \"1/1/1970 0:0:\$1 GMT\") 28 }\" 29 eval \"$obj_name.get_age() { 30 echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 )) 31 }\" 32 }" 33 34 $obj_name.set_name $name 35 $obj_name.set_firstname $firstname 36 $obj_name.set_birthdate $birthdate 37 } 38 39 echo 40 41 person.new self Bozeman Bozo 101272413 42 # Create an instance of "person.new" (actually passing args to the function). 43 44 self.get_firstname # Bozo 45 self.get_name # Bozeman 46 self.get_age # 28 47 self.get_birthdate # 101272413 48 self.show_birthdate # Sat Mar 17 20:13:33 MST 1973 49 50 echo 51 52 # typeset -f 53 # to see the created functions (careful, it scrolls off the page). 54 55 exit 0 |
Now for a script that does something useful: installing and mounting those cute USB keychain solid-state "hard drives."
Example A-22. Mounting USB keychain storage devices
1 #!/bin/bash 2 # ==> usb.sh 3 # ==> Script for mounting and installing pen/keychain USB storage devices. 4 # ==> Runs as root at system startup (see below). 5 6 # This code is free software covered by GNU GPL license version 2 or above. 7 # Please refer to http://www.gnu.org/ for the full license text. 8 # 9 # Some code lifted from usb-mount by Michael Hamilton's usb-mount (LGPL) 10 #+ see http://users.actrix.co.nz/michael/usbmount.html 11 # 12 # INSTALL 13 # ------- 14 # Put this in /etc/hotplug/usb/diskonkey. 15 # Then look in /etc/hotplug/usb.distmap, and copy all usb-storage entries 16 #+ into /etc/hotplug/usb.usermap, substituting "usb-storage" for "diskonkey". 17 # Otherwise this code is only run during the kernel module invocation/removal 18 #+ (at least in my tests), which defeats the purpose. 19 # 20 # TODO 21 # ---- 22 # Handle more than one diskonkey device at one time (e.g. /dev/diskonkey1 23 #+ and /mnt/diskonkey1), etc. The biggest problem here is the handling in 24 #+ devlabel, which I haven't yet tried. 25 # 26 # AUTHOR and SUPPORT 27 # ------------------ 28 # Konstantin Riabitsev, <icon linux duke edu>. 29 # Send any problem reports to my email address at the moment. 30 # 31 # ==> Comments added by ABS Guide author. 32 33 34 35 SYMLINKDEV=/dev/diskonkey 36 MOUNTPOINT=/mnt/diskonkey 37 DEVLABEL=/sbin/devlabel 38 DEVLABELCONFIG=/etc/sysconfig/devlabel 39 IAM=$0 40 41 ## 42 # Functions lifted near-verbatim from usb-mount code. 43 # 44 function allAttachedScsiUsb { 45 find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f | xargs grep -l 'Attached: Yes' 46 } 47 function scsiDevFromScsiUsb { 48 echo $1 | awk -F"[-/]" '{ n=$(NF-1); print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 49 1) }' 50 } 51 52 if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then 53 ## 54 # lifted from usbcam code. 55 # 56 if [ -f /var/run/console.lock ]; then 57 CONSOLEOWNER=`cat /var/run/console.lock` 58 elif [ -f /var/lock/console.lock ]; then 59 CONSOLEOWNER=`cat /var/lock/console.lock` 60 else 61 CONSOLEOWNER= 62 fi 63 for procEntry in $(allAttachedScsiUsb); do 64 scsiDev=$(scsiDevFromScsiUsb $procEntry) 65 # Some bug with usb-storage? 66 # Partitions are not in /proc/partitions until they are accessed 67 #+ somehow. 68 /sbin/fdisk -l $scsiDev >/dev/null 69 ## 70 # Most devices have partitioning info, so the data would be on 71 #+ /dev/sd?1. However, some stupider ones don't have any partitioning 72 #+ and use the entire device for data storage. This tries to 73 #+ guess semi-intelligently if we have a /dev/sd?1 and if not, then 74 #+ it uses the entire device and hopes for the better. 75 # 76 if grep -q `basename $scsiDev`1 /proc/partitions; then 77 part="$scsiDev""1" 78 else 79 part=$scsiDev 80 fi 81 ## 82 # Change ownership of the partition to the console user so they can 83 #+ mount it. 84 # 85 if [ ! -z "$CONSOLEOWNER" ]; then 86 chown $CONSOLEOWNER:disk $part 87 fi 88 ## 89 # This checks if we already have this UUID defined with devlabel. 90 # If not, it then adds the device to the list. 91 # 92 prodid=`$DEVLABEL printid -d $part` 93 if ! grep -q $prodid $DEVLABELCONFIG; then 94 # cross our fingers and hope it works 95 $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null 96 fi 97 ## 98 # Check if the mount point exists and create if it doesn't. 99 # 100 if [ ! -e $MOUNTPOINT ]; then 101 mkdir -p $MOUNTPOINT 102 fi 103 ## 104 # Take care of /etc/fstab so mounting is easy. 105 # 106 if ! grep -q "^$SYMLINKDEV" /etc/fstab; then 107 # Add an fstab entry 108 echo -e \ 109 "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \ 110 >> /etc/fstab 111 fi 112 done 113 if [ ! -z "$REMOVER" ]; then 114 ## 115 # Make sure this script is triggered on device removal. 116 # 117 mkdir -p `dirname $REMOVER` 118 ln -s $IAM $REMOVER 119 fi 120 elif [ "${ACTION}" = "remove" ]; then 121 ## 122 # If the device is mounted, unmount it cleanly. 123 # 124 if grep -q "$MOUNTPOINT" /etc/mtab; then 125 # unmount cleanly 126 umount -l $MOUNTPOINT 127 fi 128 ## 129 # Remove it from /etc/fstab if it's there. 130 # 131 if grep -q "^$SYMLINKDEV" /etc/fstab; then 132 grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new 133 mv -f /etc/.fstab.new /etc/fstab 134 fi 135 fi 136 137 exit 0 |
Here is something to warm the hearts of webmasters and mistresses everywhere: a script that saves weblogs.
Example A-23. Preserving weblogs
1 #!/bin/bash 2 # archiveweblogs.sh v1.0 3 4 # Troy Engel <tengel@fluid.com> 5 # Slightly modified by document author. 6 # Used with permission. 7 # 8 # This script will preserve the normally rotated and 9 #+ thrown away weblogs from a default RedHat/Apache installation. 10 # It will save the files with a date/time stamp in the filename, 11 #+ bzipped, to a given directory. 12 # 13 # Run this from crontab nightly at an off hour, 14 #+ as bzip2 can suck up some serious CPU on huge logs: 15 # 0 2 * * * /opt/sbin/archiveweblogs.sh 16 17 18 PROBLEM=66 19 20 # Set this to your backup dir. 21 BKP_DIR=/opt/backups/weblogs 22 23 # Default Apache/RedHat stuff 24 LOG_DAYS="4 3 2 1" 25 LOG_DIR=/var/log/httpd 26 LOG_FILES="access_log error_log" 27 28 # Default RedHat program locations 29 LS=/bin/ls 30 MV=/bin/mv 31 ID=/usr/bin/id 32 CUT=/bin/cut 33 COL=/usr/bin/column 34 BZ2=/usr/bin/bzip2 35 36 # Are we root? 37 USER=`$ID -u` 38 if [ "X$USER" != "X0" ]; then 39 echo "PANIC: Only root can run this script!" 40 exit $PROBLEM 41 fi 42 43 # Backup dir exists/writable? 44 if [ ! -x $BKP_DIR ]; then 45 echo "PANIC: $BKP_DIR doesn't exist or isn't writable!" 46 exit $PROBLEM 47 fi 48 49 # Move, rename and bzip2 the logs 50 for logday in $LOG_DAYS; do 51 for logfile in $LOG_FILES; do 52 MYFILE="$LOG_DIR/$logfile.$logday" 53 if [ -w $MYFILE ]; then 54 DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7` 55 $MV $MYFILE $BKP_DIR/$logfile.$DTS 56 $BZ2 $BKP_DIR/$logfile.$DTS 57 else 58 # Only spew an error if the file exits (ergo non-writable). 59 if [ -f $MYFILE ]; then 60 echo "ERROR: $MYFILE not writable. Skipping." 61 fi 62 fi 63 done 64 done 65 66 exit 0 |
How do you keep the shell from expanding and reinterpreting strings?
Example A-24. Protecting literal strings
1 #! /bin/bash 2 # protect_literal.sh 3 4 # set -vx 5 6 :<<-'_Protect_Literal_String_Doc' 7 8 Copyright (c) Michael S. Zick, 2003; All Rights Reserved 9 License: Unrestricted reuse in any form, for any purpose. 10 Warranty: None 11 Revision: $ID$ 12 13 Documentation redirected to the Bash no-operation. 14 Bash will '/dev/null' this block when the script is first read. 15 (Uncomment the above set command to see this action.) 16 17 Remove the first (Sha-Bang) line when sourcing this as a library 18 procedure. Also comment out the example use code in the two 19 places where shown. 20 21 22 Usage: 23 _protect_literal_str 'Whatever string meets your ${fancy}' 24 Just echos the argument to standard out, hard quotes 25 restored. 26 27 $(_protect_literal_str 'Whatever string meets your ${fancy}') 28 as the right-hand-side of an assignment statement. 29 30 Does: 31 As the right-hand-side of an assignment, preserves the 32 hard quotes protecting the contents of the literal during 33 assignment. 34 35 Notes: 36 The strange names (_*) are used to avoid trampling on 37 the user's chosen names when this is sourced as a 38 library. 39 40 _Protect_Literal_String_Doc 41 42 # The 'for illustration' function form 43 44 _protect_literal_str() { 45 46 # Pick an un-used, non-printing character as local IFS. 47 # Not required, but shows that we are ignoring it. 48 local IFS=$'\x1B' # \ESC character 49 50 # Enclose the All-Elements-Of in hard quotes during assignment. 51 local tmp=$'\x27'$@$'\x27' 52 # local tmp=$'\''$@$'\'' # Even uglier. 53 54 local len=${#tmp} # Info only. 55 echo $tmp is $len long. # Output AND information. 56 } 57 58 # This is the short-named version. 59 _pls() { 60 local IFS=$'x1B' # \ESC character (not required) 61 echo $'\x27'$@$'\x27' # Hard quoted parameter glob 62 } 63 64 # :<<-'_Protect_Literal_String_Test' 65 # # # Remove the above "# " to disable this code. # # # 66 67 # See how that looks when printed. 68 echo 69 echo "- - Test One - -" 70 _protect_literal_str 'Hello $user' 71 _protect_literal_str 'Hello "${username}"' 72 echo 73 74 # Which yields: 75 # - - Test One - - 76 # 'Hello $user' is 13 long. 77 # 'Hello "${username}"' is 21 long. 78 79 # Looks as expected, but why all of the trouble? 80 # The difference is hidden inside the Bash internal order 81 #+ of operations. 82 # Which shows when you use it on the RHS of an assignment. 83 84 # Declare an array for test values. 85 declare -a arrayZ 86 87 # Assign elements with various types of quotes and escapes. 88 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" ) 89 90 # Now list that array and see what is there. 91 echo "- - Test Two - -" 92 for (( i=0 ; i<${#arrayZ[*]} ; i++ )) 93 do 94 echo Element $i: ${arrayZ[$i]} is: ${#arrayZ[$i]} long. 95 done 96 echo 97 98 # Which yields: 99 # - - Test Two - - 100 # Element 0: zero is: 4 long. # Our marker element 101 # Element 1: 'Hello ${Me}' is: 13 long. # Our "$(_pls '...' )" 102 # Element 2: Hello ${You} is: 12 long. # Quotes are missing 103 # Element 3: \'Pass: \' is: 10 long. # ${pw} expanded to nothing 104 105 # Now make an assignment with that result. 106 declare -a array2=( ${arrayZ[@]} ) 107 108 # And print what happened. 109 echo "- - Test Three - -" 110 for (( i=0 ; i<${#array2[*]} ; i++ )) 111 do 112 echo Element $i: ${array2[$i]} is: ${#array2[$i]} long. 113 done 114 echo 115 116 # Which yields: 117 # - - Test Three - - 118 # Element 0: zero is: 4 long. # Our marker element. 119 # Element 1: Hello ${Me} is: 11 long. # Intended result. 120 # Element 2: Hello is: 5 long. # ${You} expanded to nothing. 121 # Element 3: 'Pass: is: 6 long. # Split on the whitespace. 122 # Element 4: ' is: 1 long. # The end quote is here now. 123 124 # Our Element 1 has had its leading and trailing hard quotes stripped. 125 # Although not shown, leading and trailing whitespace is also stripped. 126 # Now that the string contents are set, Bash will always, internally, 127 #+ hard quote the contents as required during its operations. 128 129 # Why? 130 # Considering our "$(_pls 'Hello ${Me}')" construction: 131 # " ... " -> Expansion required, strip the quotes. 132 # $( ... ) -> Replace with the result of..., strip this. 133 # _pls ' ... ' -> called with literal arguments, strip the quotes. 134 # The result returned includes hard quotes; BUT the above processing 135 #+ has already been done, so they become part of the value assigned. 136 # 137 # Similarly, during further usage of the string variable, the ${Me} 138 #+ is part of the contents (result) and survives any operations 139 # (Until explicitly told to evaluate the string). 140 141 # Hint: See what happens when the hard quotes ($'\x27') are replaced 142 #+ with soft quotes ($'\x22') in the above procedures. 143 # Interesting also is to remove the addition of any quoting. 144 145 # _Protect_Literal_String_Test 146 # # # Remove the above "# " to disable this code. # # # 147 148 exit 0 |
What if you want the shell to expand and reinterpret strings?
Example A-25. Unprotecting literal strings
1 #! /bin/bash 2 # unprotect_literal.sh 3 4 # set -vx 5 6 :<<-'_UnProtect_Literal_String_Doc' 7 8 Copyright (c) Michael S. Zick, 2003; All Rights Reserved 9 License: Unrestricted reuse in any form, for any purpose. 10 Warranty: None 11 Revision: $ID$ 12 13 Documentation redirected to the Bash no-operation. Bash will 14 '/dev/null' this block when the script is first read. 15 (Uncomment the above set command to see this action.) 16 17 Remove the first (Sha-Bang) line when sourcing this as a library 18 procedure. Also comment out the example use code in the two 19 places where shown. 20 21 22 Usage: 23 Complement of the "$(_pls 'Literal String')" function. 24 (See the protect_literal.sh example.) 25 26 StringVar=$(_upls ProtectedSringVariable) 27 28 Does: 29 When used on the right-hand-side of an assignment statement; 30 makes the substitions embedded in the protected string. 31 32 Notes: 33 The strange names (_*) are used to avoid trampling on 34 the user's chosen names when this is sourced as a 35 library. 36 37 38 _UnProtect_Literal_String_Doc 39 40 _upls() { 41 local IFS=$'x1B' # \ESC character (not required) 42 eval echo $@ # Substitution on the glob. 43 } 44 45 # :<<-'_UnProtect_Literal_String_Test' 46 # # # Remove the above "# " to disable this code. # # # 47 48 49 _pls() { 50 local IFS=$'x1B' # \ESC character (not required) 51 echo $'\x27'$@$'\x27' # Hard quoted parameter glob 52 } 53 54 # Declare an array for test values. 55 declare -a arrayZ 56 57 # Assign elements with various types of quotes and escapes. 58 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" ) 59 60 # Now make an assignment with that result. 61 declare -a array2=( ${arrayZ[@]} ) 62 63 # Which yielded: 64 # - - Test Three - - 65 # Element 0: zero is: 4 long # Our marker element. 66 # Element 1: Hello ${Me} is: 11 long # Intended result. 67 # Element 2: Hello is: 5 long # ${You} expanded to nothing. 68 # Element 3: 'Pass: is: 6 long # Split on the whitespace. 69 # Element 4: ' is: 1 long # The end quote is here now. 70 71 # set -vx 72 73 # Initialize 'Me' to something for the embedded ${Me} substitution. 74 # This needs to be done ONLY just prior to evaluating the 75 #+ protected string. 76 # (This is why it was protected to begin with.) 77 78 Me="to the array guy." 79 80 # Set a string variable destination to the result. 81 newVar=$(_upls ${array2[1]}) 82 83 # Show what the contents are. 84 echo $newVar 85 86 # Do we really need a function to do this? 87 newerVar=$(eval echo ${array2[1]}) 88 echo $newerVar 89 90 # I guess not, but the _upls function gives us a place to hang 91 #+ the documentation on. 92 # This helps when we forget what a # construction like: 93 #+ $(eval echo ... ) means. 94 95 # What if Me isn't set when the protected string is evaluated? 96 unset Me 97 newestVar=$(_upls ${array2[1]}) 98 echo $newestVar 99 100 # Just gone, no hints, no runs, no errors. 101 102 # Why in the world? 103 # Setting the contents of a string variable containing character 104 #+ sequences that have a meaning to Bash is a general problem in 105 #+ script programming. 106 # 107 # This problem is now solved in eight lines of code 108 #+ (and four pages of description). 109 110 # Where is all this going? 111 # Dynamic content Web pages as an array of Bash strings. 112 # Content set per request by a Bash 'eval' command 113 #+ on the stored page template. 114 # Not intended to replace PHP, just an interesting thing to do. 115 ### 116 # Don't have a webserver application? 117 # No problem, check the example directory of the Bash source; 118 #+ there is a Bash script for that also. 119 120 # _UnProtect_Literal_String_Test 121 # # # Remove the above "# " to disable this code. # # # 122 123 exit 0 |
This powerful script helps hunt down spammers .
Example A-26. Spammer Identification
1 #!/bin/bash 2 # $Id: is_spammer.bash,v 1.12.2.11 2004/10/01 21:42:33 mszick Exp $ 3 # The latest version of this script is available from ftp://ftp.morethan.org. 4 # 5 # Spammer-identification 6 # by Michael S. Zick 7 # Used in the ABS Guide with permission. 8 9 10 11 ####################################################### 12 # Documentation 13 # See also "Quickstart" at end of script. 14 ####################################################### 15 16 :<<-'__is_spammer_Doc_' 17 18 Copyright (c) Michael S. Zick, 2004 19 License: Unrestricted reuse in any form, for any purpose. 20 Warranty: None -{Its a script; the user is on their own.}- 21 22 Impatient? 23 Application code: goto "# # # Hunt the Spammer' program code # # #" 24 Example output: ":<<-'_is_spammer_outputs_'" 25 How to use: Enter script name without arguments. 26 Or goto "Quickstart" at end of script. 27 28 Provides 29 Given a domain name or IP(v4) address as input: 30 31 Does an exhaustive set of queries to find the associated 32 network resources (short of recursing into TLDs). 33 34 Checks the IP(v4) addresses found against Blacklist 35 nameservers. 36 37 If found to be a blacklisted IP(v4) address, 38 reports the blacklist text records. 39 (Usually hyper-links to the specific report.) 40 41 Requires 42 A working Internet connection. 43 (Exercise: Add check and/or abort if not on-line when running script.) 44 Bash with arrays (2.05b+). 45 46 The external program 'dig' -- 47 a utility program provided with the 'bind' set of programs. 48 Specifically, the version which is part of Bind series 9.x 49 See: http://www.isc.org 50 51 All usages of 'dig' are limited to wrapper functions, 52 which may be rewritten as required. 53 See: dig_wrappers.bash for details. 54 ("Additional documentation" -- below) 55 56 Usage 57 Script requires a single argument, which may be: 58 1) A domain name; 59 2) An IP(v4) address; 60 3) A filename, with one name or address per line. 61 62 Script accepts an optional second argument, which may be: 63 1) A Blacklist server name; 64 2) A filename, with one Blacklist server name per line. 65 66 If the second argument is not provided, the script uses 67 a built-in set of (free) Blacklist servers. 68 69 See also, the Quickstart at the end of this script (after 'exit'). 70 71 Return Codes 72 0 - All OK 73 1 - Script failure 74 2 - Something is Blacklisted 75 76 Optional environment variables 77 SPAMMER_TRACE 78 If set to a writable file, 79 script will log an execution flow trace. 80 81 SPAMMER_DATA 82 If set to a writable file, script will dump its 83 discovered data in the form of GraphViz file. 84 See: http://www.research.att.com/sw/tools/graphviz 85 86 SPAMMER_LIMIT 87 Limits the depth of resource tracing. 88 89 Default is 2 levels. 90 91 A setting of 0 (zero) means 'unlimited' . . . 92 Caution: script might recurse the whole Internet! 93 94 A limit of 1 or 2 is most useful when processing 95 a file of domain names and addresses. 96 A higher limit can be useful when hunting spam gangs. 97 98 99 Additional documentation 100 Download the archived set of scripts 101 explaining and illustrating the function contained within this script. 102 http://personal.riverusers.com/mszick_clf.tar.bz2 103 104 105 Study notes 106 This script uses a large number of functions. 107 Nearly all general functions have their own example script. 108 Each of the example scripts have tutorial level comments. 109 110 Scripting project 111 Add support for IP(v6) addresses. 112 IP(v6) addresses are recognized but not processed. 113 114 Advanced project 115 Add the reverse lookup detail to the discovered information. 116 117 Report the delegation chain and abuse contacts. 118 119 Modify the GraphViz file output to include the 120 newly discovered information. 121 122 __is_spammer_Doc_ 123 124 ####################################################### 125 126 127 128 129 #### Special IFS settings used for string parsing. #### 130 131 # Whitespace == :Space:Tab:Line Feed:Carriage Return: 132 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D' 133 134 # No Whitespace == Line Feed:Carriage Return 135 NO_WSP=$'\x0A'$'\x0D' 136 137 # Field separator for dotted decimal IP addresses 138 ADR_IFS=${NO_WSP}'.' 139 140 # Array to dotted string conversions 141 DOT_IFS='.'${WSP_IFS} 142 143 # # # Pending operations stack machine # # # 144 # This set of functions described in func_stack.bash. 145 # (See "Additional documentation" above.) 146 # # # 147 148 # Global stack of pending operations. 149 declare -f -a _pending_ 150 # Global sentinel for stack runners 151 declare -i _p_ctrl_ 152 # Global holder for currently executing function 153 declare -f _pend_current_ 154 155 # # # Debug version only - remove for regular use # # # 156 # 157 # The function stored in _pend_hook_ is called 158 # immediately before each pending function is 159 # evaluated. Stack clean, _pend_current_ set. 160 # 161 # This thingy demonstrated in pend_hook.bash. 162 declare -f _pend_hook_ 163 # # # 164 165 # The do nothing function 166 pend_dummy() { : ; } 167 168 # Clear and initialize the function stack. 169 pend_init() { 170 unset _pending_[@] 171 pend_func pend_stop_mark 172 _pend_hook_='pend_dummy' # Debug only 173 } 174 175 # Discard the top function on the stack. 176 pend_pop() { 177 if [ ${#_pending_[@]} -gt 0 ] 178 then 179 local -i _top_ 180 _top_=${#_pending_[@]}-1 181 unset _pending_[$_top_] 182 fi 183 } 184 185 # pend_func function_name [$(printf '%q\n' arguments)] 186 pend_func() { 187 local IFS=${NO_WSP} 188 set -f 189 _pending_[${#_pending_[@]}]=$@ 190 set +f 191 } 192 193 # The function which stops the release: 194 pend_stop_mark() { 195 _p_ctrl_=0 196 } 197 198 pend_mark() { 199 pend_func pend_stop_mark 200 } 201 202 # Execute functions until 'pend_stop_mark' . . . 203 pend_release() { 204 local -i _top_ # Declare _top_ as integer. 205 _p_ctrl_=${#_pending_[@]} 206 while [ ${_p_ctrl_} -gt 0 ] 207 do 208 _top_=${#_pending_[@]}-1 209 _pend_current_=${_pending_[$_top_]} 210 unset _pending_[$_top_] 211 $_pend_hook_ # Debug only 212 eval $_pend_current_ 213 done 214 } 215 216 # Drop functions until 'pend_stop_mark' . . . 217 pend_drop() { 218 local -i _top_ 219 local _pd_ctrl_=${#_pending_[@]} 220 while [ ${_pd_ctrl_} -gt 0 ] 221 do 222 _top_=$_pd_ctrl_-1 223 if [ "${_pending_[$_top_]}" == 'pend_stop_mark' ] 224 then 225 unset _pending_[$_top_] 226 break 227 else 228 unset _pending_[$_top_] 229 _pd_ctrl_=$_top_ 230 fi 231 done 232 if [ ${#_pending_[@]} -eq 0 ] 233 then 234 pend_func pend_stop_mark 235 fi 236 } 237 238 #### Array editors #### 239 240 # This function described in edit_exact.bash. 241 # (See "Additional documentation," above.) 242 # edit_exact <excludes_array_name> <target_array_name> 243 edit_exact() { 244 [ $# -eq 2 ] || 245 [ $# -eq 3 ] || return 1 246 local -a _ee_Excludes 247 local -a _ee_Target 248 local _ee_x 249 local _ee_t 250 local IFS=${NO_WSP} 251 set -f 252 eval _ee_Excludes=\( \$\{$1\[@\]\} \) 253 eval _ee_Target=\( \$\{$2\[@\]\} \) 254 local _ee_len=${#_ee_Target[@]} # Original length. 255 local _ee_cnt=${#_ee_Excludes[@]} # Exclude list length. 256 [ ${_ee_len} -ne 0 ] || return 0 # Can't edit zero length. 257 [ ${_ee_cnt} -ne 0 ] || return 0 # Can't edit zero length. 258 for (( x = 0; x < ${_ee_cnt} ; x++ )) 259 do 260 _ee_x=${_ee_Excludes[$x]} 261 for (( n = 0 ; n < ${_ee_len} ; n++ )) 262 do 263 _ee_t=${_ee_Target[$n]} 264 if [ x"${_ee_t}" == x"${_ee_x}" ] 265 then 266 unset _ee_Target[$n] # Discard match. 267 [ $# -eq 2 ] && break # If 2 arguments, then done. 268 fi 269 done 270 done 271 eval $2=\( \$\{_ee_Target\[@\]\} \) 272 set +f 273 return 0 274 } 275 276 # This function described in edit_by_glob.bash. 277 # edit_by_glob <excludes_array_name> <target_array_name> 278 edit_by_glob() { 279 [ $# -eq 2 ] || 280 [ $# -eq 3 ] || return 1 281 local -a _ebg_Excludes 282 local -a _ebg_Target 283 local _ebg_x 284 local _ebg_t 285 local IFS=${NO_WSP} 286 set -f 287 eval _ebg_Excludes=\( \$\{$1\[@\]\} \) 288 eval _ebg_Target=\( \$\{$2\[@\]\} \) 289 local _ebg_len=${#_ebg_Target[@]} 290 local _ebg_cnt=${#_ebg_Excludes[@]} 291 [ ${_ebg_len} -ne 0 ] || return 0 292 [ ${_ebg_cnt} -ne 0 ] || return 0 293 for (( x = 0; x < ${_ebg_cnt} ; x++ )) 294 do 295 _ebg_x=${_ebg_Excludes[$x]} 296 for (( n = 0 ; n < ${_ebg_len} ; n++ )) 297 do 298 [ $# -eq 3 ] && _ebg_x=${_ebg_x}'*' # Do prefix edit 299 if [ ${_ebg_Target[$n]:=} ] #+ if defined & set. 300 then 301 _ebg_t=${_ebg_Target[$n]/#${_ebg_x}/} 302 [ ${#_ebg_t} -eq 0 ] && unset _ebg_Target[$n] 303 fi 304 done 305 done 306 eval $2=\( \$\{_ebg_Target\[@\]\} \) 307 set +f 308 return 0 309 } 310 311 # This function described in unique_lines.bash. 312 # unique_lines <in_name> <out_name> 313 unique_lines() { 314 [ $# -eq 2 ] || return 1 315 local -a _ul_in 316 local -a _ul_out 317 local -i _ul_cnt 318 local -i _ul_pos 319 local _ul_tmp 320 local IFS=${NO_WSP} 321 set -f 322 eval _ul_in=\( \$\{$1\[@\]\} \) 323 _ul_cnt=${#_ul_in[@]} 324 for (( _ul_pos = 0 ; _ul_pos < ${_ul_cnt} ; _ul_pos++ )) 325 do 326 if [ ${_ul_in[${_ul_pos}]:=} ] # If defined & not empty 327 then 328 _ul_tmp=${_ul_in[${_ul_pos}]} 329 _ul_out[${#_ul_out[@]}]=${_ul_tmp} 330 for (( zap = _ul_pos ; zap < ${_ul_cnt} ; zap++ )) 331 do 332 [ ${_ul_in[${zap}]:=} ] && 333 [ 'x'${_ul_in[${zap}]} == 'x'${_ul_tmp} ] && 334 unset _ul_in[${zap}] 335 done 336 fi 337 done 338 eval $2=\( \$\{_ul_out\[@\]\} \) 339 set +f 340 return 0 341 } 342 343 # This function described in char_convert.bash. 344 # to_lower <string> 345 to_lower() { 346 [ $# -eq 1 ] || return 1 347 local _tl_out 348 _tl_out=${1//A/a} 349 _tl_out=${_tl_out//B/b} 350 _tl_out=${_tl_out//C/c} 351 _tl_out=${_tl_out//D/d} 352 _tl_out=${_tl_out//E/e} 353 _tl_out=${_tl_out//F/f} 354 _tl_out=${_tl_out//G/g} 355 _tl_out=${_tl_out//H/h} 356 _tl_out=${_tl_out//I/i} 357 _tl_out=${_tl_out//J/j} 358 _tl_out=${_tl_out//K/k} 359 _tl_out=${_tl_out//L/l} 360 _tl_out=${_tl_out//M/m} 361 _tl_out=${_tl_out//N/n} 362 _tl_out=${_tl_out//O/o} 363 _tl_out=${_tl_out//P/p} 364 _tl_out=${_tl_out//Q/q} 365 _tl_out=${_tl_out//R/r} 366 _tl_out=${_tl_out//S/s} 367 _tl_out=${_tl_out//T/t} 368 _tl_out=${_tl_out//U/u} 369 _tl_out=${_tl_out//V/v} 370 _tl_out=${_tl_out//W/w} 371 _tl_out=${_tl_out//X/x} 372 _tl_out=${_tl_out//Y/y} 373 _tl_out=${_tl_out//Z/z} 374 echo ${_tl_out} 375 return 0 376 } 377 378 #### Application helper functions #### 379 380 # Not everybody uses dots as separators (APNIC, for example). 381 # This function described in to_dot.bash 382 # to_dot <string> 383 to_dot() { 384 [ $# -eq 1 ] || return 1 385 echo ${1//[#|@|%]/.} 386 return 0 387 } 388 389 # This function described in is_number.bash. 390 # is_number <input> 391 is_number() { 392 [ "$#" -eq 1 ] || return 1 # is blank? 393 [ x"$1" == 'x0' ] && return 0 # is zero? 394 local -i tst 395 let tst=$1 2>/dev/null # else is numeric! 396 return $? 397 } 398 399 # This function described in is_address.bash. 400 # is_address <input> 401 is_address() { 402 [ $# -eq 1 ] || return 1 # Blank ==> false 403 local -a _ia_input 404 local IFS=${ADR_IFS} 405 _ia_input=( $1 ) 406 if [ ${#_ia_input[@]} -eq 4 ] && 407 is_number ${_ia_input[0]} && 408 is_number ${_ia_input[1]} && 409 is_number ${_ia_input[2]} && 410 is_number ${_ia_input[3]} && 411 [ ${_ia_input[0]} -lt 256 ] && 412 [ ${_ia_input[1]} -lt 256 ] && 413 [ ${_ia_input[2]} -lt 256 ] && 414 [ ${_ia_input[3]} -lt 256 ] 415 then 416 return 0 417 else 418 return 1 419 fi 420 } 421 422 # This function described in split_ip.bash. 423 # split_ip <IP_address> <array_name_norm> [<array_name_rev>] 424 split_ip() { 425 [ $# -eq 3 ] || # Either three 426 [ $# -eq 2 ] || return 1 #+ or two arguments 427 local -a _si_input 428 local IFS=${ADR_IFS} 429 _si_input=( $1 ) 430 IFS=${WSP_IFS} 431 eval $2=\(\ \$\{_si_input\[@\]\}\ \) 432 if [ $# -eq 3 ] 433 then 434 # Build query order array. 435 local -a _dns_ip 436 _dns_ip[0]=${_si_input[3]} 437 _dns_ip[1]=${_si_input[2]} 438 _dns_ip[2]=${_si_input[1]} 439 _dns_ip[3]=${_si_input[0]} 440 eval $3=\(\ \$\{_dns_ip\[@\]\}\ \) 441 fi 442 return 0 443 } 444 445 # This function described in dot_array.bash. 446 # dot_array <array_name> 447 dot_array() { 448 [ $# -eq 1 ] || return 1 # Single argument required. 449 local -a _da_input 450 eval _da_input=\(\ \$\{$1\[@\]\}\ \) 451 local IFS=${DOT_IFS} 452 local _da_output=${_da_input[@]} 453 IFS=${WSP_IFS} 454 echo ${_da_output} 455 return 0 456 } 457 458 # This function described in file_to_array.bash 459 # file_to_array <file_name> <line_array_name> 460 file_to_array() { 461 [ $# -eq 2 ] || return 1 # Two arguments required. 462 local IFS=${NO_WSP} 463 local -a _fta_tmp_ 464 _fta_tmp_=( $(cat $1) ) 465 eval $2=\( \$\{_fta_tmp_\[@\]\} \) 466 return 0 467 } 468 469 # Columnized print of an array of multi-field strings. 470 # col_print <array_name> <min_space> <tab_stop [tab_stops]> 471 col_print() { 472 [ $# -gt 2 ] || return 0 473 local -a _cp_inp 474 local -a _cp_spc 475 local -a _cp_line 476 local _cp_min 477 local _cp_mcnt 478 local _cp_pos 479 local _cp_cnt 480 local _cp_tab 481 local -i _cp 482 local -i _cpf 483 local _cp_fld 484 # WARNING: FOLLOWING LINE NOT BLANK -- IT IS QUOTED SPACES. 485 local _cp_max=' ' 486 set -f 487 local IFS=${NO_WSP} 488 eval _cp_inp=\(\ \$\{$1\[@\]\}\ \) 489 [ ${#_cp_inp[@]} -gt 0 ] || return 0 # Empty is easy. 490 _cp_mcnt=$2 491 _cp_min=${_cp_max:1:${_cp_mcnt}} 492 shift 493 shift 494 _cp_cnt=$# 495 for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ )) 496 do 497 _cp_spc[${#_cp_spc[@]}]="${_cp_max:2:$1}" #" 498 shift 499 done 500 _cp_cnt=${#_cp_inp[@]} 501 for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ )) 502 do 503 _cp_pos=1 504 IFS=${NO_WSP}$'\x20' 505 _cp_line=( ${_cp_inp[${_cp}]} ) 506 IFS=${NO_WSP} 507 for (( _cpf = 0 ; _cpf < ${#_cp_line[@]} ; _cpf++ )) 508 do 509 _cp_tab=${_cp_spc[${_cpf}]:${_cp_pos}} 510 if [ ${#_cp_tab} -lt ${_cp_mcnt} ] 511 then 512 _cp_tab="${_cp_min}" 513 fi 514 echo -n "${_cp_tab}" 515 (( _cp_pos = ${_cp_pos} + ${#_cp_tab} )) 516 _cp_fld="${_cp_line[${_cpf}]}" 517 echo -n ${_cp_fld} 518 (( _cp_pos = ${_cp_pos} + ${#_cp_fld} )) 519 done 520 echo 521 done 522 set +f 523 return 0 524 } 525 526 # # # # 'Hunt the Spammer' data flow # # # # 527 528 # Application return code 529 declare -i _hs_RC 530 531 # Original input, from which IP addresses are removed 532 # After which, domain names to check 533 declare -a uc_name 534 535 # Original input IP addresses are moved here 536 # After which, IP addresses to check 537 declare -a uc_address 538 539 # Names against which address expansion run 540 # Ready for name detail lookup 541 declare -a chk_name 542 543 # Addresses against which name expansion run 544 # Ready for address detail lookup 545 declare -a chk_address 546 547 # Recursion is depth-first-by-name. 548 # The expand_input_address maintains this list 549 #+ to prohibit looking up addresses twice during 550 #+ domain name recursion. 551 declare -a been_there_addr 552 been_there_addr=( '127.0.0.1' ) # Whitelist localhost 553 554 # Names which we have checked (or given up on) 555 declare -a known_name 556 557 # Addresses which we have checked (or given up on) 558 declare -a known_address 559 560 # List of zero or more Blacklist servers to check. 561 # Each 'known_address' will be checked against each server, 562 #+ with negative replies and failures suppressed. 563 declare -a list_server 564 565 # Indirection limit - set to zero == no limit 566 indirect=${SPAMMER_LIMIT:=2} 567 568 # # # # 'Hunt the Spammer' information output data # # # # 569 570 # Any domain name may have multiple IP addresses. 571 # Any IP address may have multiple domain names. 572 # Therefore, track unique address-name pairs. 573 declare -a known_pair 574 declare -a reverse_pair 575 576 # In addition to the data flow variables; known_address 577 #+ known_name and list_server, the following are output to the 578 #+ external graphics interface file. 579 580 # Authority chain, parent -> SOA fields. 581 declare -a auth_chain 582 583 # Reference chain, parent name -> child name 584 declare -a ref_chain 585 586 # DNS chain - domain name -> address 587 declare -a name_address 588 589 # Name and service pairs - domain name -> service 590 declare -a name_srvc 591 592 # Name and resource pairs - domain name -> Resource Record 593 declare -a name_resource 594 595 # Parent and Child pairs - parent name -> child name 596 # This MAY NOT be the same as the ref_chain followed! 597 declare -a parent_child 598 599 # Address and Blacklist hit pairs - address->server 600 declare -a address_hits 601 602 # Dump interface file data 603 declare -f _dot_dump 604 _dot_dump=pend_dummy # Initially a no-op 605 606 # Data dump is enabled by setting the environment variable SPAMMER_DATA 607 #+ to the name of a writable file. 608 declare _dot_file 609 610 # Helper function for the dump-to-dot-file function 611 # dump_to_dot <array_name> <prefix> 612 dump_to_dot() { 613 local -a _dda_tmp 614 local -i _dda_cnt 615 local _dda_form=' '${2}'%04u %s\n' 616 local IFS=${NO_WSP} 617 eval _dda_tmp=\(\ \$\{$1\[@\]\}\ \) 618 _dda_cnt=${#_dda_tmp[@]} 619 if [ ${_dda_cnt} -gt 0 ] 620 then 621 for (( _dda = 0 ; _dda < _dda_cnt ; _dda++ )) 622 do 623 printf "${_dda_form}" \ 624 "${_dda}" "${_dda_tmp[${_dda}]}" >>${_dot_file} 625 done 626 fi 627 } 628 629 # Which will also set _dot_dump to this function . . . 630 dump_dot() { 631 local -i _dd_cnt 632 echo '# Data vintage: '$(date -R) >${_dot_file} 633 echo '# ABS Guide: is_spammer.bash; v2, 2004-msz' >>${_dot_file} 634 echo >>${_dot_file} 635 echo 'digraph G {' >>${_dot_file} 636 637 if [ ${#known_name[@]} -gt 0 ] 638 then 639 echo >>${_dot_file} 640 echo '# Known domain name nodes' >>${_dot_file} 641 _dd_cnt=${#known_name[@]} 642 for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ )) 643 do 644 printf ' N%04u [label="%s"] ;\n' \ 645 "${_dd}" "${known_name[${_dd}]}" >>${_dot_file} 646 done 647 fi 648 649 if [ ${#known_address[@]} -gt 0 ] 650 then 651 echo >>${_dot_file} 652 echo '# Known address nodes' >>${_dot_file} 653 _dd_cnt=${#known_address[@]} 654 for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ )) 655 do 656 printf ' A%04u [label="%s"] ;\n' \ 657 "${_dd}" "${known_address[${_dd}]}" >>${_dot_file} 658 done 659 fi 660 661 echo >>${_dot_file} 662 echo '/*' >>${_dot_file} 663 echo ' * Known relationships :: User conversion to' >>${_dot_file} 664 echo ' * graphic form by hand or program required.' >>${_dot_file} 665 echo ' *' >>${_dot_file} 666 667 if [ ${#auth_chain[@]} -gt 0 ] 668 then 669 echo >>${_dot_file} 670 echo '# Authority reference edges followed and field source.' >>${_dot_file} 671 dump_to_dot auth_chain AC 672 fi 673 674 if [ ${#ref_chain[@]} -gt 0 ] 675 then 676 echo >>${_dot_file} 677 echo '# Name reference edges followed and field source.' >>${_dot_file} 678 dump_to_dot ref_chain RC 679 fi 680 681 if [ ${#name_address[@]} -gt 0 ] 682 then 683 echo >>${_dot_file} 684 echo '# Known name->address edges' >>${_dot_file} 685 dump_to_dot name_address NA 686 fi 687 688 if [ ${#name_srvc[@]} -gt 0 ] 689 then 690 echo >>${_dot_file} 691 echo '# Known name->service edges' >>${_dot_file} 692 dump_to_dot name_srvc NS 693 fi 694 695 if [ ${#name_resource[@]} -gt 0 ] 696 then 697 echo >>${_dot_file} 698 echo '# Known name->resource edges' >>${_dot_file} 699 dump_to_dot name_resource NR 700 fi 701 702 if [ ${#parent_child[@]} -gt 0 ] 703 then 704 echo >>${_dot_file} 705 echo '# Known parent->child edges' >>${_dot_file} 706 dump_to_dot parent_child PC 707 fi 708 709 if [ ${#list_server[@]} -gt 0 ] 710 then 711 echo >>${_dot_file} 712 echo '# Known Blacklist nodes' >>${_dot_file} 713 _dd_cnt=${#list_server[@]} 714 for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ )) 715 do 716 printf ' LS%04u [label="%s"] ;\n' \ 717 "${_dd}" "${list_server[${_dd}]}" >>${_dot_file} 718 done 719 fi 720 721 unique_lines address_hits address_hits 722 if [ ${#address_hits[@]} -gt 0 ] 723 then 724 echo >>${_dot_file} 725 echo '# Known address->Blacklist_hit edges' >>${_dot_file} 726 echo '# CAUTION: dig warnings can trigger false hits.' >>${_dot_file} 727 dump_to_dot address_hits AH 728 fi 729 echo >>${_dot_file} 730 echo ' *' >>${_dot_file} 731 echo ' * That is a lot of relationships. Happy graphing.' >>${_dot_file} 732 echo ' */' >>${_dot_file} 733 echo '}' >>${_dot_file} 734 return 0 735 } 736 737 # # # # 'Hunt the Spammer' execution flow # # # # 738 739 # Execution trace is enabled by setting the 740 #+ environment variable SPAMMER_TRACE to the name of a writable file. 741 declare -a _trace_log 742 declare _log_file 743 744 # Function to fill the trace log 745 trace_logger() { 746 _trace_log[${#_trace_log[@]}]=${_pend_current_} 747 } 748 749 # Dump trace log to file function variable. 750 declare -f _log_dump 751 _log_dump=pend_dummy # Initially a no-op. 752 753 # Dump the trace log to a file. 754 dump_log() { 755 local -i _dl_cnt 756 _dl_cnt=${#_trace_log[@]} 757 for (( _dl = 0 ; _dl < _dl_cnt ; _dl++ )) 758 do 759 echo ${_trace_log[${_dl}]} >> ${_log_file} 760 done 761 _dl_cnt=${#_pending_[@]} 762 if [ ${_dl_cnt} -gt 0 ] 763 then 764 _dl_cnt=${_dl_cnt}-1 765 echo '# # # Operations stack not empty # # #' >> ${_log_file} 766 for (( _dl = ${_dl_cnt} ; _dl >= 0 ; _dl-- )) 767 do 768 echo ${_pending_[${_dl}]} >> ${_log_file} 769 done 770 fi 771 } 772 773 # # # Utility program 'dig' wrappers # # # 774 # 775 # These wrappers are derived from the 776 #+ examples shown in dig_wrappers.bash. 777 # 778 # The major difference is these return 779 #+ their results as a list in an array. 780 # 781 # See dig_wrappers.bash for details and 782 #+ use that script to develop any changes. 783 # 784 # # # 785 786 # Short form answer: 'dig' parses answer. 787 788 # Forward lookup :: Name -> Address 789 # short_fwd <domain_name> <array_name> 790 short_fwd() { 791 local -a _sf_reply 792 local -i _sf_rc 793 local -i _sf_cnt 794 IFS=${NO_WSP} 795 echo -n '.' 796 # echo 'sfwd: '${1} 797 _sf_reply=( $(dig +short ${1} -c in -t a 2>/dev/null) ) 798 _sf_rc=$? 799 if [ ${_sf_rc} -ne 0 ] 800 then 801 _trace_log[${#_trace_log[@]}]='# # # Lookup error '${_sf_rc}' on '${1}' # # #' 802 # [ ${_sf_rc} -ne 9 ] && pend_drop 803 return ${_sf_rc} 804 else 805 # Some versions of 'dig' return warnings on stdout. 806 _sf_cnt=${#_sf_reply[@]} 807 for (( _sf = 0 ; _sf < ${_sf_cnt} ; _sf++ )) 808 do 809 [ 'x'${_sf_reply[${_sf}]:0:2} == 'x;;' ] && 810 unset _sf_reply[${_sf}] 811 done 812 eval $2=\( \$\{_sf_reply\[@\]\} \) 813 fi 814 return 0 815 } 816 817 # Reverse lookup :: Address -> Name 818 # short_rev <ip_address> <array_name> 819 short_rev() { 820 local -a _sr_reply 821 local -i _sr_rc 822 local -i _sr_cnt 823 IFS=${NO_WSP} 824 echo -n '.' 825 # echo 'srev: '${1} 826 _sr_reply=( $(dig +short -x ${1} 2>/dev/null) ) 827 _sr_rc=$? 828 if [ ${_sr_rc} -ne 0 ] 829 then 830 _trace_log[${#_trace_log[@]}]='# # # Lookup error '${_sr_rc}' on '${1}' # # #' 831 # [ ${_sr_rc} -ne 9 ] && pend_drop 832 return ${_sr_rc} 833 else 834 # Some versions of 'dig' return warnings on stdout. 835 _sr_cnt=${#_sr_reply[@]} 836 for (( _sr = 0 ; _sr < ${_sr_cnt} ; _sr++ )) 837 do 838 [ 'x'${_sr_reply[${_sr}]:0:2} == 'x;;' ] && 839 unset _sr_reply[${_sr}] 840 done 841 eval $2=\( \$\{_sr_reply\[@\]\} \) 842 fi 843 return 0 844 } 845 846 # Special format lookup used to query blacklist servers. 847 # short_text <ip_address> <array_name> 848 short_text() { 849 local -a _st_reply 850 local -i _st_rc 851 local -i _st_cnt 852 IFS=${NO_WSP} 853 # echo 'stxt: '${1} 854 _st_reply=( $(dig +short ${1} -c in -t txt 2>/dev/null) ) 855 _st_rc=$? 856 if [ ${_st_rc} -ne 0 ] 857 then 858 _trace_log[${#_trace_log[@]}]='# # # Text lookup error '${_st_rc}' on '${1}' # # #' 859 # [ ${_st_rc} -ne 9 ] && pend_drop 860 return ${_st_rc} 861 else 862 # Some versions of 'dig' return warnings on stdout. 863 _st_cnt=${#_st_reply[@]} 864 for (( _st = 0 ; _st < ${#_st_cnt} ; _st++ )) 865 do 866 [ 'x'${_st_reply[${_st}]:0:2} == 'x;;' ] && 867 unset _st_reply[${_st}] 868 done 869 eval $2=\( \$\{_st_reply\[@\]\} \) 870 fi 871 return 0 872 } 873 874 # The long forms, a.k.a., the parse it yourself versions 875 876 # RFC 2782 Service lookups 877 # dig +noall +nofail +answer _ldap._tcp.openldap.org -t srv 878 # _<service>._<protocol>.<domain_name> 879 # _ldap._tcp.openldap.org. 3600 IN SRV 0 0 389 ldap.openldap.org. 880 # domain TTL Class SRV Priority Weight Port Target 881 882 # Forward lookup :: Name -> poor man's zone transfer 883 # long_fwd <domain_name> <array_name> 884 long_fwd() { 885 local -a _lf_reply 886 local -i _lf_rc 887 local -i _lf_cnt 888 IFS=${NO_WSP} 889 echo -n ':' 890 # echo 'lfwd: '${1} 891 _lf_reply=( $( 892 dig +noall +nofail +answer +authority +additional \ 893 ${1} -t soa ${1} -t mx ${1} -t any 2>/dev/null) ) 894 _lf_rc=$? 895 if [ ${_lf_rc} -ne 0 ] 896 then 897 _trace_log[${#_trace_log[@]}]='# # # Zone lookup error '${_lf_rc}' on '${1}' # # #' 898 # [ ${_lf_rc} -ne 9 ] && pend_drop 899 return ${_lf_rc} 900 else 901 # Some versions of 'dig' return warnings on stdout. 902 _lf_cnt=${#_lf_reply[@]} 903 for (( _lf = 0 ; _lf < ${_lf_cnt} ; _lf++ )) 904 do 905 [ 'x'${_lf_reply[${_lf}]:0:2} == 'x;;' ] && 906 unset _lf_reply[${_lf}] 907 done 908 eval $2=\( \$\{_lf_reply\[@\]\} \) 909 fi 910 return 0 911 } 912 # The reverse lookup domain name corresponding to the IPv6 address: 913 # 4321:0:1:2:3:4:567:89ab 914 # would be (nibble, I.E: Hexdigit) reversed: 915 # b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.IP6.ARPA. 916 917 # Reverse lookup :: Address -> poor man's delegation chain 918 # long_rev <rev_ip_address> <array_name> 919 long_rev() { 920 local -a _lr_reply 921 local -i _lr_rc 922 local -i _lr_cnt 923 local _lr_dns 924 _lr_dns=${1}'.in-addr.arpa.' 925 IFS=${NO_WSP} 926 echo -n ':' 927 # echo 'lrev: '${1} 928 _lr_reply=( $( 929 dig +noall +nofail +answer +authority +additional \ 930 ${_lr_dns} -t soa ${_lr_dns} -t any 2>/dev/null) ) 931 _lr_rc=$? 932 if [ ${_lr_rc} -ne 0 ] 933 then 934 _trace_log[${#_trace_log[@]}]='# # # Delegation lookup error '${_lr_rc}' on '${1}' # # #' 935 # [ ${_lr_rc} -ne 9 ] && pend_drop 936 return ${_lr_rc} 937 else 938 # Some versions of 'dig' return warnings on stdout. 939 _lr_cnt=${#_lr_reply[@]} 940 for (( _lr = 0 ; _lr < ${_lr_cnt} ; _lr++ )) 941 do 942 [ 'x'${_lr_reply[${_lr}]:0:2} == 'x;;' ] && 943 unset _lr_reply[${_lr}] 944 done 945 eval $2=\( \$\{_lr_reply\[@\]\} \) 946 fi 947 return 0 948 } 949 950 # # # Application specific functions # # # 951 952 # Mung a possible name; suppresses root and TLDs. 953 # name_fixup <string> 954 name_fixup(){ 955 local -a _nf_tmp 956 local -i _nf_end 957 local _nf_str 958 local IFS 959 _nf_str=$(to_lower ${1}) 960 _nf_str=$(to_dot ${_nf_str}) 961 _nf_end=${#_nf_str}-1 962 [ ${_nf_str:${_nf_end}} != '.' ] && 963 _nf_str=${_nf_str}'.' 964 IFS=${ADR_IFS} 965 _nf_tmp=( ${_nf_str} ) 966 IFS=${WSP_IFS} 967 _nf_end=${#_nf_tmp[@]} 968 case ${_nf_end} in 969 0) # No dots, only dots 970 echo 971 return 1 972 ;; 973 1) # Only a TLD. 974 echo 975 return 1 976 ;; 977 2) # Maybe okay. 978 echo ${_nf_str} 979 return 0 980 # Needs a lookup table? 981 if [ ${#_nf_tmp[1]} -eq 2 ] 982 then # Country coded TLD. 983 echo 984 return 1 985 else 986 echo ${_nf_str} 987 return 0 988 fi 989 ;; 990 esac 991 echo ${_nf_str} 992 return 0 993 } 994 995 # Grope and mung original input(s). 996 split_input() { 997 [ ${#uc_name[@]} -gt 0 ] || return 0 998 local -i _si_cnt 999 local -i _si_len 1000 local _si_str 1001 unique_lines uc_name uc_name 1002 _si_cnt=${#uc_name[@]} 1003 for (( _si = 0 ; _si < _si_cnt ; _si++ )) 1004 do 1005 _si_str=${uc_name[$_si]} 1006 if is_address ${_si_str} 1007 then 1008 uc_address[${#uc_address[@]}]=${_si_str} 1009 unset uc_name[$_si] 1010 else 1011 if ! uc_name[$_si]=$(name_fixup ${_si_str}) 1012 then 1013 unset ucname[$_si] 1014 fi 1015 fi 1016 done 1017 uc_name=( ${uc_name[@]} ) 1018 _si_cnt=${#uc_name[@]} 1019 _trace_log[${#_trace_log[@]}]='# # # Input '${_si_cnt}' unchecked name input(s). # # #' 1020 _si_cnt=${#uc_address[@]} 1021 _trace_log[${#_trace_log[@]}]='# # # Input '${_si_cnt}' unchecked address input(s). # # #' 1022 return 0 1023 } 1024 1025 # # # Discovery functions -- recursively interlocked by external data # # # 1026 # # # The leading 'if list is empty; return 0' in each is required. # # # 1027 1028 # Recursion limiter 1029 # limit_chk() <next_level> 1030 limit_chk() { 1031 local -i _lc_lmt 1032 # Check indirection limit. 1033 if [ ${indirect} -eq 0 ] || [ $# -eq 0 ] 1034 then 1035 # The 'do-forever' choice 1036 echo 1 # Any value will do. 1037 return 0 # OK to continue. 1038 else 1039 # Limiting is in effect. 1040 if [ ${indirect} -lt ${1} ] 1041 then 1042 echo ${1} # Whatever. 1043 return 1 # Stop here. 1044 else 1045 _lc_lmt=${1}+1 # Bump the given limit. 1046 echo ${_lc_lmt} # Echo it. 1047 return 0 # OK to continue. 1048 fi 1049 fi 1050 } 1051 1052 # For each name in uc_name: 1053 # Move name to chk_name. 1054 # Add addresses to uc_address. 1055 # Pend expand_input_address. 1056 # Repeat until nothing new found. 1057 # expand_input_name <indirection_limit> 1058 expand_input_name() { 1059 [ ${#uc_name[@]} -gt 0 ] || return 0 1060 local -a _ein_addr 1061 local -a _ein_new 1062 local -i _ucn_cnt 1063 local -i _ein_cnt 1064 local _ein_tst 1065 _ucn_cnt=${#uc_name[@]} 1066 1067 if ! _ein_cnt=$(limit_chk ${1}) 1068 then 1069 return 0 1070 fi 1071 1072 for (( _ein = 0 ; _ein < _ucn_cnt ; _ein++ )) 1073 do 1074 if short_fwd ${uc_name[${_ein}]} _ein_new 1075 then 1076 for (( _ein_cnt = 0 ; _ein_cnt < ${#_ein_new[@]}; _ein_cnt++ )) 1077 do 1078 _ein_tst=${_ein_new[${_ein_cnt}]} 1079 if is_address ${_ein_tst} 1080 then 1081 _ein_addr[${#_ein_addr[@]}]=${_ein_tst} 1082 fi 1083 done 1084 fi 1085 done 1086 unique_lines _ein_addr _ein_addr # Scrub duplicates. 1087 edit_exact chk_address _ein_addr # Scrub pending detail. 1088 edit_exact known_address _ein_addr # Scrub already detailed. 1089 if [ ${#_ein_addr[@]} -gt 0 ] # Anything new? 1090 then 1091 uc_address=( ${uc_address[@]} ${_ein_addr[@]} ) 1092 pend_func expand_input_address ${1} 1093 _trace_log[${#_trace_log[@]}]='# # # Added '${#_ein_addr[@]}' unchecked address input(s). # # #' 1094 fi 1095 edit_exact chk_name uc_name # Scrub pending detail. 1096 edit_exact known_name uc_name # Scrub already detailed. 1097 if [ ${#uc_name[@]} -gt 0 ] 1098 then 1099 chk_name=( ${chk_name[@]} ${uc_name[@]} ) 1100 pend_func detail_each_name ${1} 1101 fi 1102 unset uc_name[@] 1103 return 0 1104 } 1105 1106 # For each address in uc_address: 1107 # Move address to chk_address. 1108 # Add names to uc_name. 1109 # Pend expand_input_name. 1110 # Repeat until nothing new found. 1111 # expand_input_address <indirection_limit> 1112 expand_input_address() { 1113 [ ${#uc_address[@]} -gt 0 ] || return 0 1114 local -a _eia_addr 1115 local -a _eia_name 1116 local -a _eia_new 1117 local -i _uca_cnt 1118 local -i _eia_cnt 1119 local _eia_tst 1120 unique_lines uc_address _eia_addr 1121 unset uc_address[@] 1122 edit_exact been_there_addr _eia_addr 1123 _uca_cnt=${#_eia_addr[@]} 1124 [ ${_uca_cnt} -gt 0 ] && 1125 been_there_addr=( ${been_there_addr[@]} ${_eia_addr[@]} ) 1126 1127 for (( _eia = 0 ; _eia < _uca_cnt ; _eia++ )) 1128 do 1129 if short_rev ${_eia_addr[${_eia}]} _eia_new 1130 then 1131 for (( _eia_cnt = 0 ; _eia_cnt < ${#_eia_new[@]} ; _eia_cnt++ )) 1132 do 1133 _eia_tst=${_eia_new[${_eia_cnt}]} 1134 if _eia_tst=$(name_fixup ${_eia_tst}) 1135 then 1136 _eia_name[${#_eia_name[@]}]=${_eia_tst} 1137 fi 1138 done 1139 fi 1140 done 1141 unique_lines _eia_name _eia_name # Scrub duplicates. 1142 edit_exact chk_name _eia_name # Scrub pending detail. 1143 edit_exact known_name _eia_name # Scrub already detailed. 1144 if [ ${#_eia_name[@]} -gt 0 ] # Anything new? 1145 then 1146 uc_name=( ${uc_name[@]} ${_eia_name[@]} ) 1147 pend_func expand_input_name ${1} 1148 _trace_log[${#_trace_log[@]}]='# # # Added '${#_eia_name[@]}' unchecked name input(s). # # #' 1149 fi 1150 edit_exact chk_address _eia_addr # Scrub pending detail. 1151 edit_exact known_address _eia_addr # Scrub already detailed. 1152 if [ ${#_eia_addr[@]} -gt 0 ] # Anything new? 1153 then 1154 chk_address=( ${chk_address[@]} ${_eia_addr[@]} ) 1155 pend_func detail_each_address ${1} 1156 fi 1157 return 0 1158 } 1159 1160 # The parse-it-yourself zone reply. 1161 # The input is the chk_name list. 1162 # detail_each_name <indirection_limit> 1163 detail_each_name() { 1164 [ ${#chk_name[@]} -gt 0 ] || return 0 1165 local -a _den_chk # Names to check 1166 local -a _den_name # Names found here 1167 local -a _den_address # Addresses found here 1168 local -a _den_pair # Pairs found here 1169 local -a _den_rev # Reverse pairs found here 1170 local -a _den_tmp # Line being parsed 1171 local -a _den_auth # SOA contact being parsed 1172 local -a _den_new # The zone reply 1173 local -a _den_pc # Parent-Child gets big fast 1174 local -a _den_ref # So does reference chain 1175 local -a _den_nr # Name-Resource can be big 1176 local -a _den_na # Name-Address 1177 local -a _den_ns # Name-Service 1178 local -a _den_achn # Chain of Authority 1179 local -i _den_cnt # Count of names to detail 1180 local -i _den_lmt # Indirection limit 1181 local _den_who # Named being processed 1182 local _den_rec # Record type being processed 1183 local _den_cont # Contact domain 1184 local _den_str # Fixed up name string 1185 local _den_str2 # Fixed up reverse 1186 local IFS=${WSP_IFS} 1187 1188 # Local, unique copy of names to check 1189 unique_lines chk_name _den_chk 1190 unset chk_name[@] # Done with globals. 1191 1192 # Less any names already known 1193 edit_exact known_name _den_chk 1194 _den_cnt=${#_den_chk[@]} 1195 1196 # If anything left, add to known_name. 1197 [ ${_den_cnt} -gt 0 ] && 1198 known_name=( ${known_name[@]} ${_den_chk[@]} ) 1199 1200 # for the list of (previously) unknown names . . . 1201 for (( _den = 0 ; _den < _den_cnt ; _den++ )) 1202 do 1203 _den_who=${_den_chk[${_den}]} 1204 if long_fwd ${_den_who} _den_new 1205 then 1206 unique_lines _den_new _den_new 1207 if [ ${#_den_new[@]} -eq 0 ] 1208 then 1209 _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who} 1210 fi 1211 1212 # Parse each line in the reply. 1213 for (( _line = 0 ; _line < ${#_den_new[@]} ; _line++ )) 1214 do 1215 IFS=${NO_WSP}$'\x09'$'\x20' 1216 _den_tmp=( ${_den_new[${_line}]} ) 1217 IFS=${WSP_IFS} 1218 # If usable record and not a warning message . . . 1219 if [ ${#_den_tmp[@]} -gt 4 ] && [ 'x'${_den_tmp[0]} != 'x;;' ] 1220 then 1221 _den_rec=${_den_tmp[3]} 1222 _den_nr[${#_den_nr[@]}]=${_den_who}' '${_den_rec} 1223 # Begin at RFC1033 (+++) 1224 case ${_den_rec} in 1225 1226 #<name> [<ttl>] [<class>] SOA <origin> <person> 1227 SOA) # Start Of Authority 1228 if _den_str=$(name_fixup ${_den_tmp[0]}) 1229 then 1230 _den_name[${#_den_name[@]}]=${_den_str} 1231 _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str}' SOA' 1232 # SOA origin -- domain name of master zone record 1233 if _den_str2=$(name_fixup ${_den_tmp[4]}) 1234 then 1235 _den_name[${#_den_name[@]}]=${_den_str2} 1236 _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str2}' SOA.O' 1237 fi 1238 # Responsible party e-mail address (possibly bogus). 1239 # Possibility of first.last@domain.name ignored. 1240 set -f 1241 if _den_str2=$(name_fixup ${_den_tmp[5]}) 1242 then 1243 IFS=${ADR_IFS} 1244 _den_auth=( ${_den_str2} ) 1245 IFS=${WSP_IFS} 1246 if [ ${#_den_auth[@]} -gt 2 ] 1247 then 1248 _den_cont=${_den_auth[1]} 1249 for (( _auth = 2 ; _auth < ${#_den_auth[@]} ; _auth++ )) 1250 do 1251 _den_cont=${_den_cont}'.'${_den_auth[${_auth}]} 1252 done 1253 _den_name[${#_den_name[@]}]=${_den_cont}'.' 1254 _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_cont}'. SOA.C' 1255 fi 1256 fi 1257 set +f 1258 fi 1259 ;; 1260 1261 1262 A) # IP(v4) Address Record 1263 if _den_str=$(name_fixup ${_den_tmp[0]}) 1264 then 1265 _den_name[${#_den_name[@]}]=${_den_str} 1266 _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str} 1267 _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]} 1268 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' A' 1269 else 1270 _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain' 1271 _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]} 1272 _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain A' 1273 fi 1274 _den_address[${#_den_address[@]}]=${_den_tmp[4]} 1275 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]} 1276 ;; 1277 1278 NS) # Name Server Record 1279 # Domain name being serviced (may be other than current) 1280 if _den_str=$(name_fixup ${_den_tmp[0]}) 1281 then 1282 _den_name[${#_den_name[@]}]=${_den_str} 1283 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' NS' 1284 1285 # Domain name of service provider 1286 if _den_str2=$(name_fixup ${_den_tmp[4]}) 1287 then 1288 _den_name[${#_den_name[@]}]=${_den_str2} 1289 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' NSH' 1290 _den_ns[${#_den_ns[@]}]=${_den_str2}' NS' 1291 _den_pc[${#_den_pc[@]}]=${_den_str}' '${_den_str2} 1292 fi 1293 fi 1294 ;; 1295 1296 MX) # Mail Server Record 1297 # Domain name being serviced (wildcards not handled here) 1298 if _den_str=$(name_fixup ${_den_tmp[0]}) 1299 then 1300 _den_name[${#_den_name[@]}]=${_den_str} 1301 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MX' 1302 fi 1303 # Domain name of service provider 1304 if _den_str=$(name_fixup ${_den_tmp[5]}) 1305 then 1306 _den_name[${#_den_name[@]}]=${_den_str} 1307 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MXH' 1308 _den_ns[${#_den_ns[@]}]=${_den_str}' MX' 1309 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str} 1310 fi 1311 ;; 1312 1313 PTR) # Reverse address record 1314 # Special name 1315 if _den_str=$(name_fixup ${_den_tmp[0]}) 1316 then 1317 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' PTR' 1318 # Host name (not a CNAME) 1319 if _den_str2=$(name_fixup ${_den_tmp[4]}) 1320 then 1321 _den_rev[${#_den_rev[@]}]=${_den_str}' '${_den_str2} 1322 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' PTRH' 1323 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str} 1324 fi 1325 fi 1326 ;; 1327 1328 AAAA) # IP(v6) Address Record 1329 if _den_str=$(name_fixup ${_den_tmp[0]}) 1330 then 1331 _den_name[${#_den_name[@]}]=${_den_str} 1332 _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str} 1333 _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]} 1334 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' AAAA' 1335 else 1336 _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain' 1337 _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]} 1338 _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain' 1339 fi 1340 # No processing for IPv6 addresses 1341 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]} 1342 ;; 1343 1344 CNAME) # Alias name record 1345 # Nickname 1346 if _den_str=$(name_fixup ${_den_tmp[0]}) 1347 then 1348 _den_name[${#_den_name[@]}]=${_den_str} 1349 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CNAME' 1350 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str} 1351 fi 1352 # Hostname 1353 if _den_str=$(name_fixup ${_den_tmp[4]}) 1354 then 1355 _den_name[${#_den_name[@]}]=${_den_str} 1356 _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CHOST' 1357 _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str} 1358 fi 1359 ;; 1360 # TXT) 1361 # ;; 1362 esac 1363 fi 1364 done 1365 else # Lookup error == 'A' record 'unknown address' 1366 _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who} 1367 fi 1368 done 1369 1370 # Control dot array growth. 1371 unique_lines _den_achn _den_achn # Works best, all the same. 1372 edit_exact auth_chain _den_achn # Works best, unique items. 1373 if [ ${#_den_achn[@]} -gt 0 ] 1374 then 1375 IFS=${NO_WSP} 1376 auth_chain=( ${auth_chain[@]} ${_den_achn[@]} ) 1377 IFS=${WSP_IFS} 1378 fi 1379 1380 unique_lines _den_ref _den_ref # Works best, all the same. 1381 edit_exact ref_chain _den_ref # Works best, unique items. 1382 if [ ${#_den_ref[@]} -gt 0 ] 1383 then 1384 IFS=${NO_WSP} 1385 ref_chain=( ${ref_chain[@]} ${_den_ref[@]} ) 1386 IFS=${WSP_IFS} 1387 fi 1388 1389 unique_lines _den_na _den_na 1390 edit_exact name_address _den_na 1391 if [ ${#_den_na[@]} -gt 0 ] 1392 then 1393 IFS=${NO_WSP} 1394 name_address=( ${name_address[@]} ${_den_na[@]} ) 1395 IFS=${WSP_IFS} 1396 fi 1397 1398 unique_lines _den_ns _den_ns 1399 edit_exact name_srvc _den_ns 1400 if [ ${#_den_ns[@]} -gt 0 ] 1401 then 1402 IFS=${NO_WSP} 1403 name_srvc=( ${name_srvc[@]} ${_den_ns[@]} ) 1404 IFS=${WSP_IFS} 1405 fi 1406 1407 unique_lines _den_nr _den_nr 1408 edit_exact name_resource _den_nr 1409 if [ ${#_den_nr[@]} -gt 0 ] 1410 then 1411 IFS=${NO_WSP} 1412 name_resource=( ${name_resource[@]} ${_den_nr[@]} ) 1413 IFS=${WSP_IFS} 1414 fi 1415 1416 unique_lines _den_pc _den_pc 1417 edit_exact parent_child _den_pc 1418 if [ ${#_den_pc[@]} -gt 0 ] 1419 then 1420 IFS=${NO_WSP} 1421 parent_child=( ${parent_child[@]} ${_den_pc[@]} ) 1422 IFS=${WSP_IFS} 1423 fi 1424 1425 # Update list known_pair (Address and Name). 1426 unique_lines _den_pair _den_pair 1427 edit_exact known_pair _den_pair 1428 if [ ${#_den_pair[@]} -gt 0 ] # Anything new? 1429 then 1430 IFS=${NO_WSP} 1431 known_pair=( ${known_pair[@]} ${_den_pair[@]} ) 1432 IFS=${WSP_IFS} 1433 fi 1434 1435 # Update list of reverse pairs. 1436 unique_lines _den_rev _den_rev 1437 edit_exact reverse_pair _den_rev 1438 if [ ${#_den_rev[@]} -gt 0 ] # Anything new? 1439 then 1440 IFS=${NO_WSP} 1441 reverse_pair=( ${reverse_pair[@]} ${_den_rev[@]} ) 1442 IFS=${WSP_IFS} 1443 fi 1444 1445 # Check indirection limit -- give up if reached. 1446 if ! _den_lmt=$(limit_chk ${1}) 1447 then 1448 return 0 1449 fi 1450 1451 # Execution engine is LIFO. Order of pend operations is important. 1452 # Did we define any new addresses? 1453 unique_lines _den_address _den_address # Scrub duplicates. 1454 edit_exact known_address _den_address # Scrub already processed. 1455 edit_exact un_address _den_address # Scrub already waiting. 1456 if [ ${#_den_address[@]} -gt 0 ] # Anything new? 1457 then 1458 uc_address=( ${uc_address[@]} ${_den_address[@]} ) 1459 pend_func expand_input_address ${_den_lmt} 1460 _trace_log[${#_trace_log[@]}]='# # # Added '${#_den_address[@]}' unchecked address(s). # # #' 1461 fi 1462 1463 # Did we find any new names? 1464 unique_lines _den_name _den_name # Scrub duplicates. 1465 edit_exact known_name _den_name # Scrub already processed. 1466 edit_exact uc_name _den_name # Scrub already waiting. 1467 if [ ${#_den_name[@]} -gt 0 ] # Anything new? 1468 then 1469 uc_name=( ${uc_name[@]} ${_den_name[@]} ) 1470 pend_func expand_input_name ${_den_lmt} 1471 _trace_log[${#_trace_log[@]}]='# # # Added '${#_den_name[@]}' unchecked name(s). # # #' 1472 fi 1473 return 0 1474 } 1475 1476 # The parse-it-yourself delegation reply 1477 # Input is the chk_address list. 1478 # detail_each_address <indirection_limit> 1479 detail_each_address() { 1480 [ ${#chk_address[@]} -gt 0 ] || return 0 1481 unique_lines chk_address chk_address 1482 edit_exact known_address chk_address 1483 if [ ${#chk_address[@]} -gt 0 ] 1484 then 1485 known_address=( ${known_address[@]} ${chk_address[@]} ) 1486 unset chk_address[@] 1487 fi 1488 return 0 1489 } 1490 1491 # # # Application specific output functions # # # 1492 1493 # Pretty print the known pairs. 1494 report_pairs() { 1495 echo 1496 echo 'Known network pairs.' 1497 col_print known_pair 2 5 30 1498 1499 if [ ${#auth_chain[@]} -gt 0 ] 1500 then 1501 echo 1502 echo 'Known chain of authority.' 1503 col_print auth_chain 2 5 30 55 1504 fi 1505 1506 if [ ${#reverse_pair[@]} -gt 0 ] 1507 then 1508 echo 1509 echo 'Known reverse pairs.' 1510 col_print reverse_pair 2 5 55 1511 fi 1512 return 0 1513 } 1514 1515 # Check an address against the list of blacklist servers. 1516 # A good place to capture for GraphViz: address->status(server(reports)) 1517 # check_lists <ip_address> 1518 check_lists() { 1519 [ $# -eq 1 ] || return 1 1520 local -a _cl_fwd_addr 1521 local -a _cl_rev_addr 1522 local -a _cl_reply 1523 local -i _cl_rc 1524 local -i _ls_cnt 1525 local _cl_dns_addr 1526 local _cl_lkup 1527 1528 split_ip ${1} _cl_fwd_addr _cl_rev_addr 1529 _cl_dns_addr=$(dot_array _cl_rev_addr)'.' 1530 _ls_cnt=${#list_server[@]} 1531 echo ' Checking address '${1} 1532 for (( _cl = 0 ; _cl < _ls_cnt ; _cl++ )) 1533 do 1534 _cl_lkup=${_cl_dns_addr}${list_server[${_cl}]} 1535 if short_text ${_cl_lkup} _cl_reply 1536 then 1537 if [ ${#_cl_reply[@]} -gt 0 ] 1538 then 1539 echo ' Records from '${list_server[${_cl}]} 1540 address_hits[${#address_hits[@]}]=${1}' '${list_server[${_cl}]} 1541 _hs_RC=2 1542 for (( _clr = 0 ; _clr < ${#_cl_reply[@]} ; _clr++ )) 1543 do 1544 echo ' '${_cl_reply[${_clr}]} 1545 done 1546 fi 1547 fi 1548 done 1549 return 0 1550 } 1551 1552 # # # The usual application glue # # # 1553 1554 # Who did it? 1555 credits() { 1556 echo 1557 echo 'Advanced Bash Scripting Guide: is_spammer.bash, v2, 2004-msz' 1558 } 1559 1560 # How to use it? 1561 # (See also, "Quickstart" at end of script.) 1562 usage() { 1563 cat <<-'_usage_statement_' 1564 The script is_spammer.bash requires either one or two arguments. 1565 1566 arg 1) May be one of: 1567 a) A domain name 1568 b) An IPv4 address 1569 c) The name of a file with any mix of names 1570 and addresses, one per line. 1571 1572 arg 2) May be one of: 1573 a) A Blacklist server domain name 1574 b) The name of a file with Blacklist server 1575 domain names, one per line. 1576 c) If not present, a default list of (free) 1577 Blacklist servers is used. 1578 d) If a filename of an empty, readable, file 1579 is given, 1580 Blacklist server lookup is disabled. 1581 1582 All script output is written to stdout. 1583 1584 Return codes: 0 -> All OK, 1 -> Script failure, 1585 2 -> Something is Blacklisted. 1586 1587 Requires the external program 'dig' from the 'bind-9' 1588 set of DNS programs. See: http://www.isc.org 1589 1590 The domain name lookup depth limit defaults to 2 levels. 1591 Set the environment variable SPAMMER_LIMIT to change. 1592 SPAMMER_LIMIT=0 means 'unlimited' 1593 1594 Limit may also be set on the command line. 1595 If arg#1 is an integer, the limit is set to that value 1596 and then the above argument rules are applied. 1597 1598 Setting the environment variable 'SPAMMER_DATA' to a filename 1599 will cause the script to write a GraphViz graphic file. 1600 1601 For the development version; 1602 Setting the environment variable 'SPAMMER_TRACE' to a filename 1603 will cause the execution engine to log a function call trace. 1604 1605 _usage_statement_ 1606 } 1607 1608 # The default list of Blacklist servers: 1609 # Many choices, see: http://www.spews.org/lists.html 1610 1611 declare -a default_servers 1612 # See: http://www.spamhaus.org (Conservative, well maintained) 1613 default_servers[0]='sbl-xbl.spamhaus.org' 1614 # See: http://ordb.org (Open mail relays) 1615 default_servers[1]='relays.ordb.org' 1616 # See: http://www.spamcop.net/ (You can report spammers here) 1617 default_servers[2]='bl.spamcop.net' 1618 # See: http://www.spews.org (An 'early detect' system) 1619 default_servers[3]='l2.spews.dnsbl.sorbs.net' 1620 # See: http://www.dnsbl.us.sorbs.net/using.shtml 1621 default_servers[4]='dnsbl.sorbs.net' 1622 # See: http://dsbl.org/usage (Various mail relay lists) 1623 default_servers[5]='list.dsbl.org' 1624 default_servers[6]='multihop.dsbl.org' 1625 default_servers[7]='unconfirmed.dsbl.org' 1626 1627 # User input argument #1 1628 setup_input() { 1629 if [ -e ${1} ] && [ -r ${1} ] # Name of readable file 1630 then 1631 file_to_array ${1} uc_name 1632 echo 'Using filename >'${1}'< as input.' 1633 else 1634 if is_address ${1} # IP address? 1635 then 1636 uc_address=( ${1} ) 1637 echo 'Starting with address >'${1}'<' 1638 else # Must be a name. 1639 uc_name=( ${1} ) 1640 echo 'Starting with domain name >'${1}'<' 1641 fi 1642 fi 1643 return 0 1644 } 1645 1646 # User input argument #2 1647 setup_servers() { 1648 if [ -e ${1} ] && [ -r ${1} ] # Name of a readable file 1649 then 1650 file_to_array ${1} list_server 1651 echo 'Using filename >'${1}'< as blacklist server list.' 1652 else 1653 list_server=( ${1} ) 1654 echo 'Using blacklist server >'${1}'<' 1655 fi 1656 return 0 1657 } 1658 1659 # User environment variable SPAMMER_TRACE 1660 live_log_die() { 1661 if [ ${SPAMMER_TRACE:=} ] # Wants trace log? 1662 then 1663 if [ ! -e ${SPAMMER_TRACE} ] 1664 then 1665 if ! touch ${SPAMMER_TRACE} 2>/dev/null 1666 then 1667 pend_func echo $(printf '%q\n' \ 1668 'Unable to create log file >'${SPAMMER_TRACE}'<') 1669 pend_release 1670 exit 1 1671 fi 1672 _log_file=${SPAMMER_TRACE} 1673 _pend_hook_=trace_logger 1674 _log_dump=dump_log 1675 else 1676 if [ ! -w ${SPAMMER_TRACE} ] 1677 then 1678 pend_func echo $(printf '%q\n' \ 1679 'Unable to write log file >'${SPAMMER_TRACE}'<') 1680 pend_release 1681 exit 1 1682 fi 1683 _log_file=${SPAMMER_TRACE} 1684 echo '' > ${_log_file} 1685 _pend_hook_=trace_logger 1686 _log_dump=dump_log 1687 fi 1688 fi 1689 return 0 1690 } 1691 1692 # User environment variable SPAMMER_DATA 1693 data_capture() { 1694 if [ ${SPAMMER_DATA:=} ] # Wants a data dump? 1695 then 1696 if [ ! -e ${SPAMMER_DATA} ] 1697 then 1698 if ! touch ${SPAMMER_DATA} 2>/dev/null 1699 then 1700 pend_func echo $(printf '%q]n' \ 1701 'Unable to create data output file >'${SPAMMER_DATA}'<') 1702 pend_release 1703 exit 1 1704 fi 1705 _dot_file=${SPAMMER_DATA} 1706 _dot_dump=dump_dot 1707 else 1708 if [ ! -w ${SPAMMER_DATA} ] 1709 then 1710 pend_func echo $(printf '%q\n' \ 1711 'Unable to write data output file >'${SPAMMER_DATA}'<') 1712 pend_release 1713 exit 1 1714 fi 1715 _dot_file=${SPAMMER_DATA} 1716 _dot_dump=dump_dot 1717 fi 1718 fi 1719 return 0 1720 } 1721 1722 # Grope user specified arguments. 1723 do_user_args() { 1724 if [ $# -gt 0 ] && is_number $1 1725 then 1726 indirect=$1 1727 shift 1728 fi 1729 1730 case $# in # Did user treat us well? 1731 1) 1732 if ! setup_input $1 # Needs error checking. 1733 then 1734 pend_release 1735 $_log_dump 1736 exit 1 1737 fi 1738 list_server=( ${default_servers[@]} ) 1739 _list_cnt=${#list_server[@]} 1740 echo 'Using default blacklist server list.' 1741 echo 'Search depth limit: '${indirect} 1742 ;; 1743 2) 1744 if ! setup_input $1 # Needs error checking. 1745 then 1746 pend_release 1747 $_log_dump 1748 exit 1 1749 fi 1750 if ! setup_servers $2 # Needs error checking. 1751 then 1752 pend_release 1753 $_log_dump 1754 exit 1 1755 fi 1756 echo 'Search depth limit: '${indirect} 1757 ;; 1758 *) 1759 pend_func usage 1760 pend_release 1761 $_log_dump 1762 exit 1 1763 ;; 1764 esac 1765 return 0 1766 } 1767 1768 # A general purpose debug tool. 1769 # list_array <array_name> 1770 list_array() { 1771 [ $# -eq 1 ] || return 1 # One argument required. 1772 1773 local -a _la_lines 1774 set -f 1775 local IFS=${NO_WSP} 1776 eval _la_lines=\(\ \$\{$1\[@\]\}\ \) 1777 echo 1778 echo "Element count "${#_la_lines[@]}" array "${1} 1779 local _ln_cnt=${#_la_lines[@]} 1780 1781 for (( _i = 0; _i < ${_ln_cnt}; _i++ )) 1782 do 1783 echo 'Element '$_i' >'${_la_lines[$_i]}'<' 1784 done 1785 set +f 1786 return 0 1787 } 1788 1789 # # # 'Hunt the Spammer' program code # # # 1790 pend_init # Ready stack engine. 1791 pend_func credits # Last thing to print. 1792 1793 # # # Deal with user # # # 1794 live_log_die # Setup debug trace log. 1795 data_capture # Setup data capture file. 1796 echo 1797 do_user_args $@ 1798 1799 # # # Haven't exited yet - There is some hope # # # 1800 # Discovery group - Execution engine is LIFO - pend 1801 # in reverse order of execution. 1802 _hs_RC=0 # Hunt the Spammer return code 1803 pend_mark 1804 pend_func report_pairs # Report name-address pairs. 1805 1806 # The two detail_* are mutually recursive functions. 1807 # They also pend expand_* functions as required. 1808 # These two (the last of ???) exit the recursion. 1809 pend_func detail_each_address # Get all resources of addresses. 1810 pend_func detail_each_name # Get all resources of names. 1811 1812 # The two expand_* are mutually recursive functions, 1813 #+ which pend additional detail_* functions as required. 1814 pend_func expand_input_address 1 # Expand input names by address. 1815 pend_func expand_input_name 1 # #xpand input addresses by name. 1816 1817 # Start with a unique set of names and addresses. 1818 pend_func unique_lines uc_address uc_address 1819 pend_func unique_lines uc_name uc_name 1820 1821 # Separate mixed input of names and addresses. 1822 pend_func split_input 1823 pend_release 1824 1825 # # # Pairs reported -- Unique list of IP addresses found 1826 echo 1827 _ip_cnt=${#known_address[@]} 1828 if [ ${#list_server[@]} -eq 0 ] 1829 then 1830 echo 'Blacklist server list empty, none checked.' 1831 else 1832 if [ ${_ip_cnt} -eq 0 ] 1833 then 1834 echo 'Known address list empty, none checked.' 1835 else 1836 _ip_cnt=${_ip_cnt}-1 # Start at top. 1837 echo 'Checking Blacklist servers.' 1838 for (( _ip = _ip_cnt ; _ip >= 0 ; _ip-- )) 1839 do 1840 pend_func check_lists $( printf '%q\n' ${known_address[$_ip]} ) 1841 done 1842 fi 1843 fi 1844 pend_release 1845 $_dot_dump # Graphics file dump 1846 $_log_dump # Execution trace 1847 echo 1848 1849 1850 ############################## 1851 # Example output from script # 1852 ############################## 1853 :<<-'_is_spammer_outputs_' 1854 1855 ./is_spammer.bash 0 web4.alojamentos7.com 1856 1857 Starting with domain name >web4.alojamentos7.com< 1858 Using default blacklist server list. 1859 Search depth limit: 0 1860 .:....::::...:::...:::.......::..::...:::.......:: 1861 Known network pairs. 1862 66.98.208.97 web4.alojamentos7.com. 1863 66.98.208.97 ns1.alojamentos7.com. 1864 69.56.202.147 ns2.alojamentos.ws. 1865 66.98.208.97 alojamentos7.com. 1866 66.98.208.97 web.alojamentos7.com. 1867 69.56.202.146 ns1.alojamentos.ws. 1868 69.56.202.146 alojamentos.ws. 1869 66.235.180.113 ns1.alojamentos.org. 1870 66.235.181.192 ns2.alojamentos.org. 1871 66.235.180.113 alojamentos.org. 1872 66.235.180.113 web6.alojamentos.org. 1873 216.234.234.30 ns1.theplanet.com. 1874 12.96.160.115 ns2.theplanet.com. 1875 216.185.111.52 mail1.theplanet.com. 1876 69.56.141.4 spooling.theplanet.com. 1877 216.185.111.40 theplanet.com. 1878 216.185.111.40 www.theplanet.com. 1879 216.185.111.52 mail.theplanet.com. 1880 1881 Checking Blacklist servers. 1882 Checking address 66.98.208.97 1883 Records from dnsbl.sorbs.net 1884 "Spam Received See: http://www.dnsbl.sorbs.net/lookup.shtml?66.98.208.97" 1885 Checking address 69.56.202.147 1886 Checking address 69.56.202.146 1887 Checking address 66.235.180.113 1888 Checking address 66.235.181.192 1889 Checking address 216.185.111.40 1890 Checking address 216.234.234.30 1891 Checking address 12.96.160.115 1892 Checking address 216.185.111.52 1893 Checking address 69.56.141.4 1894 1895 Advanced Bash Scripting Guide: is_spammer.bash, v2, 2004-msz 1896 1897 _is_spammer_outputs_ 1898 1899 exit ${_hs_RC} 1900 1901 #################################################### 1902 # The script ignores everything from here on down # 1903 #+ because of the 'exit' command, just above. # 1904 #################################################### 1905 1906 1907 1908 Quickstart 1909 ========== 1910 1911 Prerequisites 1912 1913 Bash version 2.05b or 3.00 (bash --version) 1914 A version of Bash which supports arrays. Array 1915 support is included by default Bash configurations. 1916 1917 'dig,' version 9.x.x (dig $HOSTNAME, see first line of output) 1918 A version of dig which supports the +short options. 1919 See: dig_wrappers.bash for details. 1920 1921 1922 Optional Prerequisites 1923 1924 'named,' a local DNS caching program. Any flavor will do. 1925 Do twice: dig $HOSTNAME 1926 Check near bottom of output for: SERVER: 127.0.0.1#53 1927 That means you have one running. 1928 1929 1930 Optional Graphics Support 1931 1932 'date,' a standard *nix thing. (date -R) 1933 1934 dot Program to convert graphic description file to a 1935 diagram. (dot -V) 1936 A part of the Graph-Viz set of programs. 1937 See: [http://www.research.att.com/sw/tools/graphviz||GraphViz] 1938 1939 'dotty,' a visual editor for graphic description files. 1940 Also a part of the Graph-Viz set of programs. 1941 1942 1943 1944 1945 Quick Start 1946 1947 In the same directory as the is_spammer.bash script; 1948 Do: ./is_spammer.bash 1949 1950 Usage Details 1951 1952 1. Blacklist server choices. 1953 1954 (a) To use default, built-in list: Do nothing. 1955 1956 (b) To use your own list: 1957 1958 i. Create a file with a single Blacklist server 1959 domain name per line. 1960 1961 ii. Provide that filename as the last argument to 1962 the script. 1963 1964 (c) To use a single Blacklist server: Last argument 1965 to the script. 1966 1967 (d) To disable Blacklist lookups: 1968 1969 i. Create an empty file (touch spammer.nul) 1970 Your choice of filename. 1971 1972 ii. Provide the filename of that empty file as the 1973 last argument to the script. 1974 1975 2. Search depth limit. 1976 1977 (a) To use the default value of 2: Do nothing. 1978 1979 (b) To set a different limit: 1980 A limit of 0 means: no limit. 1981 1982 i. export SPAMMER_LIMIT=1 1983 or whatever limit you want. 1984 1985 ii. OR provide the desired limit as the first 1986 argument to the script. 1987 1988 3. Optional execution trace log. 1989 1990 (a) To use the default setting of no log output: Do nothing. 1991 1992 (b) To write an execution trace log: 1993 export SPAMMER_TRACE=spammer.log 1994 or whatever filename you want. 1995 1996 4. Optional graphic description file. 1997 1998 (a) To use the default setting of no graphic file: Do nothing. 1999 2000 (b) To write a Graph-Viz graphic description file: 2001 export SPAMMER_DATA=spammer.dot 2002 or whatever filename you want. 2003 2004 5. Where to start the search. 2005 2006 (a) Starting with a single domain name: 2007 2008 i. Without a command line search limit: First 2009 argument to script. 2010 2011 ii. With a command line search limit: Second 2012 argument to script. 2013 2014 (b) Starting with a single IP address: 2015 2016 i. Without a command line search limit: First 2017 argument to script. 2018 2019 ii. With a command line search limit: Second 2020 argument to script. 2021 2022 (c) Starting with (mixed) multiple name(s) and/or address(es): 2023 Create a file with one name or address per line. 2024 Your choice of filename. 2025 2026 i. Without a command line search limit: Filename as 2027 first argument to script. 2028 2029 ii. With a command line search limit: Filename as 2030 second argument to script. 2031 2032 6. What to do with the display output. 2033 2034 (a) To view display output on screen: Do nothing. 2035 2036 (b) To save display output to a file: Redirect stdout to a filename. 2037 2038 (c) To discard display output: Redirect stdout to /dev/null. 2039 2040 7. Temporary end of decision making. 2041 press RETURN 2042 wait (optionally, watch the dots and colons). 2043 2044 8. Optionally check the return code. 2045 2046 (a) Return code 0: All OK 2047 2048 (b) Return code 1: Script setup failure 2049 2050 (c) Return code 2: Something was blacklisted. 2051 2052 9. Where is my graph (diagram)? 2053 2054 The script does not directly produce a graph (diagram). 2055 It only produces a graphic description file. You can 2056 process the graphic descriptor file that was output 2057 with the 'dot' program. 2058 2059 Until you edit that descriptor file, to describe the 2060 relationships you want shown, all that you will get is 2061 a bunch of labeled name and address nodes. 2062 2063 All of the script's discovered relationships are within 2064 a comment block in the graphic descriptor file, each 2065 with a descriptive heading. 2066 2067 The editing required to draw a line between a pair of 2068 nodes from the information in the descriptor file may 2069 be done with a text editor. 2070 2071 Given these lines somewhere in the descriptor file: 2072 2073 # Known domain name nodes 2074 2075 N0000 [label="guardproof.info."] ; 2076 2077 N0002 [label="third.guardproof.info."] ; 2078 2079 2080 2081 # Known address nodes 2082 2083 A0000 [label="61.141.32.197"] ; 2084 2085 2086 2087 /* 2088 2089 # Known name->address edges 2090 2091 NA0000 third.guardproof.info. 61.141.32.197 2092 2093 2094 2095 # Known parent->child edges 2096 2097 PC0000 guardproof.info. third.guardproof.info. 2098 2099 */ 2100 2101 Turn that into the following lines by substituting node 2102 identifiers into the relationships: 2103 2104 # Known domain name nodes 2105 2106 N0000 [label="guardproof.info."] ; 2107 2108 N0002 [label="third.guardproof.info."] ; 2109 2110 2111 2112 # Known address nodes 2113 2114 A0000 [label="61.141.32.197"] ; 2115 2116 2117 2118 # PC0000 guardproof.info. third.guardproof.info. 2119 2120 N0000->N0002 ; 2121 2122 2123 2124 # NA0000 third.guardproof.info. 61.141.32.197 2125 2126 N0002->A0000 ; 2127 2128 2129 2130 /* 2131 2132 # Known name->address edges 2133 2134 NA0000 third.guardproof.info. 61.141.32.197 2135 2136 2137 2138 # Known parent->child edges 2139 2140 PC0000 guardproof.info. third.guardproof.info. 2141 2142 */ 2143 2144 Process that with the 'dot' program, and you have your 2145 first network diagram. 2146 2147 In addition to the conventional graphic edges, the 2148 descriptor file includes similar format pair-data that 2149 describes services, zone records (sub-graphs?), 2150 blacklisted addresses, and other things which might be 2151 interesting to include in your graph. This additional 2152 information could be displayed as different node 2153 shapes, colors, line sizes, etc. 2154 2155 The descriptor file can also be read and edited by a 2156 Bash script (of course). You should be able to find 2157 most of the functions required within the 2158 "is_spammer.bash" script. 2159 2160 # End Quickstart. |
To end this section, a review of the basics . . . and more.
Example A-27. Basics Reviewed
1 #!/bin/bash 2 # basics-reviewed.bash 3 4 # File extension == *.bash == specific to Bash 5 6 # Copyright (c) Michael S. Zick, 2003; All rights reserved. 7 # License: Use in any form, for any purpose. 8 # Revision: $ID$ 9 # 10 # Edited for layout by M.C. 11 # (author of the "Advanced Bash Scripting Guide") 12 13 14 # This script tested under Bash versions 2.04, 2.05a and 2.05b. 15 # It may not work with earlier versions. 16 # This demonstration script generates one --intentional-- 17 #+ "command not found" error message. See line 394. 18 19 # The current Bash maintainer, Chet Ramey, has fixed the items noted 20 #+ for an upcoming version of Bash. 21 22 23 24 ###-------------------------------------------### 25 ### Pipe the output of this script to 'more' ### 26 ###+ else it will scroll off the page. ### 27 ### ### 28 ### You may also redirect its output ### 29 ###+ to a file for examination. ### 30 ###-------------------------------------------### 31 32 33 34 # Most of the following points are described at length in 35 #+ the text of the foregoing "Advanced Bash Scripting Guide." 36 # This demonstration script is mostly just a reorganized presentation. 37 # -- msz 38 39 # Variables are not typed unless otherwise specified. 40 41 # Variables are named. Names must contain a non-digit. 42 # File descriptor names (as in, for example: 2>&1) 43 #+ contain ONLY digits. 44 45 # Parameters and Bash array elements are numbered. 46 # (Parameters are very similar to Bash arrays.) 47 48 # A variable name may be undefined (null reference). 49 unset VarNull 50 51 # A variable name may be defined but empty (null contents). 52 VarEmpty='' # Two, adjacent, single quotes. 53 54 # A variable name my be defined and non-empty 55 VarSomething='Literal' 56 57 # A variable may contain: 58 # * A whole number as a signed 32-bit (or larger) integer 59 # * A string 60 # A variable may also be an array. 61 62 # A string may contain embedded blanks and may be treated 63 #+ as if it where a function name with optional arguments. 64 65 # The names of variables and the names of functions 66 #+ are in different namespaces. 67 68 69 # A variable may be defined as a Bash array either explicitly or 70 #+ implicitly by the syntax of the assignment statement. 71 # Explicit: 72 declare -a ArrayVar 73 74 75 76 # The echo command is a built-in. 77 echo $VarSomething 78 79 # The printf command is a built-in. 80 # Translate %s as: String-Format 81 printf %s $VarSomething # No linebreak specified, none output. 82 echo # Default, only linebreak output. 83 84 85 86 87 # The Bash parser word breaks on whitespace. 88 # Whitespace, or the lack of it is significant. 89 # (This holds true in general; there are, of course, exceptions.) 90 91 92 93 94 # Translate the DOLLAR_SIGN character as: Content-Of. 95 96 # Extended-Syntax way of writing Content-Of: 97 echo ${VarSomething} 98 99 # The ${ ... } Extended-Syntax allows more than just the variable 100 #+ name to be specified. 101 # In general, $VarSomething can always be written as: ${VarSomething}. 102 103 # Call this script with arguments to see the following in action. 104 105 106 107 # Outside of double-quotes, the special characters @ and * 108 #+ specify identical behavior. 109 # May be pronounced as: All-Elements-Of. 110 111 # Without specification of a name, they refer to the 112 #+ pre-defined parameter Bash-Array. 113 114 115 116 # Glob-Pattern references 117 echo $* # All parameters to script or function 118 echo ${*} # Same 119 120 # Bash disables filename expansion for Glob-Patterns. 121 # Only character matching is active. 122 123 124 # All-Elements-Of references 125 echo $@ # Same as above 126 echo ${@} # Same as above 127 128 129 130 131 # Within double-quotes, the behavior of Glob-Pattern references 132 #+ depends on the setting of IFS (Input Field Separator). 133 # Within double-quotes, All-Elements-Of references behave the same. 134 135 136 # Specifying only the name of a variable holding a string refers 137 #+ to all elements (characters) of a string. 138 139 140 # To specify an element (character) of a string, 141 #+ the Extended-Syntax reference notation (see below) MAY be used. 142 143 144 145 146 # Specifying only the name of a Bash array references 147 #+ the subscript zero element, 148 #+ NOT the FIRST DEFINED nor the FIRST WITH CONTENTS element. 149 150 # Additional qualification is needed to reference other elements, 151 #+ which means that the reference MUST be written in Extended-Syntax. 152 # The general form is: ${name[subscript]}. 153 154 # The string forms may also be used: ${name:subscript} 155 #+ for Bash-Arrays when referencing the subscript zero element. 156 157 158 # Bash-Arrays are implemented internally as linked lists, 159 #+ not as a fixed area of storage as in some programming languages. 160 161 162 # Characteristics of Bash arrays (Bash-Arrays): 163 # -------------------------------------------- 164 165 # If not otherwise specified, Bash-Array subscripts begin with 166 #+ subscript number zero. Literally: [0] 167 # This is called zero-based indexing. 168 ### 169 # If not otherwise specified, Bash-Arrays are subscript packed 170 #+ (sequential subscripts without subscript gaps). 171 ### 172 # Negative subscripts are not allowed. 173 ### 174 # Elements of a Bash-Array need not all be of the same type. 175 ### 176 # Elements of a Bash-Array may be undefined (null reference). 177 # That is, a Bash-Array my be "subscript sparse." 178 ### 179 # Elements of a Bash-Array may be defined and empty (null contents). 180 ### 181 # Elements of a Bash-Array may contain: 182 # * A whole number as a signed 32-bit (or larger) integer 183 # * A string 184 # * A string formated so that it appears to be a function name 185 # + with optional arguments 186 ### 187 # Defined elements of a Bash-Array may be undefined (unset). 188 # That is, a subscript packed Bash-Array may be changed 189 # + into a subscript sparse Bash-Array. 190 ### 191 # Elements may be added to a Bash-Array by defining an element 192 #+ not previously defined. 193 ### 194 # For these reasons, I have been calling them "Bash-Arrays". 195 # I'll return to the generic term "array" from now on. 196 # -- msz 197 198 199 200 201 # Demo time -- initialize the previously declared ArrayVar as a 202 #+ sparse array. 203 # (The 'unset ... ' is just documentation here.) 204 205 unset ArrayVar[0] # Just for the record 206 ArrayVar[1]=one # Unquoted literal 207 ArrayVar[2]='' # Defined, and empty 208 unset ArrayVar[3] # Just for the record 209 ArrayVar[4]='four' # Quoted literal 210 211 212 213 # Translate the %q format as: Quoted-Respecting-IFS-Rules. 214 echo 215 echo '- - Outside of double-quotes - -' 216 ### 217 printf %q ${ArrayVar[*]} # Glob-Pattern All-Elements-Of 218 echo 219 echo 'echo command:'${ArrayVar[*]} 220 ### 221 printf %q ${ArrayVar[@]} # All-Elements-Of 222 echo 223 echo 'echo command:'${ArrayVar[@]} 224 225 # The use of double-quotes may be translated as: Enable-Substitution. 226 227 # There are five cases recognized for the IFS setting. 228 229 echo 230 echo '- - Within double-quotes - Default IFS of space-tab-newline - -' 231 IFS=$'\x20'$'\x09'$'\x0A' # These three bytes, 232 #+ in exactly this order. 233 234 235 printf %q "${ArrayVar[*]}" # Glob-Pattern All-Elements-Of 236 echo 237 echo 'echo command:'"${ArrayVar[*]}" 238 ### 239 printf %q "${ArrayVar[@]}" # All-Elements-Of 240 echo 241 echo 'echo command:'"${ArrayVar[@]}" 242 243 244 echo 245 echo '- - Within double-quotes - First character of IFS is ^ - -' 246 # Any printing, non-whitespace character should do the same. 247 IFS='^'$IFS # ^ + space tab newline 248 ### 249 printf %q "${ArrayVar[*]}" # Glob-Pattern All-Elements-Of 250 echo 251 echo 'echo command:'"${ArrayVar[*]}" 252 ### 253 printf %q "${ArrayVar[@]}" # All-Elements-Of 254 echo 255 echo 'echo command:'"${ArrayVar[@]}" 256 257 258 echo 259 echo '- - Within double-quotes - Without whitespace in IFS - -' 260 IFS='^:%!' 261 ### 262 printf %q "${ArrayVar[*]}" # Glob-Pattern All-Elements-Of 263 echo 264 echo 'echo command:'"${ArrayVar[*]}" 265 ### 266 printf %q "${ArrayVar[@]}" # All-Elements-Of 267 echo 268 echo 'echo command:'"${ArrayVar[@]}" 269 270 271 echo 272 echo '- - Within double-quotes - IFS set and empty - -' 273 IFS='' 274 ### 275 printf %q "${ArrayVar[*]}" # Glob-Pattern All-Elements-Of 276 echo 277 echo 'echo command:'"${ArrayVar[*]}" 278 ### 279 printf %q "${ArrayVar[@]}" # All-Elements-Of 280 echo 281 echo 'echo command:'"${ArrayVar[@]}" 282 283 284 echo 285 echo '- - Within double-quotes - IFS undefined - -' 286 unset IFS 287 ### 288 printf %q "${ArrayVar[*]}" # Glob-Pattern All-Elements-Of 289 echo 290 echo 'echo command:'"${ArrayVar[*]}" 291 ### 292 printf %q "${ArrayVar[@]}" # All-Elements-Of 293 echo 294 echo 'echo command:'"${ArrayVar[@]}" 295 296 297 # Put IFS back to the default. 298 # Default is exactly these three bytes. 299 IFS=$'\x20'$'\x09'$'\x0A' # In exactly this order. 300 301 # Interpretation of the above outputs: 302 # A Glob-Pattern is I/O; the setting of IFS matters. 303 ### 304 # An All-Elements-Of does not consider IFS settings. 305 ### 306 # Note the different output using the echo command and the 307 #+ quoted format operator of the printf command. 308 309 310 # Recall: 311 # Parameters are similar to arrays and have the similar behaviors. 312 ### 313 # The above examples demonstrate the possible variations. 314 # To retain the shape of a sparse array, additional script 315 #+ programming is required. 316 ### 317 # The source code of Bash has a routine to output the 318 #+ [subscript]=value array assignment format. 319 # As of version 2.05b, that routine is not used, 320 #+ but that might change in future releases. 321 322 323 324 # The length of a string, measured in non-null elements (characters): 325 echo 326 echo '- - Non-quoted references - -' 327 echo 'Non-Null character count: '${#VarSomething}' characters.' 328 329 # test='Lit'$'\x00''eral' # $'\x00' is a null character. 330 # echo ${#test} # See that? 331 332 333 334 # The length of an array, measured in defined elements, 335 #+ including null content elements. 336 echo 337 echo 'Defined content count: '${#ArrayVar[@]}' elements.' 338 # That is NOT the maximum subscript (4). 339 # That is NOT the range of the subscripts (1 . . 4 inclusive). 340 # It IS the length of the linked list. 341 ### 342 # Both the maximum subscript and the range of the subscripts may 343 #+ be found with additional script programming. 344 345 # The length of a string, measured in non-null elements (characters): 346 echo 347 echo '- - Quoted, Glob-Pattern references - -' 348 echo 'Non-Null character count: '"${#VarSomething}"' characters.' 349 350 # The length of an array, measured in defined elements, 351 #+ including null-content elements. 352 echo 353 echo 'Defined element count: '"${#ArrayVar[*]}"' elements.' 354 355 # Interpretation: Substitution does not effect the ${# ... } operation. 356 # Suggestion: 357 # Always use the All-Elements-Of character 358 #+ if that is what is intended (independence from IFS). 359 360 361 362 # Define a simple function. 363 # I include an underscore in the name 364 #+ to make it distinctive in the examples below. 365 ### 366 # Bash separates variable names and function names 367 #+ in different namespaces. 368 # The Mark-One eyeball isn't that advanced. 369 ### 370 _simple() { 371 echo -n 'SimpleFunc'$@ # Newlines are swallowed in 372 } #+ result returned in any case. 373 374 375 # The ( ... ) notation invokes a command or function. 376 # The $( ... ) notation is pronounced: Result-Of. 377 378 379 # Invoke the function _simple 380 echo 381 echo '- - Output of function _simple - -' 382 _simple # Try passing arguments. 383 echo 384 # or 385 (_simple) # Try passing arguments. 386 echo 387 388 echo '- Is there a variable of that name? -' 389 echo $_simple not defined # No variable by that name. 390 391 # Invoke the result of function _simple (Error msg intended) 392 393 ### 394 $(_simple) # Gives an error message: 395 # line 394: SimpleFunc: command not found 396 # --------------------------------------- 397 398 echo 399 ### 400 401 # The first word of the result of function _simple 402 #+ is neither a valid Bash command nor the name of a defined function. 403 ### 404 # This demonstrates that the output of _simple is subject to evaluation. 405 ### 406 # Interpretation: 407 # A function can be used to generate in-line Bash commands. 408 409 410 # A simple function where the first word of result IS a bash command: 411 ### 412 _print() { 413 echo -n 'printf %q '$@ 414 } 415 416 echo '- - Outputs of function _print - -' 417 _print parm1 parm2 # An Output NOT A Command. 418 echo 419 420 $(_print parm1 parm2) # Executes: printf %q parm1 parm2 421 # See above IFS examples for the 422 #+ various possibilities. 423 echo 424 425 $(_print $VarSomething) # The predictable result. 426 echo 427 428 429 430 # Function variables 431 # ------------------ 432 433 echo 434 echo '- - Function variables - -' 435 # A variable may represent a signed integer, a string or an array. 436 # A string may be used like a function name with optional arguments. 437 438 # set -vx # Enable if desired 439 declare -f funcVar #+ in namespace of functions 440 441 funcVar=_print # Contains name of function. 442 $funcVar parm1 # Same as _print at this point. 443 echo 444 445 funcVar=$(_print ) # Contains result of function. 446 $funcVar # No input, No output. 447 $funcVar $VarSomething # The predictable result. 448 echo 449 450 funcVar=$(_print $VarSomething) # $VarSomething replaced HERE. 451 $funcVar # The expansion is part of the 452 echo #+ variable contents. 453 454 funcVar="$(_print $VarSomething)" # $VarSomething replaced HERE. 455 $funcVar # The expansion is part of the 456 echo #+ variable contents. 457 458 # The difference between the unquoted and the double-quoted versions 459 #+ above can be seen in the "protect_literal.sh" example. 460 # The first case above is processed as two, unquoted, Bash-Words. 461 # The second case above is processed as one, quoted, Bash-Word. 462 463 464 465 466 # Delayed replacement 467 # ------------------- 468 469 echo 470 echo '- - Delayed replacement - -' 471 funcVar="$(_print '$VarSomething')" # No replacement, single Bash-Word. 472 eval $funcVar # $VarSomething replaced HERE. 473 echo 474 475 VarSomething='NewThing' 476 eval $funcVar # $VarSomething replaced HERE. 477 echo 478 479 # Restore the original setting trashed above. 480 VarSomething=Literal 481 482 # There are a pair of functions demonstrated in the 483 #+ "protect_literal.sh" and "unprotect_literal.sh" examples. 484 # These are general purpose functions for delayed replacement literals 485 #+ containing variables. 486 487 488 489 490 491 # REVIEW: 492 # ------ 493 494 # A string can be considered a Classic-Array of elements (characters). 495 # A string operation applies to all elements (characters) of the string 496 #+ (in concept, anyway). 497 ### 498 # The notation: ${array_name[@]} represents all elements of the 499 #+ Bash-Array: array_name. 500 ### 501 # The Extended-Syntax string operations can be applied to all 502 #+ elements of an array. 503 ### 504 # This may be thought of as a For-Each operation on a vector of strings. 505 ### 506 # Parameters are similar to an array. 507 # The initialization of a parameter array for a script 508 #+ and a parameter array for a function only differ 509 #+ in the initialization of ${0}, which never changes its setting. 510 ### 511 # Subscript zero of the script's parameter array contains 512 #+ the name of the script. 513 ### 514 # Subscript zero of a function's parameter array DOES NOT contain 515 #+ the name of the function. 516 # The name of the current function is accessed by the $FUNCNAME variable. 517 ### 518 # A quick, review list follows (quick, not short). 519 520 echo 521 echo '- - Test (but not change) - -' 522 echo '- null reference -' 523 echo -n ${VarNull-'NotSet'}' ' # NotSet 524 echo ${VarNull} # NewLine only 525 echo -n ${VarNull:-'NotSet'}' ' # NotSet 526 echo ${VarNull} # Newline only 527 528 echo '- null contents -' 529 echo -n ${VarEmpty-'Empty'}' ' # Only the space 530 echo ${VarEmpty} # Newline only 531 echo -n ${VarEmpty:-'Empty'}' ' # Empty 532 echo ${VarEmpty} # Newline only 533 534 echo '- contents -' 535 echo ${VarSomething-'Content'} # Literal 536 echo ${VarSomething:-'Content'} # Literal 537 538 echo '- Sparse Array -' 539 echo ${ArrayVar[@]-'not set'} 540 541 # ASCII-Art time 542 # State Y==yes, N==no 543 # - :- 544 # Unset Y Y ${# ... } == 0 545 # Empty N Y ${# ... } == 0 546 # Contents N N ${# ... } > 0 547 548 # Either the first and/or the second part of the tests 549 #+ may be a command or a function invocation string. 550 echo 551 echo '- - Test 1 for undefined - -' 552 declare -i t 553 _decT() { 554 t=$t-1 555 } 556 557 # Null reference, set: t == -1 558 t=${#VarNull} # Results in zero. 559 ${VarNull- _decT } # Function executes, t now -1. 560 echo $t 561 562 # Null contents, set: t == 0 563 t=${#VarEmpty} # Results in zero. 564 ${VarEmpty- _decT } # _decT function NOT executed. 565 echo $t 566 567 # Contents, set: t == number of non-null characters 568 VarSomething='_simple' # Set to valid function name. 569 t=${#VarSomething} # non-zero length 570 ${VarSomething- _decT } # Function _simple executed. 571 echo $t # Note the Append-To action. 572 573 # Exercise: clean up that example. 574 unset t 575 unset _decT 576 VarSomething=Literal 577 578 echo 579 echo '- - Test and Change - -' 580 echo '- Assignment if null reference -' 581 echo -n ${VarNull='NotSet'}' ' # NotSet NotSet 582 echo ${VarNull} 583 unset VarNull 584 585 echo '- Assignment if null reference -' 586 echo -n ${VarNull:='NotSet'}' ' # NotSet NotSet 587 echo ${VarNull} 588 unset VarNull 589 590 echo '- No assignment if null contents -' 591 echo -n ${VarEmpty='Empty'}' ' # Space only 592 echo ${VarEmpty} 593 VarEmpty='' 594 595 echo '- Assignment if null contents -' 596 echo -n ${VarEmpty:='Empty'}' ' # Empty Empty 597 echo ${VarEmpty} 598 VarEmpty='' 599 600 echo '- No change if already has contents -' 601 echo ${VarSomething='Content'} # Literal 602 echo ${VarSomething:='Content'} # Literal 603 604 605 # "Subscript sparse" Bash-Arrays 606 ### 607 # Bash-Arrays are subscript packed, beginning with 608 #+ subscript zero unless otherwise specified. 609 ### 610 # The initialization of ArrayVar was one way 611 #+ to "otherwise specify". Here is the other way: 612 ### 613 echo 614 declare -a ArraySparse 615 ArraySparse=( [1]=one [2]='' [4]='four' ) 616 # [0]=null reference, [2]=null content, [3]=null reference 617 618 echo '- - Array-Sparse List - -' 619 # Within double-quotes, default IFS, Glob-Pattern 620 621 IFS=$'\x20'$'\x09'$'\x0A' 622 printf %q "${ArraySparse[*]}" 623 echo 624 625 # Note that the output does not distinguish between "null content" 626 #+ and "null reference". 627 # Both print as escaped whitespace. 628 ### 629 # Note also that the output does NOT contain escaped whitespace 630 #+ for the "null reference(s)" prior to the first defined element. 631 ### 632 # This behavior of 2.04, 2.05a and 2.05b has been reported 633 #+ and may change in a future version of Bash. 634 635 # To output a sparse array and maintain the [subscript]=value 636 #+ relationship without change requires a bit of programming. 637 # One possible code fragment: 638 ### 639 # local l=${#ArraySparse[@]} # Count of defined elements 640 # local f=0 # Count of found subscripts 641 # local i=0 # Subscript to test 642 ( # Anonymous in-line function 643 for (( l=${#ArraySparse[@]}, f = 0, i = 0 ; f < l ; i++ )) 644 do 645 # 'if defined then...' 646 ${ArraySparse[$i]+ eval echo '\ ['$i']='${ArraySparse[$i]} ; (( f++ )) } 647 done 648 ) 649 650 # The reader coming upon the above code fragment cold 651 #+ might want to review "command lists" and "multiple commands on a line" 652 #+ in the text of the foregoing "Advanced Bash Scripting Guide." 653 ### 654 # Note: 655 # The "read -a array_name" version of the "read" command 656 #+ begins filling array_name at subscript zero. 657 # ArraySparse does not define a value at subscript zero. 658 ### 659 # The user needing to read/write a sparse array to either 660 #+ external storage or a communications socket must invent 661 #+ a read/write code pair suitable for their purpose. 662 ### 663 # Exercise: clean it up. 664 665 unset ArraySparse 666 667 echo 668 echo '- - Conditional alternate (But not change)- -' 669 echo '- No alternate if null reference -' 670 echo -n ${VarNull+'NotSet'}' ' 671 echo ${VarNull} 672 unset VarNull 673 674 echo '- No alternate if null reference -' 675 echo -n ${VarNull:+'NotSet'}' ' 676 echo ${VarNull} 677 unset VarNull 678 679 echo '- Alternate if null contents -' 680 echo -n ${VarEmpty+'Empty'}' ' # Empty 681 echo ${VarEmpty} 682 VarEmpty='' 683 684 echo '- No alternate if null contents -' 685 echo -n ${VarEmpty:+'Empty'}' ' # Space only 686 echo ${VarEmpty} 687 VarEmpty='' 688 689 echo '- Alternate if already has contents -' 690 691 # Alternate literal 692 echo -n ${VarSomething+'Content'}' ' # Content Literal 693 echo ${VarSomething} 694 695 # Invoke function 696 echo -n ${VarSomething:+ $(_simple) }' ' # SimpleFunc Literal 697 echo ${VarSomething} 698 echo 699 700 echo '- - Sparse Array - -' 701 echo ${ArrayVar[@]+'Empty'} # An array of 'Empty'(ies) 702 echo 703 704 echo '- - Test 2 for undefined - -' 705 706 declare -i t 707 _incT() { 708 t=$t+1 709 } 710 711 # Note: 712 # This is the same test used in the sparse array 713 #+ listing code fragment. 714 715 # Null reference, set: t == -1 716 t=${#VarNull}-1 # Results in minus-one. 717 ${VarNull+ _incT } # Does not execute. 718 echo $t' Null reference' 719 720 # Null contents, set: t == 0 721 t=${#VarEmpty}-1 # Results in minus-one. 722 ${VarEmpty+ _incT } # Executes. 723 echo $t' Null content' 724 725 # Contents, set: t == (number of non-null characters) 726 t=${#VarSomething}-1 # non-null length minus-one 727 ${VarSomething+ _incT } # Executes. 728 echo $t' Contents' 729 730 # Exercise: clean up that example. 731 unset t 732 unset _incT 733 734 # ${name?err_msg} ${name:?err_msg} 735 # These follow the same rules but always exit afterwards 736 #+ if an action is specified following the question mark. 737 # The action following the question mark may be a literal 738 #+ or a function result. 739 ### 740 # ${name?} ${name:?} are test-only, the return can be tested. 741 742 743 744 745 # Element operations 746 # ------------------ 747 748 echo 749 echo '- - Trailing sub-element selection - -' 750 751 # Strings, Arrays and Positional parameters 752 753 # Call this script with multiple arguments 754 #+ to see the parameter selections. 755 756 echo '- All -' 757 echo ${VarSomething:0} # all non-null characters 758 echo ${ArrayVar[@]:0} # all elements with content 759 echo ${@:0} # all parameters with content; 760 # ignoring parameter[0] 761 762 echo 763 echo '- All after -' 764 echo ${VarSomething:1} # all non-null after character[0] 765 echo ${ArrayVar[@]:1} # all after element[0] with content 766 echo ${@:2} # all after param[1] with content 767 768 echo 769 echo '- Range after -' 770 echo ${VarSomething:4:3} # ral 771 # Three characters after 772 # character[3] 773 774 echo '- Sparse array gotch -' 775 echo ${ArrayVar[@]:1:2} # four - The only element with content. 776 # Two elements after (if that many exist). 777 # the FIRST WITH CONTENTS 778 #+ (the FIRST WITH CONTENTS is being 779 #+ considered as if it 780 #+ were subscript zero). 781 # Executed as if Bash considers ONLY array elements with CONTENT 782 # printf %q "${ArrayVar[@]:0:3}" # Try this one 783 784 # In versions 2.04, 2.05a and 2.05b, 785 #+ Bash does not handle sparse arrays as expected using this notation. 786 # 787 # The current Bash maintainer, Chet Ramey, has corrected this 788 #+ for an upcoming version of Bash. 789 790 791 echo '- Non-sparse array -' 792 echo ${@:2:2} # Two parameters following parameter[1] 793 794 # New victims for string vector examples: 795 stringZ=abcABC123ABCabc 796 arrayZ=( abcabc ABCABC 123123 ABCABC abcabc ) 797 sparseZ=( [1]='abcabc' [3]='ABCABC' [4]='' [5]='123123' ) 798 799 echo 800 echo ' - - Victim string - -'$stringZ'- - ' 801 echo ' - - Victim array - -'${arrayZ[@]}'- - ' 802 echo ' - - Sparse array - -'${sparseZ[@]}'- - ' 803 echo ' - [0]==null ref, [2]==null ref, [4]==null content - ' 804 echo ' - [1]=abcabc [3]=ABCABC [5]=123123 - ' 805 echo ' - non-null-reference count: '${#sparseZ[@]}' elements' 806 807 echo 808 echo '- - Prefix sub-element removal - -' 809 echo '- - Glob-Pattern match must include the first character. - -' 810 echo '- - Glob-Pattern may be a literal or a function result. - -' 811 echo 812 813 814 # Function returning a simple, Literal, Glob-Pattern 815 _abc() { 816 echo -n 'abc' 817 } 818 819 echo '- Shortest prefix -' 820 echo ${stringZ#123} # Unchanged (not a prefix). 821 echo ${stringZ#$(_abc)} # ABC123ABCabc 822 echo ${arrayZ[@]#abc} # Applied to each element. 823 824 # Fixed by Chet Ramey for an upcoming version of Bash. 825 # echo ${sparseZ[@]#abc} # Version-2.05b core dumps. 826 827 # The -it would be nice- First-Subscript-Of 828 # echo ${#sparseZ[@]#*} # This is NOT valid Bash. 829 830 echo 831 echo '- Longest prefix -' 832 echo ${stringZ##1*3} # Unchanged (not a prefix) 833 echo ${stringZ##a*C} # abc 834 echo ${arrayZ[@]##a*c} # ABCABC 123123 ABCABC 835 836 # Fixed by Chet Ramey for an upcoming version of Bash 837 # echo ${sparseZ[@]##a*c} # Version-2.05b core dumps. 838 839 echo 840 echo '- - Suffix sub-element removal - -' 841 echo '- - Glob-Pattern match must include the last character. - -' 842 echo '- - Glob-Pattern may be a literal or a function result. - -' 843 echo 844 echo '- Shortest suffix -' 845 echo ${stringZ%1*3} # Unchanged (not a suffix). 846 echo ${stringZ%$(_abc)} # abcABC123ABC 847 echo ${arrayZ[@]%abc} # Applied to each element. 848 849 # Fixed by Chet Ramey for an upcoming version of Bash. 850 # echo ${sparseZ[@]%abc} # Version-2.05b core dumps. 851 852 # The -it would be nice- Last-Subscript-Of 853 # echo ${#sparseZ[@]%*} # This is NOT valid Bash. 854 855 echo 856 echo '- Longest suffix -' 857 echo ${stringZ%%1*3} # Unchanged (not a suffix) 858 echo ${stringZ%%b*c} # a 859 echo ${arrayZ[@]%%b*c} # a ABCABC 123123 ABCABC a 860 861 # Fixed by Chet Ramey for an upcoming version of Bash. 862 # echo ${sparseZ[@]%%b*c} # Version-2.05b core dumps. 863 864 echo 865 echo '- - Sub-element replacement - -' 866 echo '- - Sub-element at any location in string. - -' 867 echo '- - First specification is a Glob-Pattern - -' 868 echo '- - Glob-Pattern may be a literal or Glob-Pattern function result. - -' 869 echo '- - Second specification may be a literal or function result. - -' 870 echo '- - Second specification may be unspecified. Pronounce that' 871 echo ' as: Replace-With-Nothing (Delete) - -' 872 echo 873 874 875 876 # Function returning a simple, Literal, Glob-Pattern 877 _123() { 878 echo -n '123' 879 } 880 881 echo '- Replace first occurrence -' 882 echo ${stringZ/$(_123)/999} # Changed (123 is a component). 883 echo ${stringZ/ABC/xyz} # xyzABC123ABCabc 884 echo ${arrayZ[@]/ABC/xyz} # Applied to each element. 885 echo ${sparseZ[@]/ABC/xyz} # Works as expected. 886 887 echo 888 echo '- Delete first occurrence -' 889 echo ${stringZ/$(_123)/} 890 echo ${stringZ/ABC/} 891 echo ${arrayZ[@]/ABC/} 892 echo ${sparseZ[@]/ABC/} 893 894 # The replacement need not be a literal, 895 #+ since the result of a function invocation is allowed. 896 # This is general to all forms of replacement. 897 echo 898 echo '- Replace first occurrence with Result-Of -' 899 echo ${stringZ/$(_123)/$(_simple)} # Works as expected. 900 echo ${arrayZ[@]/ca/$(_simple)} # Applied to each element. 901 echo ${sparseZ[@]/ca/$(_simple)} # Works as expected. 902 903 echo 904 echo '- Replace all occurrences -' 905 echo ${stringZ//[b2]/X} # X-out b's and 2's 906 echo ${stringZ//abc/xyz} # xyzABC123ABCxyz 907 echo ${arrayZ[@]//abc/xyz} # Applied to each element. 908 echo ${sparseZ[@]//abc/xyz} # Works as expected. 909 910 echo 911 echo '- Delete all occurrences -' 912 echo ${stringZ//[b2]/} 913 echo ${stringZ//abc/} 914 echo ${arrayZ[@]//abc/} 915 echo ${sparseZ[@]//abc/} 916 917 echo 918 echo '- - Prefix sub-element replacement - -' 919 echo '- - Match must include the first character. - -' 920 echo 921 922 echo '- Replace prefix occurrences -' 923 echo ${stringZ/#[b2]/X} # Unchanged (neither is a prefix). 924 echo ${stringZ/#$(_abc)/XYZ} # XYZABC123ABCabc 925 echo ${arrayZ[@]/#abc/XYZ} # Applied to each element. 926 echo ${sparseZ[@]/#abc/XYZ} # Works as expected. 927 928 echo 929 echo '- Delete prefix occurrences -' 930 echo ${stringZ/#[b2]/} 931 echo ${stringZ/#$(_abc)/} 932 echo ${arrayZ[@]/#abc/} 933 echo ${sparseZ[@]/#abc/} 934 935 echo 936 echo '- - Suffix sub-element replacement - -' 937 echo '- - Match must include the last character. - -' 938 echo 939 940 echo '- Replace suffix occurrences -' 941 echo ${stringZ/%[b2]/X} # Unchanged (neither is a suffix). 942 echo ${stringZ/%$(_abc)/XYZ} # abcABC123ABCXYZ 943 echo ${arrayZ[@]/%abc/XYZ} # Applied to each element. 944 echo ${sparseZ[@]/%abc/XYZ} # Works as expected. 945 946 echo 947 echo '- Delete suffix occurrences -' 948 echo ${stringZ/%[b2]/} 949 echo ${stringZ/%$(_abc)/} 950 echo ${arrayZ[@]/%abc/} 951 echo ${sparseZ[@]/%abc/} 952 953 echo 954 echo '- - Special cases of null Glob-Pattern - -' 955 echo 956 957 echo '- Prefix all -' 958 # null substring pattern means 'prefix' 959 echo ${stringZ/#/NEW} # NEWabcABC123ABCabc 960 echo ${arrayZ[@]/#/NEW} # Applied to each element. 961 echo ${sparseZ[@]/#/NEW} # Applied to null-content also. 962 # That seems reasonable. 963 964 echo 965 echo '- Suffix all -' 966 # null substring pattern means 'suffix' 967 echo ${stringZ/%/NEW} # abcABC123ABCabcNEW 968 echo ${arrayZ[@]/%/NEW} # Applied to each element. 969 echo ${sparseZ[@]/%/NEW} # Applied to null-content also. 970 # That seems reasonable. 971 972 echo 973 echo '- - Special case For-Each Glob-Pattern - -' 974 echo '- - - - This is a nice-to-have dream - - - -' 975 echo 976 977 _GenFunc() { 978 echo -n ${0} # Illustration only. 979 # Actually, that would be an arbitrary computation. 980 } 981 982 # All occurrences, matching the AnyThing pattern. 983 # Currently //*/ does not match null-content nor null-reference. 984 # /#/ and /%/ does match null-content but not null-reference. 985 echo ${sparseZ[@]//*/$(_GenFunc)} 986 987 988 # A possible syntax would be to make 989 #+ the parameter notation used within this construct mean: 990 # ${1} - The full element 991 # ${2} - The prefix, if any, to the matched sub-element 992 # ${3} - The matched sub-element 993 # ${4} - The suffix, if any, to the matched sub-element 994 # 995 # echo ${sparseZ[@]//*/$(_GenFunc ${3})} # Same as ${1} here. 996 # Perhaps it will be implemented in a future version of Bash. 997 998 999 exit 0 |