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 is useful when writing man page source and you want to 5 #+ look at the intermediate results on the fly while working on it. 6 7 E_WRONGARGS=65 8 9 if [ -z "$1" ] 10 then 11 echo "Usage: `basename $0` filename" 12 exit $E_WRONGARGS 13 fi 14 15 groff -Tascii -man $1 | less 16 # From the man page for groff. 17 18 # If the man page includes tables and/or equations, 19 #+ then the above code will barf. 20 # The following line can handle such cases. 21 # 22 # gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man 23 # 24 # Thanks, S.C. 25 26 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-15.
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 somewhat insecure, 6 #+ since the decrypted password is sent in the clear. 7 # Use something like "ssh" if this is a concern. 8 9 E_BADARGS=65 10 11 if [ -z "$1" ] 12 then 13 echo "Usage: `basename $0` filename" 14 exit $E_BADARGS 15 fi 16 17 Username=bozo # Change to suit. 18 pword=/home/bozo/secret/password_encrypted.file 19 # File containing encrypted password. 20 21 Filename=`basename $1` # Strips pathname out of file name 22 23 Server="XXX" 24 Directory="YYY" # Change above to actual server name & directory. 25 26 27 Password=`cruft <$pword` # Decrypt password. 28 # Uses the author's own "cruft" file encryption package, 29 #+ based on the classic "onetime pad" algorithm, 30 #+ and obtainable from: 31 #+ Primary-site: ftp://ibiblio.org/pub/Linux/utils/file 32 #+ cruft-0.2.tar.gz [16k] 33 34 35 ftp -n $Server <<End-Of-Session 36 user $Username $Password 37 binary 38 bell 39 cd $Directory 40 put $Filename 41 bye 42 End-Of-Session 43 # -n option to "ftp" disables auto-logon. 44 # "bell" rings 'bell' after each file transfer. 45 46 exit 0 |
Example A-6. copy-cd: Copying a data CD
1 #!/bin/bash 2 # copy-cd.sh: copying a data CD 3 4 CDROM=/dev/cdrom # CD ROM device 5 OF=/home/bozo/projects/cdimage.iso # output file 6 # /xxxx/xxxxxxx/ Change to suit your system. 7 BLOCKSIZE=2048 8 SPEED=2 # May use higher speed if supported. 9 10 echo; echo "Insert source CD, but do *not* mount it." 11 echo "Press ENTER when ready. " 12 read ready # Wait for input, $ready not used. 13 14 echo; echo "Copying the source CD to $OF." 15 echo "This may take a while. Please be patient." 16 17 dd if=$CDROM of=$OF bs=$BLOCKSIZE # Raw device copy. 18 19 20 echo; echo "Remove data CD." 21 echo "Insert blank CDR." 22 echo "Press ENTER when ready. " 23 read ready # Wait for input, $ready not used. 24 25 echo "Copying $OF to CDR." 26 27 cdrecord -v -isosize speed=$SPEED dev=0,0 $OF 28 # Uses Joerg Schilling's "cdrecord" package (see its docs). 29 # http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html 30 31 32 echo; echo "Done copying $OF to CDR on device $CDROM." 33 34 echo "Do you want to erase the image file (y/n)? " # Probably a huge file. 35 read answer 36 37 case "$answer" in 38 [yY]) rm -f $OF 39 echo "$OF erased." 40 ;; 41 *) echo "$OF not erased.";; 42 esac 43 44 echo 45 46 # Exercise: 47 # Change the above "case" statement to also accept "yes" and "Yes" as input. 48 49 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 ARGS=2 # Two command line parameters expected. 6 E_PARAM_ERR=65 # Param error. 7 8 REFYR=1600 # Reference year. 9 CENTURY=100 10 DIY=365 11 ADJ_DIY=367 # Adjusted for leap year + fraction. 12 MIY=12 13 DIM=31 14 LEAPCYCLE=4 15 16 MAXRETVAL=256 # Largest permissable 17 # positive return value from a function. 18 19 diff= # Declare global variable for date difference. 20 value= # Declare global variable for absolute value. 21 day= # Declare globals for day, month, year. 22 month= 23 year= 24 25 26 Param_Error () # Command line parameters wrong. 27 { 28 echo "Usage: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY" 29 echo " (date must be after 1/3/1600)" 30 exit $E_PARAM_ERR 31 } 32 33 34 Parse_Date () # Parse date from command line params. 35 { 36 month=${1%%/**} 37 dm=${1%/**} # Day and month. 38 day=${dm#*/} 39 let "year = `basename $1`" # Not a filename, but works just the same. 40 } 41 42 43 check_date () # Checks for invalid date(s) passed. 44 { 45 [ "$day" -gt "$DIM" ] || [ "$month" -gt "$MIY" ] || [ "$year" -lt "$REFYR" ] && Param_Error 46 # Exit script on bad value(s). 47 # Uses "or-list / and-list". 48 # 49 # Exercise: Implement more rigorous date checking. 50 } 51 52 53 strip_leading_zero () # Better to strip possible leading zero(s) 54 { # from day and/or month 55 val=${1#0} # since otherwise Bash will interpret them 56 return $val # as octal values (POSIX.2, sect 2.9.2.1). 57 } 58 59 60 day_index () # Gauss' Formula: 61 { # Days from Jan. 3, 1600 to date passed as param. 62 63 day=$1 64 month=$2 65 year=$3 66 67 let "month = $month - 2" 68 if [ "$month" -le 0 ] 69 then 70 let "month += 12" 71 let "year -= 1" 72 fi 73 74 let "year -= $REFYR" 75 let "indexyr = $year / $CENTURY" 76 77 78 let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM" 79 # For an in-depth explanation of this algorithm, see 80 # http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm 81 82 83 if [ "$Days" -gt "$MAXRETVAL" ] # If greater than 256, 84 then # then change to negative value 85 let "dindex = 0 - $Days" # which can be returned from function. 86 else let "dindex = $Days" 87 fi 88 89 return $dindex 90 91 } 92 93 94 calculate_difference () # Difference between to day indices. 95 { 96 let "diff = $1 - $2" # Global variable. 97 } 98 99 100 abs () # Absolute value 101 { # Uses global "value" variable. 102 if [ "$1" -lt 0 ] # If negative 103 then # then 104 let "value = 0 - $1" # change sign, 105 else # else 106 let "value = $1" # leave it alone. 107 fi 108 } 109 110 111 112 if [ $# -ne "$ARGS" ] # Require two command line params. 113 then 114 Param_Error 115 fi 116 117 Parse_Date $1 118 check_date $day $month $year # See if valid date. 119 120 strip_leading_zero $day # Remove any leading zeroes 121 day=$? # on day and/or month. 122 strip_leading_zero $month 123 month=$? 124 125 day_index $day $month $year 126 date1=$? 127 128 abs $date1 # Make sure it's positive 129 date1=$value # by getting absolute value. 130 131 Parse_Date $2 132 check_date $day $month $year 133 134 strip_leading_zero $day 135 day=$? 136 strip_leading_zero $month 137 month=$? 138 139 day_index $day $month $year 140 date2=$? 141 142 abs $date2 # Make sure it's positive. 143 date2=$value 144 145 calculate_difference $date1 $date2 146 147 abs $diff # Make sure it's positive. 148 diff=$value 149 150 echo $diff 151 152 exit 0 153 # Compare this script with the implementation of Gauss' Formula in C at 154 # http://buschencrew.hypermart.net/software/datedif |
Example A-9. Make a "dictionary"
1 #!/bin/bash 2 # makedict.sh [make dictionary] 3 4 # Modification of /usr/sbin/mkdict script. 5 # Original script copyright 1993, by Alec Muffett. 6 # 7 # This modified script included in this document in a manner 8 #+ consistent with the "LICENSE" document of the "Crack" package 9 #+ that the original script is a part of. 10 11 # This script processes text files to produce a sorted list 12 #+ of words found in the files. 13 # This may be useful for compiling dictionaries 14 #+ and for lexicographic research. 15 16 17 E_BADARGS=65 18 19 if [ ! -r "$1" ] # Need at least one 20 then #+ valid file argument. 21 echo "Usage: $0 files-to-process" 22 exit $E_BADARGS 23 fi 24 25 26 # SORT="sort" # No longer necessary to define options 27 #+ to sort. Changed from original script. 28 29 cat $* | # Contents of specified files to stdout. 30 tr A-Z a-z | # Convert to lowercase. 31 tr ' ' '\012' | # New: change spaces to newlines. 32 # tr -cd '\012[a-z][0-9]' | # Get rid of everything non-alphanumeric 33 #+ (original script). 34 tr -c '\012a-z' '\012' | # Rather than deleting 35 #+ now change non-alpha to newlines. 36 sort | # $SORT options unnecessary now. 37 uniq | # Remove duplicates. 38 grep -v '^#' | # Delete lines beginning with a hashmark. 39 grep -v '^$' # Delete blank lines. 40 41 exit 0 |
Example A-10. Soundex conversion
1 #!/bin/bash 2 # soundex.sh: Calculate "soundex" code for names 3 4 # ======================================================= 5 # Soundex script 6 # by 7 # Mendel Cooper 8 # thegrendel@theriver.com 9 # 23 January, 2002 10 # 11 # Placed in the Public Domain. 12 # 13 # A slightly different version of this script appeared in 14 #+ Ed Schaefer's July, 2002 "Shell Corner" column 15 #+ in "Unix Review" on-line, 16 #+ http://www.unixreview.com/documents/uni1026336632258/ 17 # ======================================================= 18 19 20 ARGCOUNT=1 # Need name as argument. 21 E_WRONGARGS=70 22 23 if [ $# -ne "$ARGCOUNT" ] 24 then 25 echo "Usage: `basename $0` name" 26 exit $E_WRONGARGS 27 fi 28 29 30 assign_value () # Assigns numerical value 31 { #+ to letters of name. 32 33 val1=bfpv # 'b,f,p,v' = 1 34 val2=cgjkqsxz # 'c,g,j,k,q,s,x,z' = 2 35 val3=dt # etc. 36 val4=l 37 val5=mn 38 val6=r 39 40 # Exceptionally clever use of 'tr' follows. 41 # Try to figure out what is going on here. 42 43 value=$( echo "$1" \ 44 | tr -d wh \ 45 | tr $val1 1 | tr $val2 2 | tr $val3 3 \ 46 | tr $val4 4 | tr $val5 5 | tr $val6 6 \ 47 | tr -s 123456 \ 48 | tr -d aeiouy ) 49 50 # Assign letter values. 51 # Remove duplicate numbers, except when separated by vowels. 52 # Ignore vowels, except as separators, so delete them last. 53 # Ignore 'w' and 'h', even as separators, so delete them first. 54 # 55 # The above command substitution lays more pipe than a plumber <g>. 56 57 } 58 59 60 input_name="$1" 61 echo 62 echo "Name = $input_name" 63 64 65 # Change all characters of name input to lowercase. 66 # ------------------------------------------------ 67 name=$( echo $input_name | tr A-Z a-z ) 68 # ------------------------------------------------ 69 # Just in case argument to script is mixed case. 70 71 72 # Prefix of soundex code: first letter of name. 73 # -------------------------------------------- 74 75 76 char_pos=0 # Initialize character position. 77 prefix0=${name:$char_pos:1} 78 prefix=`echo $prefix0 | tr a-z A-Z` 79 # Uppercase 1st letter of soundex. 80 81 let "char_pos += 1" # Bump character position to 2nd letter of name. 82 name1=${name:$char_pos} 83 84 85 # ++++++++++++++++++++++++++ Exception Patch +++++++++++++++++++++++++++++++++ 86 # Now, we run both the input name and the name shifted one char to the right 87 #+ through the value-assigning function. 88 # If we get the same value out, that means that the first two characters 89 #+ of the name have the same value assigned, and that one should cancel. 90 # However, we also need to test whether the first letter of the name 91 #+ is a vowel or 'w' or 'h', because otherwise this would bollix things up. 92 93 char1=`echo $prefix | tr A-Z a-z` # First letter of name, lowercased. 94 95 assign_value $name 96 s1=$value 97 assign_value $name1 98 s2=$value 99 assign_value $char1 100 s3=$value 101 s3=9$s3 # If first letter of name is a vowel 102 #+ or 'w' or 'h', 103 #+ then its "value" will be null (unset). 104 #+ Therefore, set it to 9, an otherwise 105 #+ unused value, which can be tested for. 106 107 108 if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]] 109 then 110 suffix=$s2 111 else 112 suffix=${s2:$char_pos} 113 fi 114 # ++++++++++++++++++++++ end Exception Patch +++++++++++++++++++++++++++++++++ 115 116 117 padding=000 # Use at most 3 zeroes to pad. 118 119 120 soun=$prefix$suffix$padding # Pad with zeroes. 121 122 MAXLEN=4 # Truncate to maximum of 4 chars. 123 soundex=${soun:0:$MAXLEN} 124 125 echo "Soundex = $soundex" 126 127 echo 128 129 # The soundex code is a method of indexing and classifying names 130 #+ by grouping together the ones that sound alike. 131 # The soundex code for a given name is the first letter of the name, 132 #+ followed by a calculated three-number code. 133 # Similar sounding names should have almost the same soundex codes. 134 135 # Examples: 136 # Smith and Smythe both have a "S-530" soundex. 137 # Harrison = H-625 138 # Hargison = H-622 139 # Harriman = H-655 140 141 # This works out fairly well in practice, but there are numerous anomalies. 142 # 143 # 144 # The U.S. Census and certain other governmental agencies use soundex, 145 # as do genealogical researchers. 146 # 147 # For more information, 148 #+ see the "National Archives and Records Administration home page", 149 #+ http://www.nara.gov/genealogy/soundex/soundex.html 150 151 152 153 # Exercise: 154 # -------- 155 # Simplify the "Exception Patch" section of this script. 156 157 exit 0 |
Example A-11. "Game of Life"
1 #!/bin/bash 2 # life.sh: "Life in the Slow Lane" 3 4 # ##################################################################### # 5 # This is the Bash script version of John Conway's "Game of Life". # 6 # "Life" is a simple implementation of cellular automata. # 7 # --------------------------------------------------------------------- # 8 # On a rectangular grid, let each "cell" be either "living" or "dead". # 9 # Designate a living cell with a dot, and a dead one with a blank space.# 10 # Begin with an arbitrarily drawn dot-and-blank grid, # 11 #+ and let this be the starting generation, "generation 0". # 12 # Determine each successive generation by the following rules: # 13 # 1) Each cell has 8 neighbors, the adjoining cells # 14 #+ left, right, top, bottom, and the 4 diagonals. # 15 # 123 # 16 # 4*5 # 17 # 678 # 18 # # 19 # 2) A living cell with either 2 or 3 living neighbors remains alive. # 20 # 3) A dead cell with 3 living neighbors becomes alive (a "birth"). # 21 SURVIVE=2 # 22 BIRTH=3 # 23 # 4) All other cases result in dead cells. # 24 # ##################################################################### # 25 26 27 startfile=gen0 # Read the starting generation from the file "gen0". 28 # Default, if no other file specified when invoking script. 29 # 30 if [ -n "$1" ] # Specify another "generation 0" file. 31 then 32 if [ -e "$1" ] # Check for existence. 33 then 34 startfile="$1" 35 fi 36 fi 37 38 39 ALIVE1=. 40 DEAD1=_ 41 # Represent living and "dead" cells in the start-up file. 42 43 # This script uses a 10 x 10 grid (may be increased, 44 #+ but a large grid will will cause very slow execution). 45 ROWS=10 46 COLS=10 47 48 GENERATIONS=10 # How many generations to cycle through. 49 # Adjust this upwards, 50 #+ if you have time on your hands. 51 52 NONE_ALIVE=80 # Exit status on premature bailout, 53 #+ if no cells left alive. 54 TRUE=0 55 FALSE=1 56 ALIVE=0 57 DEAD=1 58 59 avar= # Global; holds current generation. 60 generation=0 # Initialize generation count. 61 62 # ================================================================= 63 64 65 let "cells = $ROWS * $COLS" 66 # How many cells. 67 68 declare -a initial # Arrays containing "cells". 69 declare -a current 70 71 display () 72 { 73 74 alive=0 # How many cells "alive". 75 # Initially zero. 76 77 declare -a arr 78 arr=( `echo "$1"` ) # Convert passed arg to array. 79 80 element_count=${#arr[*]} 81 82 local i 83 local rowcheck 84 85 for ((i=0; i<$element_count; i++)) 86 do 87 88 # Insert newline at end of each row. 89 let "rowcheck = $i % ROWS" 90 if [ "$rowcheck" -eq 0 ] 91 then 92 echo # Newline. 93 echo -n " " # Indent. 94 fi 95 96 cell=${arr[i]} 97 98 if [ "$cell" = . ] 99 then 100 let "alive += 1" 101 fi 102 103 echo -n "$cell" | sed -e 's/_/ /g' 104 # Print out array and change underscores to spaces. 105 done 106 107 return 108 109 } 110 111 IsValid () # Test whether cell coordinate valid. 112 { 113 114 if [ -z "$1" -o -z "$2" ] # Mandatory arguments missing? 115 then 116 return $FALSE 117 fi 118 119 local row 120 local lower_limit=0 # Disallow negative coordinate. 121 local upper_limit 122 local left 123 local right 124 125 let "upper_limit = $ROWS * $COLS - 1" # Total number of cells. 126 127 128 if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ] 129 then 130 return $FALSE # Out of array bounds. 131 fi 132 133 row=$2 134 let "left = $row * $ROWS" # Left limit. 135 let "right = $left + $COLS - 1" # Right limit. 136 137 if [ "$1" -lt "$left" -o "$1" -gt "$right" ] 138 then 139 return $FALSE # Beyond row boundary. 140 fi 141 142 return $TRUE # Valid coordinate. 143 144 } 145 146 147 IsAlive () # Test whether cell is alive. 148 # Takes array, cell number, state of cell as arguments. 149 { 150 GetCount "$1" $2 # Get alive cell count in neighborhood. 151 local nhbd=$? 152 153 154 if [ "$nhbd" -eq "$BIRTH" ] # Alive in any case. 155 then 156 return $ALIVE 157 fi 158 159 if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ] 160 then # Alive only if previously alive. 161 return $ALIVE 162 fi 163 164 return $DEAD # Default. 165 166 } 167 168 169 GetCount () # Count live cells in passed cell's neighborhood. 170 # Two arguments needed: 171 # $1) variable holding array 172 # $2) cell number 173 { 174 local cell_number=$2 175 local array 176 local top 177 local center 178 local bottom 179 local r 180 local row 181 local i 182 local t_top 183 local t_cen 184 local t_bot 185 local count=0 186 local ROW_NHBD=3 187 188 array=( `echo "$1"` ) 189 190 let "top = $cell_number - $COLS - 1" # Set up cell neighborhood. 191 let "center = $cell_number - 1" 192 let "bottom = $cell_number + $COLS - 1" 193 let "r = $cell_number / $ROWS" 194 195 for ((i=0; i<$ROW_NHBD; i++)) # Traverse from left to right. 196 do 197 let "t_top = $top + $i" 198 let "t_cen = $center + $i" 199 let "t_bot = $bottom + $i" 200 201 202 let "row = $r" # Count center row of neighborhood. 203 IsValid $t_cen $row # Valid cell position? 204 if [ $? -eq "$TRUE" ] 205 then 206 if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive? 207 then # Yes? 208 let "count += 1" # Increment count. 209 fi 210 fi 211 212 let "row = $r - 1" # Count top row. 213 IsValid $t_top $row 214 if [ $? -eq "$TRUE" ] 215 then 216 if [ ${array[$t_top]} = "$ALIVE1" ] 217 then 218 let "count += 1" 219 fi 220 fi 221 222 let "row = $r + 1" # Count bottom row. 223 IsValid $t_bot $row 224 if [ $? -eq "$TRUE" ] 225 then 226 if [ ${array[$t_bot]} = "$ALIVE1" ] 227 then 228 let "count += 1" 229 fi 230 fi 231 232 done 233 234 235 if [ ${array[$cell_number]} = "$ALIVE1" ] 236 then 237 let "count -= 1" # Make sure value of tested cell itself 238 fi #+ is not counted. 239 240 241 return $count 242 243 } 244 245 next_gen () # Update generation array. 246 { 247 248 local array 249 local i=0 250 251 array=( `echo "$1"` ) # Convert passed arg to array. 252 253 while [ "$i" -lt "$cells" ] 254 do 255 IsAlive "$1" $i ${array[$i]} # Is cell alive? 256 if [ $? -eq "$ALIVE" ] 257 then # If alive, then 258 array[$i]=. #+ represent the cell as a period. 259 else 260 array[$i]="_" # Otherwise underscore 261 fi #+ (which will later be converted to space). 262 let "i += 1" 263 done 264 265 266 # let "generation += 1" # Increment generation count. 267 268 # Set variable to pass as parameter to "display" function. 269 avar=`echo ${array[@]}` # Convert array back to string variable. 270 display "$avar" # Display it. 271 echo; echo 272 echo "Generation $generation -- $alive alive" 273 274 if [ "$alive" -eq 0 ] 275 then 276 echo 277 echo "Premature exit: no more cells alive!" 278 exit $NONE_ALIVE # No point in continuing 279 fi #+ if no live cells. 280 281 } 282 283 284 # ========================================================= 285 286 # main () 287 288 # Load initial array with contents of startup file. 289 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\ 290 sed -e 's/\./\. /g' -e 's/_/_ /g'` ) 291 # Delete lines containing '#' comment character. 292 # Remove linefeeds and insert space between elements. 293 294 clear # Clear screen. 295 296 echo # Title 297 echo "=======================" 298 echo " $GENERATIONS generations" 299 echo " of" 300 echo "\"Life in the Slow Lane\"" 301 echo "=======================" 302 303 304 # -------- Display first generation. -------- 305 Gen0=`echo ${initial[@]}` 306 display "$Gen0" # Display only. 307 echo; echo 308 echo "Generation $generation -- $alive alive" 309 # ------------------------------------------- 310 311 312 let "generation += 1" # Increment generation count. 313 echo 314 315 # ------- Display second generation. ------- 316 Cur=`echo ${initial[@]}` 317 next_gen "$Cur" # Update & display. 318 # ------------------------------------------ 319 320 let "generation += 1" # Increment generation count. 321 322 # ------ Main loop for displaying subsequent generations ------ 323 while [ "$generation" -le "$GENERATIONS" ] 324 do 325 Cur="$avar" 326 next_gen "$Cur" 327 let "generation += 1" 328 done 329 # ============================================================== 330 331 echo 332 333 exit 0 334 335 # -------------------------------------------------------------- 336 # The grid in this script has a "boundary problem". 337 # The the top, bottom, and sides border on a void of dead cells. 338 # Exercise: Change the script to have the grid wrap around, 339 # + so that the left and right sides will "touch", 340 # + as will the top and bottom. |
Example A-12. Data file for "Game of Life"
1 # This is an example "generation 0" start-up file for "life.sh". 2 # -------------------------------------------------------------- 3 # The "gen0" file is a 10 x 10 grid using a period (.) for live cells, 4 #+ and an underscore (_) for dead ones. We cannot simply use spaces 5 #+ for dead cells in this file because of a peculiarity in Bash arrays. 6 # [Exercise for the reader: explain this.] 7 # 8 # Lines beginning with a '#' are comments, and the script ignores them. 9 __.__..___ 10 ___._.____ 11 ____.___.. 12 _._______. 13 ____._____ 14 ..__...___ 15 ____._____ 16 ___...____ 17 __.._..___ 18 _..___..__ |
+++
The following two scripts are by Mark Moraes of the University of Toronto. See the enclosed file "Moraes-COPYRIGHT" for permissions and restrictions.
Example A-13. behead: Removing mail and news message headers
1 #! /bin/sh 2 # Strips off the header from a mail/News message i.e. till the first 3 # empty line 4 # Mark Moraes, University of Toronto 5 6 # ==> These comments added by author of this document. 7 8 if [ $# -eq 0 ]; then 9 # ==> If no command line args present, then works on file redirected to stdin. 10 sed -e '1,/^$/d' -e '/^[ ]*$/d' 11 # --> Delete empty lines and all lines until 12 # --> first one beginning with white space. 13 else 14 # ==> If command line args present, then work on files named. 15 for i do 16 sed -e '1,/^$/d' -e '/^[ ]*$/d' $i 17 # --> Ditto, as above. 18 done 19 fi 20 21 # ==> Exercise: Add error checking and other options. 22 # ==> 23 # ==> Note that the small sed script repeats, except for the arg passed. 24 # ==> Does it make sense to embed it in a function? Why or why not? |
Example A-14. ftpget: Downloading files via ftp
1 #! /bin/sh 2 # $Id: ftpget,v 1.2 91/05/07 21:15:43 moraes Exp $ 3 # Script to perform batch anonymous ftp. Essentially converts a list of 4 # of command line arguments into input to ftp. 5 # Simple, and quick - written as a companion to ftplist 6 # -h specifies the remote host (default prep.ai.mit.edu) 7 # -d specifies the remote directory to cd to - you can provide a sequence 8 # of -d options - they will be cd'ed to in turn. If the paths are relative, 9 # make sure you get the sequence right. Be careful with relative paths - 10 # there are far too many symlinks nowadays. 11 # (default is the ftp login directory) 12 # -v turns on the verbose option of ftp, and shows all responses from the 13 # ftp server. 14 # -f remotefile[:localfile] gets the remote file into localfile 15 # -m pattern does an mget with the specified pattern. Remember to quote 16 # shell characters. 17 # -c does a local cd to the specified directory 18 # For example, 19 # ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \ 20 # -d ../pub/R3/fixes -c ~/fixes -m 'fix*' 21 # will get xplaces.shar from ~ftp/contrib on expo.lcs.mit.edu, and put it in 22 # xplaces.sh in the current working directory, and get all fixes from 23 # ~ftp/pub/R3/fixes and put them in the ~/fixes directory. 24 # Obviously, the sequence of the options is important, since the equivalent 25 # commands are executed by ftp in corresponding order 26 # 27 # Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 28 # ==> Angle brackets changed to parens, so Docbook won't get indigestion. 29 # 30 31 32 # ==> These comments added by author of this document. 33 34 # PATH=/local/bin:/usr/ucb:/usr/bin:/bin 35 # export PATH 36 # ==> Above 2 lines from original script probably superfluous. 37 38 TMPFILE=/tmp/ftp.$$ 39 # ==> Creates temp file, using process id of script ($$) 40 # ==> to construct filename. 41 42 SITE=`domainname`.toronto.edu 43 # ==> 'domainname' similar to 'hostname' 44 # ==> May rewrite this to parameterize this for general use. 45 46 usage="Usage: $0 [-h remotehost] [-d remotedirectory]... [-f remfile:localfile]... \ 47 [-c localdirectory] [-m filepattern] [-v]" 48 ftpflags="-i -n" 49 verbflag= 50 set -f # So we can use globbing in -m 51 set x `getopt vh:d:c:m:f: $*` 52 if [ $? != 0 ]; then 53 echo $usage 54 exit 65 55 fi 56 shift 57 trap 'rm -f ${TMPFILE} ; exit' 0 1 2 3 15 58 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}" 59 # ==> Added quotes (recommended in complex echoes). 60 echo binary >> ${TMPFILE} 61 for i in $* # ==> Parse command line args. 62 do 63 case $i in 64 -v) verbflag=-v; echo hash >> ${TMPFILE}; shift;; 65 -h) remhost=$2; shift 2;; 66 -d) echo cd $2 >> ${TMPFILE}; 67 if [ x${verbflag} != x ]; then 68 echo pwd >> ${TMPFILE}; 69 fi; 70 shift 2;; 71 -c) echo lcd $2 >> ${TMPFILE}; shift 2;; 72 -m) echo mget "$2" >> ${TMPFILE}; shift 2;; 73 -f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`; 74 echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;; 75 --) shift; break;; 76 esac 77 done 78 if [ $# -ne 0 ]; then 79 echo $usage 80 exit 65 # ==> Changed from "exit 2" to conform with standard. 81 fi 82 if [ x${verbflag} != x ]; then 83 ftpflags="${ftpflags} -v" 84 fi 85 if [ x${remhost} = x ]; then 86 remhost=prep.ai.mit.edu 87 # ==> Rewrite to match your favorite ftp site. 88 fi 89 echo quit >> ${TMPFILE} 90 # ==> All commands saved in tempfile. 91 92 ftp ${ftpflags} ${remhost} < ${TMPFILE} 93 # ==> Now, tempfile batch processed by ftp. 94 95 rm -f ${TMPFILE} 96 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile). 97 98 99 # ==> Exercises: 100 # ==> --------- 101 # ==> 1) Add error checking. 102 # ==> 2) Add bells & whistles. |
+
Antek Sawicki contributed the following script, which makes very clever use of the parameter substitution operators discussed in Section 9.3.
Example A-15. password: Generating random 8-character passwords
1 #!/bin/bash 2 # May need to be invoked with #!/bin/bash2 on older machines. 3 # 4 # Random password generator for bash 2.x by Antek Sawicki <tenox@tenox.tc>, 5 # who generously gave permission to the document author to use it here. 6 # 7 # ==> Comments added by document author ==> 8 9 10 MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 11 LENGTH="8" 12 # ==> May change 'LENGTH' for longer password, of course. 13 14 15 while [ "${n:=1}" -le "$LENGTH" ] 16 # ==> Recall that := is "default substitution" operator. 17 # ==> So, if 'n' has not been initialized, set it to 1. 18 do 19 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}" 20 # ==> Very clever, but tricky. 21 22 # ==> Starting from the innermost nesting... 23 # ==> ${#MATRIX} returns length of array MATRIX. 24 25 # ==> $RANDOM%${#MATRIX} returns random number between 1 26 # ==> and length of MATRIX - 1. 27 28 # ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1} 29 # ==> returns expansion of MATRIX at random position, by length 1. 30 # ==> See {var:pos:len} parameter substitution in Section 3.3.1 31 # ==> and following examples. 32 33 # ==> PASS=... simply pastes this result onto previous PASS (concatenation). 34 35 # ==> To visualize this more clearly, uncomment the following line 36 # ==> echo "$PASS" 37 # ==> to see PASS being built up, 38 # ==> one character at a time, each iteration of the loop. 39 40 let n+=1 41 # ==> Increment 'n' for next pass. 42 done 43 44 echo "$PASS" # ==> Or, redirect to file, as desired. 45 46 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 17 and 25 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. |
+
Jordi Sanfeliu gave permission to use his elegant tree script.
Example A-18. tree: Displaying a directory tree
1 #!/bin/sh 2 # @(#) tree 1.1 30/11/95 by Jordi Sanfeliu 3 # email: mikaku@fiwix.org 4 # 5 # Initial version: 1.0 30/11/95 6 # Next version : 1.1 24/02/97 Now, with symbolic links 7 # Patch by : Ian Kjos, to support unsearchable dirs 8 # email: beth13@mail.utexas.edu 9 # 10 # Tree is a tool for view the directory tree (obvious :-) ) 11 # 12 13 # ==> 'Tree' script used here with the permission of its author, Jordi Sanfeliu. 14 # ==> Comments added by the author of this document. 15 # ==> Argument quoting added. 16 17 18 search () { 19 for dir in `echo *` 20 # ==> `echo *` lists all the files in current working directory, without line breaks. 21 # ==> Similar effect to for dir in * 22 # ==> but "dir in `echo *`" will not handle filenames with blanks. 23 do 24 if [ -d "$dir" ] ; then # ==> If it is a directory (-d)... 25 zz=0 # ==> Temp variable, keeping track of directory level. 26 while [ $zz != $deep ] # Keep track of inner nested loop. 27 do 28 echo -n "| " # ==> Display vertical connector symbol, 29 # ==> with 2 spaces & no line feed in order to indent. 30 zz=`expr $zz + 1` # ==> Increment zz. 31 done 32 if [ -L "$dir" ] ; then # ==> If directory is a symbolic link... 33 echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'` 34 # ==> Display horiz. connector and list directory name, but... 35 # ==> delete date/time part of long listing. 36 else 37 echo "+---$dir" # ==> Display horizontal connector symbol... 38 # ==> and print directory name. 39 if cd "$dir" ; then # ==> If can move to subdirectory... 40 deep=`expr $deep + 1` # ==> Increment depth. 41 search # with recursivity ;-) 42 # ==> Function calls itself. 43 numdirs=`expr $numdirs + 1` # ==> Increment directory count. 44 fi 45 fi 46 fi 47 done 48 cd .. # ==> Up one directory level. 49 if [ "$deep" ] ; then # ==> If depth = 0 (returns TRUE)... 50 swfi=1 # ==> set flag showing that search is done. 51 fi 52 deep=`expr $deep - 1` # ==> Decrement depth. 53 } 54 55 # - Main - 56 if [ $# = 0 ] ; then 57 cd `pwd` # ==> No args to script, then use current working directory. 58 else 59 cd $1 # ==> Otherwise, move to indicated directory. 60 fi 61 echo "Initial directory = `pwd`" 62 swfi=0 # ==> Search finished flag. 63 deep=0 # ==> Depth of listing. 64 numdirs=0 65 zz=0 66 67 while [ "$swfi" != 1 ] # While flag not set... 68 do 69 search # ==> Call function after initializing variables. 70 done 71 echo "Total directories = $numdirs" 72 73 exit 0 74 # ==> Challenge: try to figure out exactly how this script works. |
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 |