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