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