These scripts, while not fitting into the text of this document, do illustrate some interesting shell programming techniques. They are useful, too. Have fun analyzing and running them.
Example A-1. mailformat: Formatting an e-mail message
1 #!/bin/bash 2 # mail-format.sh (ver. 1.1): Format e-mail messages. 3 4 # Gets rid of carets, tabs, and also folds excessively long lines. 5 6 # ================================================================= 7 # Standard Check for Script Argument(s) 8 ARGS=1 9 E_BADARGS=65 10 E_NOFILE=66 11 12 if [ $# -ne $ARGS ] # Correct number of arguments passed to script? 13 then 14 echo "Usage: `basename $0` filename" 15 exit $E_BADARGS 16 fi 17 18 if [ -f "$1" ] # Check if file exists. 19 then 20 file_name=$1 21 else 22 echo "File \"$1\" does not exist." 23 exit $E_NOFILE 24 fi 25 # ================================================================= 26 27 MAXWIDTH=70 # Width to fold excessively long lines to. 28 29 # --------------------------------- 30 # A variable can hold a sed script. 31 sedscript='s/^>// 32 s/^ *>// 33 s/^ *// 34 s/ *//' 35 # --------------------------------- 36 37 # Delete carets and tabs at beginning of lines, 38 #+ then fold lines to $MAXWIDTH characters. 39 sed "$sedscript" $1 | fold -s --width=$MAXWIDTH 40 # -s option to "fold" 41 #+ breaks lines at whitespace, if possible. 42 43 44 # This script was inspired by an article in a well-known trade journal 45 #+ extolling a 164K MS Windows utility with similar functionality. 46 # 47 # An nice set of text processing utilities and an efficient 48 #+ scripting language provide an alternative to bloated executables. 49 50 exit 0 |
Example A-2. rn: A simple-minded file rename utility
This script is a modification of Example 12-19.
1 #! /bin/bash 2 # 3 # Very simpleminded filename "rename" utility (based on "lowercase.sh"). 4 # 5 # The "ren" utility, by Vladimir Lanin (lanin@csd2.nyu.edu), 6 #+ does a much better job of this. 7 8 9 ARGS=2 10 E_BADARGS=65 11 ONE=1 # For getting singular/plural right (see below). 12 13 if [ $# -ne "$ARGS" ] 14 then 15 echo "Usage: `basename $0` old-pattern new-pattern" 16 # As in "rn gif jpg", which renames all gif files in working directory to jpg. 17 exit $E_BADARGS 18 fi 19 20 number=0 # Keeps track of how many files actually renamed. 21 22 23 for filename in *$1* #Traverse all matching files in directory. 24 do 25 if [ -f "$filename" ] # If finds match... 26 then 27 fname=`basename $filename` # Strip off path. 28 n=`echo $fname | sed -e "s/$1/$2/"` # Substitute new for old in filename. 29 mv $fname $n # Rename. 30 let "number += 1" 31 fi 32 done 33 34 if [ "$number" -eq "$ONE" ] # For correct grammar. 35 then 36 echo "$number file renamed." 37 else 38 echo "$number files renamed." 39 fi 40 41 exit 0 42 43 44 # Exercises: 45 # --------- 46 # What type of files will this not work on? 47 # How can this be fixed? 48 # 49 # Rewrite this script to process all the files in a directory 50 #+ containing spaces in their names, and to rename them, 51 #+ substituting an underscore for each space. |
Example A-3. blank-rename: renames filenames containing blanks
This is an even simpler-minded version of previous script.
1 #! /bin/bash 2 # blank-rename.sh 3 # 4 # Substitutes underscores for blanks in all the filenames in a directory. 5 6 ONE=1 # For getting singular/plural right (see below). 7 number=0 # Keeps track of how many files actually renamed. 8 FOUND=0 # Successful return value. 9 10 for filename in * #Traverse all files in directory. 11 do 12 echo "$filename" | grep -q " " # Check whether filename 13 if [ $? -eq $FOUND ] #+ contains space(s). 14 then 15 fname=$filename # Strip off path. 16 n=`echo $fname | sed -e "s/ /_/g"` # Substitute underscore for blank. 17 mv "$fname" "$n" # Do the actual renaming. 18 let "number += 1" 19 fi 20 done 21 22 if [ "$number" -eq "$ONE" ] # For correct grammar. 23 then 24 echo "$number file renamed." 25 else 26 echo "$number files renamed." 27 fi 28 29 exit 0 |
Example A-4. encryptedpw: Uploading to an ftp site, using a locally encrypted password
1 #!/bin/bash 2 3 # Example "ex72.sh" modified to use encrypted password. 4 5 # Note that this is still rather insecure, 6 #+ since the decrypted password is sent in the clear. 7 # Use something like "ssh" if this is a concern. 8 9 E_BADARGS=65 10 11 if [ -z "$1" ] 12 then 13 echo "Usage: `basename $0` filename" 14 exit $E_BADARGS 15 fi 16 17 Username=bozo # Change to suit. 18 pword=/home/bozo/secret/password_encrypted.file 19 # File containing encrypted password. 20 21 Filename=`basename $1` # Strips pathname out of file name. 22 23 Server="XXX" 24 Directory="YYY" # Change above to actual server name & directory. 25 26 27 Password=`cruft <$pword` # Decrypt password. 28 # Uses the author's own "cruft" file encryption package, 29 #+ based on the classic "onetime pad" algorithm, 30 #+ and obtainable from: 31 #+ Primary-site: ftp://ibiblio.org/pub/Linux/utils/file 32 #+ cruft-0.2.tar.gz [16k] 33 34 35 ftp -n $Server <<End-Of-Session 36 user $Username $Password 37 binary 38 bell 39 cd $Directory 40 put $Filename 41 bye 42 End-Of-Session 43 # -n option to "ftp" disables auto-logon. 44 # Note that "bell" rings 'bell' after each file transfer. 45 46 exit 0 |
Example A-5. copy-cd: Copying a data CD
1 #!/bin/bash 2 # copy-cd.sh: copying a data CD 3 4 CDROM=/dev/cdrom # CD ROM device 5 OF=/home/bozo/projects/cdimage.iso # output file 6 # /xxxx/xxxxxxx/ Change to suit your system. 7 BLOCKSIZE=2048 8 SPEED=2 # May use higher speed if supported. 9 DEVICE=cdrom 10 # DEVICE="0,0" on older versions of cdrecord. 11 12 echo; echo "Insert source CD, but do *not* mount it." 13 echo "Press ENTER when ready. " 14 read ready # Wait for input, $ready not used. 15 16 echo; echo "Copying the source CD to $OF." 17 echo "This may take a while. Please be patient." 18 19 dd if=$CDROM of=$OF bs=$BLOCKSIZE # Raw device copy. 20 21 22 echo; echo "Remove data CD." 23 echo "Insert blank CDR." 24 echo "Press ENTER when ready. " 25 read ready # Wait for input, $ready not used. 26 27 echo "Copying $OF to CDR." 28 29 cdrecord -v -isosize speed=$SPEED dev=$DEVICE $OF 30 # Uses Joerg Schilling's "cdrecord" package (see its docs). 31 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html 32 33 34 echo; echo "Done copying $OF to CDR on device $CDROM." 35 36 echo "Do you want to erase the image file (y/n)? " # Probably a huge file. 37 read answer 38 39 case "$answer" in 40 [yY]) rm -f $OF 41 echo "$OF erased." 42 ;; 43 *) echo "$OF not erased.";; 44 esac 45 46 echo 47 48 # Exercise: 49 # Change the above "case" statement to also accept "yes" and "Yes" as input. 50 51 exit 0 |
Example A-6. Collatz series
1 #!/bin/bash
2 # collatz.sh
3
4 # The notorious "hailstone" or Collatz series.
5 # -------------------------------------------
6 # 1) Get the integer "seed" from the command line.
7 # 2) NUMBER <--- seed
8 # 3) Print NUMBER.
9 # 4) If NUMBER is even, divide by 2, or
10 # 5)+ if odd, multiply by 3 and add 1.
11 # 6) NUMBER <--- result
12 # 7) Loop back to step 3 (for specified number of iterations).
13 #
14 # The theory is that every sequence,
15 #+ no matter how large the initial value,
16 #+ eventually settles down to repeating "4,2,1..." cycles,
17 #+ even after fluctuating through a wide range of values.
18 #
19 # This is an instance of an "iterate",
20 #+ an operation that feeds its output back into the input.
21 # Sometimes the result is a "chaotic" series.
22
23
24 MAX_ITERATIONS=200
25 # For large seed numbers (>32000), increase MAX_ITERATIONS.
26
27 h=${1:-$$} # Seed
28 # Use $PID as seed,
29 #+ if not specified as command-line arg.
30
31 echo
32 echo "C($h) --- $MAX_ITERATIONS Iterations"
33 echo
34
35 for ((i=1; i<=MAX_ITERATIONS; i++))
36 do
37
38 echo -n "$h "
39 # ^^^^^
40 # tab
41
42 let "remainder = h % 2"
43 if [ "$remainder" -eq 0 ] # Even?
44 then
45 let "h /= 2" # Divide by 2.
46 else
47 let "h = h*3 + 1" # Multiply by 3 and add 1.
48 fi
49
50
51 COLUMNS=10 # Output 10 values per line.
52 let "line_break = i % $COLUMNS"
53 if [ "$line_break" -eq 0 ]
54 then
55 echo
56 fi
57
58 done
59
60 echo
61
62 # For more information on this mathematical function,
63 #+ see "Computers, Pattern, Chaos, and Beauty", by Pickover, p. 185 ff.,
64 #+ as listed in the bibliography.
65
66 exit 0 |
Example A-7. days-between: Calculate number of days between two dates
1 #!/bin/bash
2 # days-between.sh: Number of days between two dates.
3 # Usage: ./days-between.sh [M]M/[D]D/YYYY [M]M/[D]D/YYYY
4 #
5 # Note: Script modified to account for changes in Bash 2.05b
6 #+ that closed the loophole permitting large negative
7 #+ integer return values.
8
9 ARGS=2 # Two command line parameters expected.
10 E_PARAM_ERR=65 # Param error.
11
12 REFYR=1600 # Reference year.
13 CENTURY=100
14 DIY=365
15 ADJ_DIY=367 # Adjusted for leap year + fraction.
16 MIY=12
17 DIM=31
18 LEAPCYCLE=4
19
20 MAXRETVAL=255 # Largest permissable
21 #+ positive return value from a function.
22
23 diff= # Declare global variable for date difference.
24 value= # Declare global variable for absolute value.
25 day= # Declare globals for day, month, year.
26 month=
27 year=
28
29
30 Param_Error () # Command line parameters wrong.
31 {
32 echo "Usage: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY"
33 echo " (date must be after 1/3/1600)"
34 exit $E_PARAM_ERR
35 }
36
37
38 Parse_Date () # Parse date from command line params.
39 {
40 month=${1%%/**}
41 dm=${1%/**} # Day and month.
42 day=${dm#*/}
43 let "year = `basename $1`" # Not a filename, but works just the same.
44 }
45
46
47 check_date () # Checks for invalid date(s) passed.
48 {
49 [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error
50 # Exit script on bad value(s).
51 # Uses "or-list / and-list".
52 #
53 # Exercise: Implement more rigorous date checking.
54 }
55
56
57 strip_leading_zero () # Better to strip possible leading zero(s)
58 { #+ from day and/or month
59 return ${1#0} #+ since otherwise Bash will interpret them
60 } #+ as octal values (POSIX.2, sect 2.9.2.1).
61
62
63 day_index () # Gauss' Formula:
64 { # Days from Jan. 3, 1600 to date passed as param.
65
66 day=$1
67 month=$2
68 year=$3
69
70 let "month = $month - 2"
71 if [ "$month" -le 0 ]
72 then
73 let "month += 12"
74 let "year -= 1"
75 fi
76
77 let "year -= $REFYR"
78 let "indexyr = $year / $CENTURY"
79
80
81 let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM"
82 # For an in-depth explanation of this algorithm, see
83 #+ http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm
84
85
86 echo $Days
87
88 }
89
90
91 calculate_difference () # Difference between to day indices.
92 {
93 let "diff = $1 - $2" # Global variable.
94 }
95
96
97 abs () # Absolute value
98 { # Uses global "value" variable.
99 if [ "$1" -lt 0 ] # If negative
100 then #+ then
101 let "value = 0 - $1" #+ change sign,
102 else #+ else
103 let "value = $1" #+ leave it alone.
104 fi
105 }
106
107
108
109 if [ $# -ne "$ARGS" ] # Require two command line params.
110 then
111 Param_Error
112 fi
113
114 Parse_Date $1
115 check_date $day $month $year # See if valid date.
116
117 strip_leading_zero $day # Remove any leading zeroes
118 day=$? #+ on day and/or month.
119 strip_leading_zero $month
120 month=$?
121
122 let "date1 = `day_index $day $month $year`"
123
124
125 Parse_Date $2
126 check_date $day $month $year
127
128 strip_leading_zero $day
129 day=$?
130 strip_leading_zero $month
131 month=$?
132
133 date2=$(day_index $day $month $year) # Command substitution.
134
135
136 calculate_difference $date1 $date2
137
138 abs $diff # Make sure it's positive.
139 diff=$value
140
141 echo $diff
142
143 exit 0
144 # Compare this script with
145 #+ the implementation of Gauss' Formula in a C program at:
146 #+ http://buschencrew.hypermart.net/software/datedif |
Example A-8. Make a "dictionary"
1 #!/bin/bash 2 # makedict.sh [make dictionary] 3 4 # Modification of /usr/sbin/mkdict script. 5 # Original script copyright 1993, by Alec Muffett. 6 # 7 # This modified script included in this document in a manner 8 #+ consistent with the "LICENSE" document of the "Crack" package 9 #+ that the original script is a part of. 10 11 # This script processes text files to produce a sorted list 12 #+ of words found in the files. 13 # This may be useful for compiling dictionaries 14 #+ and for lexicographic research. 15 16 17 E_BADARGS=65 18 19 if [ ! -r "$1" ] # Need at least one 20 then #+ valid file argument. 21 echo "Usage: $0 files-to-process" 22 exit $E_BADARGS 23 fi 24 25 26 # SORT="sort" # No longer necessary to define options 27 #+ to sort. Changed from original script. 28 29 cat $* | # Contents of specified files to stdout. 30 tr A-Z a-z | # Convert to lowercase. 31 tr ' ' '\012' | # New: change spaces to newlines. 32 # tr -cd '\012[a-z][0-9]' | # Get rid of everything non-alphanumeric 33 #+ (original script). 34 tr -c '\012a-z' '\012' | # Rather than deleting 35 #+ now change non-alpha to newlines. 36 sort | # $SORT options unnecessary now. 37 uniq | # Remove duplicates. 38 grep -v '^#' | # Delete lines beginning with a hashmark. 39 grep -v '^$' # Delete blank lines. 40 41 exit 0 |
Example A-9. Soundex conversion
1 #!/bin/bash
2 # soundex.sh: Calculate "soundex" code for names
3
4 # =======================================================
5 # Soundex script
6 # by
7 # Mendel Cooper
8 # thegrendel@theriver.com
9 # 23 January, 2002
10 #
11 # Placed in the Public Domain.
12 #
13 # A slightly different version of this script appeared in
14 #+ Ed Schaefer's July, 2002 "Shell Corner" column
15 #+ in "Unix Review" on-line,
16 #+ http://www.unixreview.com/documents/uni1026336632258/
17 # =======================================================
18
19
20 ARGCOUNT=1 # Need name as argument.
21 E_WRONGARGS=70
22
23 if [ $# -ne "$ARGCOUNT" ]
24 then
25 echo "Usage: `basename $0` name"
26 exit $E_WRONGARGS
27 fi
28
29
30 assign_value () # Assigns numerical value
31 { #+ to letters of name.
32
33 val1=bfpv # 'b,f,p,v' = 1
34 val2=cgjkqsxz # 'c,g,j,k,q,s,x,z' = 2
35 val3=dt # etc.
36 val4=l
37 val5=mn
38 val6=r
39
40 # Exceptionally clever use of 'tr' follows.
41 # Try to figure out what is going on here.
42
43 value=$( echo "$1" \
44 | tr -d wh \
45 | tr $val1 1 | tr $val2 2 | tr $val3 3 \
46 | tr $val4 4 | tr $val5 5 | tr $val6 6 \
47 | tr -s 123456 \
48 | tr -d aeiouy )
49
50 # Assign letter values.
51 # Remove duplicate numbers, except when separated by vowels.
52 # Ignore vowels, except as separators, so delete them last.
53 # Ignore 'w' and 'h', even as separators, so delete them first.
54 #
55 # The above command substitution lays more pipe than a plumber <g>.
56
57 }
58
59
60 input_name="$1"
61 echo
62 echo "Name = $input_name"
63
64
65 # Change all characters of name input to lowercase.
66 # ------------------------------------------------
67 name=$( echo $input_name | tr A-Z a-z )
68 # ------------------------------------------------
69 # Just in case argument to script is mixed case.
70
71
72 # Prefix of soundex code: first letter of name.
73 # --------------------------------------------
74
75
76 char_pos=0 # Initialize character position.
77 prefix0=${name:$char_pos:1}
78 prefix=`echo $prefix0 | tr a-z A-Z`
79 # Uppercase 1st letter of soundex.
80
81 let "char_pos += 1" # Bump character position to 2nd letter of name.
82 name1=${name:$char_pos}
83
84
85 # ++++++++++++++++++++++++++ Exception Patch +++++++++++++++++++++++++++++++++
86 # Now, we run both the input name and the name shifted one char to the right
87 #+ through the value-assigning function.
88 # If we get the same value out, that means that the first two characters
89 #+ of the name have the same value assigned, and that one should cancel.
90 # However, we also need to test whether the first letter of the name
91 #+ is a vowel or 'w' or 'h', because otherwise this would bollix things up.
92
93 char1=`echo $prefix | tr A-Z a-z` # First letter of name, lowercased.
94
95 assign_value $name
96 s1=$value
97 assign_value $name1
98 s2=$value
99 assign_value $char1
100 s3=$value
101 s3=9$s3 # If first letter of name is a vowel
102 #+ or 'w' or 'h',
103 #+ then its "value" will be null (unset).
104 #+ Therefore, set it to 9, an otherwise
105 #+ unused value, which can be tested for.
106
107
108 if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]]
109 then
110 suffix=$s2
111 else
112 suffix=${s2:$char_pos}
113 fi
114 # ++++++++++++++++++++++ end Exception Patch +++++++++++++++++++++++++++++++++
115
116
117 padding=000 # Use at most 3 zeroes to pad.
118
119
120 soun=$prefix$suffix$padding # Pad with zeroes.
121
122 MAXLEN=4 # Truncate to maximum of 4 chars.
123 soundex=${soun:0:$MAXLEN}
124
125 echo "Soundex = $soundex"
126
127 echo
128
129 # The soundex code is a method of indexing and classifying names
130 #+ by grouping together the ones that sound alike.
131 # The soundex code for a given name is the first letter of the name,
132 #+ followed by a calculated three-number code.
133 # Similar sounding names should have almost the same soundex codes.
134
135 # Examples:
136 # Smith and Smythe both have a "S-530" soundex.
137 # Harrison = H-625
138 # Hargison = H-622
139 # Harriman = H-655
140
141 # This works out fairly well in practice, but there are numerous anomalies.
142 #
143 #
144 # The U.S. Census and certain other governmental agencies use soundex,
145 # as do genealogical researchers.
146 #
147 # For more information,
148 #+ see the "National Archives and Records Administration home page",
149 #+ http://www.nara.gov/genealogy/soundex/soundex.html
150
151
152
153 # Exercise:
154 # --------
155 # Simplify the "Exception Patch" section of this script.
156
157 exit 0 |
Example A-10. "Game of Life"
1 #!/bin/bash
2 # life.sh: "Life in the Slow Lane"
3 # Version 2: Patched by Daniel Albers
4 #+ to allow non-square grids as input.
5
6 # ##################################################################### #
7 # This is the Bash script version of John Conway's "Game of Life". #
8 # "Life" is a simple implementation of cellular automata. #
9 # --------------------------------------------------------------------- #
10 # On a rectangular grid, let each "cell" be either "living" or "dead". #
11 # Designate a living cell with a dot, and a dead one with a blank space.#
12 # Begin with an arbitrarily drawn dot-and-blank grid, #
13 #+ and let this be the starting generation, "generation 0". #
14 # Determine each successive generation by the following rules: #
15 # 1) Each cell has 8 neighbors, the adjoining cells #
16 #+ left, right, top, bottom, and the 4 diagonals. #
17 # 123 #
18 # 4*5 #
19 # 678 #
20 # #
21 # 2) A living cell with either 2 or 3 living neighbors remains alive. #
22 # 3) A dead cell with 3 living neighbors becomes alive (a "birth"). #
23 SURVIVE=2 #
24 BIRTH=3 #
25 # 4) All other cases result in a dead cell for the next generation. #
26 # ##################################################################### #
27
28
29 startfile=gen0 # Read the starting generation from the file "gen0".
30 # Default, if no other file specified when invoking script.
31 #
32 if [ -n "$1" ] # Specify another "generation 0" file.
33 then
34 if [ -e "$1" ] # Check for existence.
35 then
36 startfile="$1"
37 fi
38 fi
39
40
41 ALIVE1=.
42 DEAD1=_
43 # Represent living and "dead" cells in the start-up file.
44
45 # ---------------------------------------------------------- #
46 # This script uses a 10 x 10 grid (may be increased,
47 #+ but a large grid will will cause very slow execution).
48 ROWS=10
49 COLS=10
50 # Change above two variables to match grid size, if necessary.
51 # ---------------------------------------------------------- #
52
53 GENERATIONS=10 # How many generations to cycle through.
54 # Adjust this upwards,
55 #+ if you have time on your hands.
56
57 NONE_ALIVE=80 # Exit status on premature bailout,
58 #+ if no cells left alive.
59 TRUE=0
60 FALSE=1
61 ALIVE=0
62 DEAD=1
63
64 avar= # Global; holds current generation.
65 generation=0 # Initialize generation count.
66
67 # =================================================================
68
69
70 let "cells = $ROWS * $COLS"
71 # How many cells.
72
73 declare -a initial # Arrays containing "cells".
74 declare -a current
75
76 display ()
77 {
78
79 alive=0 # How many cells "alive" at any given time.
80 # Initially zero.
81
82 declare -a arr
83 arr=( `echo "$1"` ) # Convert passed arg to array.
84
85 element_count=${#arr[*]}
86
87 local i
88 local rowcheck
89
90 for ((i=0; i<$element_count; i++))
91 do
92
93 # Insert newline at end of each row.
94 let "rowcheck = $i % COLS"
95 if [ "$rowcheck" -eq 0 ]
96 then
97 echo # Newline.
98 echo -n " " # Indent.
99 fi
100
101 cell=${arr[i]}
102
103 if [ "$cell" = . ]
104 then
105 let "alive += 1"
106 fi
107
108 echo -n "$cell" | sed -e 's/_/ /g'
109 # Print out array and change underscores to spaces.
110 done
111
112 return
113
114 }
115
116 IsValid () # Test whether cell coordinate valid.
117 {
118
119 if [ -z "$1" -o -z "$2" ] # Mandatory arguments missing?
120 then
121 return $FALSE
122 fi
123
124 local row
125 local lower_limit=0 # Disallow negative coordinate.
126 local upper_limit
127 local left
128 local right
129
130 let "upper_limit = $ROWS * $COLS - 1" # Total number of cells.
131
132
133 if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ]
134 then
135 return $FALSE # Out of array bounds.
136 fi
137
138 row=$2
139 let "left = $row * $COLS" # Left limit.
140 let "right = $left + $COLS - 1" # Right limit.
141
142 if [ "$1" -lt "$left" -o "$1" -gt "$right" ]
143 then
144 return $FALSE # Beyond row boundary.
145 fi
146
147 return $TRUE # Valid coordinate.
148
149 }
150
151
152 IsAlive () # Test whether cell is alive.
153 # Takes array, cell number, state of cell as arguments.
154 {
155 GetCount "$1" $2 # Get alive cell count in neighborhood.
156 local nhbd=$?
157
158
159 if [ "$nhbd" -eq "$BIRTH" ] # Alive in any case.
160 then
161 return $ALIVE
162 fi
163
164 if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ]
165 then # Alive only if previously alive.
166 return $ALIVE
167 fi
168
169 return $DEAD # Default.
170
171 }
172
173
174 GetCount () # Count live cells in passed cell's neighborhood.
175 # Two arguments needed:
176 # $1) variable holding array
177 # $2) cell number
178 {
179 local cell_number=$2
180 local array
181 local top
182 local center
183 local bottom
184 local r
185 local row
186 local i
187 local t_top
188 local t_cen
189 local t_bot
190 local count=0
191 local ROW_NHBD=3
192
193 array=( `echo "$1"` )
194
195 let "top = $cell_number - $COLS - 1" # Set up cell neighborhood.
196 let "center = $cell_number - 1"
197 let "bottom = $cell_number + $COLS - 1"
198 let "r = $cell_number / $COLS"
199
200 for ((i=0; i<$ROW_NHBD; i++)) # Traverse from left to right.
201 do
202 let "t_top = $top + $i"
203 let "t_cen = $center + $i"
204 let "t_bot = $bottom + $i"
205
206
207 let "row = $r" # Count center row of neighborhood.
208 IsValid $t_cen $row # Valid cell position?
209 if [ $? -eq "$TRUE" ]
210 then
211 if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive?
212 then # Yes?
213 let "count += 1" # Increment count.
214 fi
215 fi
216
217 let "row = $r - 1" # Count top row.
218 IsValid $t_top $row
219 if [ $? -eq "$TRUE" ]
220 then
221 if [ ${array[$t_top]} = "$ALIVE1" ]
222 then
223 let "count += 1"
224 fi
225 fi
226
227 let "row = $r + 1" # Count bottom row.
228 IsValid $t_bot $row
229 if [ $? -eq "$TRUE" ]
230 then
231 if [ ${array[$t_bot]} = "$ALIVE1" ]
232 then
233 let "count += 1"
234 fi
235 fi
236
237 done
238
239
240 if [ ${array[$cell_number]} = "$ALIVE1" ]
241 then
242 let "count -= 1" # Make sure value of tested cell itself
243 fi #+ is not counted.
244
245
246 return $count
247
248 }
249
250 next_gen () # Update generation array.
251 {
252
253 local array
254 local i=0
255
256 array=( `echo "$1"` ) # Convert passed arg to array.
257
258 while [ "$i" -lt "$cells" ]
259 do
260 IsAlive "$1" $i ${array[$i]} # Is cell alive?
261 if [ $? -eq "$ALIVE" ]
262 then # If alive, then
263 array[$i]=. #+ represent the cell as a period.
264 else
265 array[$i]="_" # Otherwise underscore
266 fi #+ (which will later be converted to space).
267 let "i += 1"
268 done
269
270
271 # let "generation += 1" # Increment generation count.
272 # Why was the above line commented out?
273
274
275 # Set variable to pass as parameter to "display" function.
276 avar=`echo ${array[@]}` # Convert array back to string variable.
277 display "$avar" # Display it.
278 echo; echo
279 echo "Generation $generation - $alive alive"
280
281 if [ "$alive" -eq 0 ]
282 then
283 echo
284 echo "Premature exit: no more cells alive!"
285 exit $NONE_ALIVE # No point in continuing
286 fi #+ if no live cells.
287
288 }
289
290
291 # =========================================================
292
293 # main ()
294
295 # Load initial array with contents of startup file.
296 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\
297 sed -e 's/\./\. /g' -e 's/_/_ /g'` )
298 # Delete lines containing '#' comment character.
299 # Remove linefeeds and insert space between elements.
300
301 clear # Clear screen.
302
303 echo # Title
304 echo "======================="
305 echo " $GENERATIONS generations"
306 echo " of"
307 echo "\"Life in the Slow Lane\""
308 echo "======================="
309
310
311 # -------- Display first generation. --------
312 Gen0=`echo ${initial[@]}`
313 display "$Gen0" # Display only.
314 echo; echo
315 echo "Generation $generation - $alive alive"
316 # -------------------------------------------
317
318
319 let "generation += 1" # Increment generation count.
320 echo
321
322 # ------- Display second generation. -------
323 Cur=`echo ${initial[@]}`
324 next_gen "$Cur" # Update & display.
325 # ------------------------------------------
326
327 let "generation += 1" # Increment generation count.
328
329 # ------ Main loop for displaying subsequent generations ------
330 while [ "$generation" -le "$GENERATIONS" ]
331 do
332 Cur="$avar"
333 next_gen "$Cur"
334 let "generation += 1"
335 done
336 # ==============================================================
337
338 echo
339
340 exit 0
341
342 # --------------------------------------------------------------
343
344 # The grid in this script has a "boundary problem."
345 # The the top, bottom, and sides border on a void of dead cells.
346 # Exercise: Change the script to have the grid wrap around,
347 # + so that the left and right sides will "touch,"
348 # + as will the top and bottom.
349 #
350 # Exercise: Create a new "gen0" file to seed this script.
351 # Use a 12 x 16 grid, instead of the original 10 x 10 one.
352 # Make the necessary changes to the script,
353 #+ so it will run with the altered file.
354 #
355 # Exercise: Modify this script so that it can determine the grid size
356 #+ from the "gen0" file, and set any variables necessary
357 #+ for the script to run.
358 # This would make unnecessary any changes to variables
359 #+ in the script for an altered grid size. |
Example A-11. Data file for "Game of Life"
1 # This is an example "generation 0" start-up file for "life.sh". 2 # -------------------------------------------------------------- 3 # The "gen0" file is a 10 x 10 grid using a period (.) for live cells, 4 #+ and an underscore (_) for dead ones. We cannot simply use spaces 5 #+ for dead cells in this file because of a peculiarity in Bash arrays. 6 # [Exercise for the reader: explain this.] 7 # 8 # Lines beginning with a '#' are comments, and the script ignores them. 9 __.__..___ 10 ___._.____ 11 ____.___.. 12 _._______. 13 ____._____ 14 ..__...___ 15 ____._____ 16 ___...____ 17 __.._..___ 18 _..___..__ |
+++
The following two scripts are by Mark Moraes of the University of Toronto. See the enclosed file "Moraes-COPYRIGHT" for permissions and restrictions.
Example A-12. behead: Removing mail and news message headers
1 #! /bin/sh 2 # Strips off the header from a mail/News message i.e. till the first 3 # empty line 4 # Mark Moraes, University of Toronto 5 6 # ==> These comments added by author of this document. 7 8 if [ $# -eq 0 ]; then 9 # ==> If no command line args present, then works on file redirected to stdin. 10 sed -e '1,/^$/d' -e '/^[ ]*$/d' 11 # --> Delete empty lines and all lines until 12 # --> first one beginning with white space. 13 else 14 # ==> If command line args present, then work on files named. 15 for i do 16 sed -e '1,/^$/d' -e '/^[ ]*$/d' $i 17 # --> Ditto, as above. 18 done 19 fi 20 21 # ==> Exercise: Add error checking and other options. 22 # ==> 23 # ==> Note that the small sed script repeats, except for the arg passed. 24 # ==> Does it make sense to embed it in a function? Why or why not? |
Example A-13. ftpget: Downloading files via ftp
1 #! /bin/sh
2 # $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $
3 # Script to perform batch anonymous ftp. Essentially converts a list of
4 # of command line arguments into input to ftp.
5 # ==> This script is nothing but a shell wrapper around "ftp" . . .
6 # Simple, and quick - written as a companion to ftplist
7 # -h specifies the remote host (default prep.ai.mit.edu)
8 # -d specifies the remote directory to cd to - you can provide a sequence
9 # of -d options - they will be cd'ed to in turn. If the paths are relative,
10 # make sure you get the sequence right. Be careful with relative paths -
11 # there are far too many symlinks nowadays.
12 # (default is the ftp login directory)
13 # -v turns on the verbose option of ftp, and shows all responses from the
14 # ftp server.
15 # -f remotefile[:localfile] gets the remote file into localfile
16 # -m pattern does an mget with the specified pattern. Remember to quote
17 # shell characters.
18 # -c does a local cd to the specified directory
19 # For example,
20 # ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \
21 # -d ../pub/R3/fixes -c ~/fixes -m 'fix*'
22 # will get xplaces.shar from ~ftp/contrib on expo.lcs.mit.edu, and put it in
23 # xplaces.sh in the current working directory, and get all fixes from
24 # ~ftp/pub/R3/fixes and put them in the ~/fixes directory.
25 # Obviously, the sequence of the options is important, since the equivalent
26 # commands are executed by ftp in corresponding order
27 #
28 # Mark Moraes <moraes@csri.toronto.edu>, Feb 1, 1989
29 #
30
31
32 # ==> These comments added by author of this document.
33
34 # PATH=/local/bin:/usr/ucb:/usr/bin:/bin
35 # export PATH
36 # ==> Above 2 lines from original script probably superfluous.
37
38 E_BADARGS=65
39
40 TMPFILE=/tmp/ftp.$$
41 # ==> Creates temp file, using process id of script ($$)
42 # ==> to construct filename.
43
44 SITE=`domainname`.toronto.edu
45 # ==> 'domainname' similar to 'hostname'
46 # ==> May rewrite this to parameterize this for general use.
47
48 usage="Usage: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \
49 [-c localdirectory] [-m filepattern] [-v]"
50 ftpflags="-i -n"
51 verbflag=
52 set -f # So we can use globbing in -m
53 set x `getopt vh:d:c:m:f: $*`
54 if [ $? != 0 ]; then
55 echo $usage
56 exit $E_BADARGS
57 fi
58 shift
59 trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15
60 # ==> Delete tempfile in case of abnormal exit from script.
61 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}"
62 # ==> Added quotes (recommended in complex echoes).
63 echo binary >> ${TMPFILE}
64 for i in $* # ==> Parse command line args.
65 do
66 case $i in
67 -v) verbflag=-v; echo hash >> ${TMPFILE}; shift;;
68 -h) remhost=$2; shift 2;;
69 -d) echo cd $2 >> ${TMPFILE};
70 if [ x${verbflag} != x ]; then
71 echo pwd >> ${TMPFILE};
72 fi;
73 shift 2;;
74 -c) echo lcd $2 >> ${TMPFILE}; shift 2;;
75 -m) echo mget "$2" >> ${TMPFILE}; shift 2;;
76 -f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
77 echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;;
78 --) shift; break;;
79 esac
80 # ==> 'lcd' and 'mget' are ftp commands. See "man ftp" . . .
81 done
82 if [ $# -ne 0 ]; then
83 echo $usage
84 exit $E_BADARGS
85 # ==> Changed from "exit 2" to conform with style standard.
86 fi
87 if [ x${verbflag} != x ]; then
88 ftpflags="${ftpflags} -v"
89 fi
90 if [ x${remhost} = x ]; then
91 remhost=prep.ai.mit.edu
92 # ==> Change to match appropriate ftp site.
93 fi
94 echo quit >> ${TMPFILE}
95 # ==> All commands saved in tempfile.
96
97 ftp ${ftpflags} ${remhost} < ${TMPFILE}
98 # ==> Now, tempfile batch processed by ftp.
99
100 rm -f ${TMPFILE}
101 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile).
102
103
104 # ==> Exercises:
105 # ==> ---------
106 # ==> 1) Add error checking.
107 # ==> 2) Add bells & whistles. |
+
Antek Sawicki contributed the following script, which makes very clever use of the parameter substitution operators discussed in Section 9.3.
Example A-14. password: Generating random 8-character passwords
1 #!/bin/bash
2 # May need to be invoked with #!/bin/bash2 on older machines.
3 #
4 # Random password generator for Bash 2.x by Antek Sawicki <tenox@tenox.tc>,
5 # who generously gave permission to the document author to use it here.
6 #
7 # ==> Comments added by document author ==>
8
9
10 MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
11 # ==> Password will consist of alphanumeric characters.
12 LENGTH="8"
13 # ==> May change 'LENGTH' for longer password.
14
15
16 while [ "${n:=1}" -le "$LENGTH" ]
17 # ==> Recall that := is "default substitution" operator.
18 # ==> So, if 'n' has not been initialized, set it to 1.
19 do
20 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
21 # ==> Very clever, but tricky.
22
23 # ==> Starting from the innermost nesting...
24 # ==> ${#MATRIX} returns length of array MATRIX.
25
26 # ==> $RANDOM%${#MATRIX} returns random number between 1
27 # ==> and [length of MATRIX] - 1.
28
29 # ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1}
30 # ==> returns expansion of MATRIX at random position, by length 1.
31 # ==> See {var:pos:len} parameter substitution in Chapter 9.
32 # ==> and the associated examples.
33
34 # ==> PASS=... simply pastes this result onto previous PASS (concatenation).
35
36 # ==> To visualize this more clearly, uncomment the following line
37 # echo "$PASS"
38 # ==> to see PASS being built up,
39 # ==> one character at a time, each iteration of the loop.
40
41 let n+=1
42 # ==> Increment 'n' for next pass.
43 done
44
45 echo "$PASS" # ==> Or, redirect to a file, as desired.
46
47 exit 0 |
+
James R. Van Zandt contributed this script, which uses named pipes and, in his words, "really exercises quoting and escaping".
Example A-15. fifo: Making daily backups, using named pipes
1 #!/bin/bash
2 # ==> Script by James R. Van Zandt, and used here with his permission.
3
4 # ==> Comments added by author of this document.
5
6
7 HERE=`uname -n` # ==> hostname
8 THERE=bilbo
9 echo "starting remote backup to $THERE at `date +%r`"
10 # ==> `date +%r` returns time in 12-hour format, i.e. "08:08:34 PM".
11
12 # make sure /pipe really is a pipe and not a plain file
13 rm -rf /pipe
14 mkfifo /pipe # ==> Create a "named pipe", named "/pipe".
15
16 # ==> 'su xyz' runs commands as user "xyz".
17 # ==> 'ssh' invokes secure shell (remote login client).
18 su xyz -c "ssh $THERE \"cat >/home/xyz/backup/${HERE}-daily.tar.gz\" < /pipe"&
19 cd /
20 tar -czf - bin boot dev etc home info lib man root sbin share usr var >/pipe
21 # ==> Uses named pipe, /pipe, to communicate between processes:
22 # ==> 'tar/gzip' writes to /pipe and 'ssh' reads from /pipe.
23
24 # ==> The end result is this backs up the main directories, from / on down.
25
26 # ==> What are the advantages of a "named pipe" in this situation,
27 # ==>+ as opposed to an "anonymous pipe", with |?
28 # ==> Will an anonymous pipe even work here?
29
30
31 exit 0 |
+
St