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 15-20.
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 # Yes, this filename needs work. 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, v. 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 permissible
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" ] ||
50 [ "$year" -lt "$REFYR" ] && Param_Error
51 # Exit script on bad value(s).
52 # Uses or-list / and-list.
53 #
54 # Exercise: Implement more rigorous date checking.
55 }
56
57
58 strip_leading_zero () # Better to strip possible leading zero(s)
59 { #+ from day and/or month
60 return ${1#0} #+ since otherwise Bash will interpret them
61 } #+ as octal values (POSIX.2, sect 2.9.2.1).
62
63
64 day_index () # Gauss' Formula:
65 { # Days from March 1, 1600 to date passed as param.
66 # ^^^^^^^^^^^^^
67 day=$1
68 month=$2
69 year=$3
70
71 let "month = $month - 2"
72 if [ "$month" -le 0 ]
73 then
74 let "month += 12"
75 let "year -= 1"
76 fi
77
78 let "year -= $REFYR"
79 let "indexyr = $year / $CENTURY"
80
81
82 let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr \
83 + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM"
84 # For an in-depth explanation of this algorithm, see
85 #+ http://weblogs.asp.net/pgreborio/archive/2005/01/06/347968.aspx
86
87
88 echo $Days
89
90 }
91
92
93 calculate_difference () # Difference between two day indices.
94 {
95 let "diff = $1 - $2" # Global variable.
96 }
97
98
99 abs () # Absolute value
100 { # Uses global "value" variable.
101 if [ "$1" -lt 0 ] # If negative
102 then #+ then
103 let "value = 0 - $1" #+ change sign,
104 else #+ else
105 let "value = $1" #+ leave it alone.
106 fi
107 }
108
109
110
111 if [ $# -ne "$ARGS" ] # Require two command line params.
112 then
113 Param_Error
114 fi
115
116 Parse_Date $1
117 check_date $day $month $year # See if valid date.
118
119 strip_leading_zero $day # Remove any leading zeroes
120 day=$? #+ on day and/or month.
121 strip_leading_zero $month
122 month=$?
123
124 let "date1 = `day_index $day $month $year`"
125
126
127 Parse_Date $2
128 check_date $day $month $year
129
130 strip_leading_zero $day
131 day=$?
132 strip_leading_zero $month
133 month=$?
134
135 date2=$(day_index $day $month $year) # Command substitution.
136
137
138 calculate_difference $date1 $date2
139
140 abs $diff # Make sure it's positive.
141 diff=$value
142
143 echo $diff
144
145 exit 0
146
147 # Compare this script with
148 #+ the implementation of Gauss' Formula in a C program at:
149 #+ http://buschencrew.hypermart.net/software/datedif |
Example A-8. Making 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 startfile="$1"
35 fi
36
37 ############################################
38 # Abort script if "startfile" not specified
39 #+ AND
40 #+ "gen0" not present.
41
42 E_NOSTARTFILE=68
43
44 if [ ! -e "$startfile" ]
45 then
46 echo "Startfile \""$startfile"\" missing!"
47 exit $E_NOSTARTFILE
48 fi
49 ############################################
50
51
52 ALIVE1=.
53 DEAD1=_
54 # Represent living and "dead" cells in the start-up file.
55
56 # ---------------------------------------------------------- #
57 # This script uses a 10 x 10 grid (may be increased,
58 #+ but a large grid will will cause very slow execution).
59 ROWS=10
60 COLS=10
61 # Change above two variables to match grid size, if necessary.
62 # ---------------------------------------------------------- #
63
64 GENERATIONS=10 # How many generations to cycle through.
65 # Adjust this upwards,
66 #+ if you have time on your hands.
67
68 NONE_ALIVE=80 # Exit status on premature bailout,
69 #+ if no cells left alive.
70 TRUE=0
71 FALSE=1
72 ALIVE=0
73 DEAD=1
74
75 avar= # Global; holds current generation.
76 generation=0 # Initialize generation count.
77
78 # =================================================================
79
80
81 let "cells = $ROWS * $COLS"
82 # How many cells.
83
84 declare -a initial # Arrays containing "cells".
85 declare -a current
86
87 display ()
88 {
89
90 alive=0 # How many cells "alive" at any given time.
91 # Initially zero.
92
93 declare -a arr
94 arr=( `echo "$1"` ) # Convert passed arg to array.
95
96 element_count=${#arr[*]}
97
98 local i
99 local rowcheck
100
101 for ((i=0; i<$element_count; i++))
102 do
103
104 # Insert newline at end of each row.
105 let "rowcheck = $i % COLS"
106 if [ "$rowcheck" -eq 0 ]
107 then
108 echo # Newline.
109 echo -n " " # Indent.
110 fi
111
112 cell=${arr[i]}
113
114 if [ "$cell" = . ]
115 then
116 let "alive += 1"
117 fi
118
119 echo -n "$cell" | sed -e 's/_/ /g'
120 # Print out array and change underscores to spaces.
121 done
122
123 return
124
125 }
126
127 IsValid () # Test whether cell coordinate valid.
128 {
129
130 if [ -z "$1" -o -z "$2" ] # Mandatory arguments missing?
131 then
132 return $FALSE
133 fi
134
135 local row
136 local lower_limit=0 # Disallow negative coordinate.
137 local upper_limit
138 local left
139 local right
140
141 let "upper_limit = $ROWS * $COLS - 1" # Total number of cells.
142
143
144 if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ]
145 then
146 return $FALSE # Out of array bounds.
147 fi
148
149 row=$2
150 let "left = $row * $COLS" # Left limit.
151 let "right = $left + $COLS - 1" # Right limit.
152
153 if [ "$1" -lt "$left" -o "$1" -gt "$right" ]
154 then
155 return $FALSE # Beyond row boundary.
156 fi
157
158 return $TRUE # Valid coordinate.
159
160 }
161
162
163 IsAlive () # Test whether cell is alive.
164 # Takes array, cell number, state of cell as arguments.
165 {
166 GetCount "$1" $2 # Get alive cell count in neighborhood.
167 local nhbd=$?
168
169
170 if [ "$nhbd" -eq "$BIRTH" ] # Alive in any case.
171 then
172 return $ALIVE
173 fi
174
175 if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ]
176 then # Alive only if previously alive.
177 return $ALIVE
178 fi
179
180 return $DEAD # Default.
181
182 }
183
184
185 GetCount () # Count live cells in passed cell's neighborhood.
186 # Two arguments needed:
187 # $1) variable holding array
188 # $2) cell number
189 {
190 local cell_number=$2
191 local array
192 local top
193 local center
194 local bottom
195 local r
196 local row
197 local i
198 local t_top
199 local t_cen
200 local t_bot
201 local count=0
202 local ROW_NHBD=3
203
204 array=( `echo "$1"` )
205
206 let "top = $cell_number - $COLS - 1" # Set up cell neighborhood.
207 let "center = $cell_number - 1"
208 let "bottom = $cell_number + $COLS - 1"
209 let "r = $cell_number / $COLS"
210
211 for ((i=0; i<$ROW_NHBD; i++)) # Traverse from left to right.
212 do
213 let "t_top = $top + $i"
214 let "t_cen = $center + $i"
215 let "t_bot = $bottom + $i"
216
217
218 let "row = $r" # Count center row of neighborhood.
219 IsValid $t_cen $row # Valid cell position?
220 if [ $? -eq "$TRUE" ]
221 then
222 if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive?
223 then # Yes?
224 let "count += 1" # Increment count.
225 fi
226 fi
227
228 let "row = $r - 1" # Count top row.
229 IsValid $t_top $row
230 if [ $? -eq "$TRUE" ]
231 then
232 if [ ${array[$t_top]} = "$ALIVE1" ]
233 then
234 let "count += 1"
235 fi
236 fi
237
238 let "row = $r + 1" # Count bottom row.
239 IsValid $t_bot $row
240 if [ $? -eq "$TRUE" ]
241 then
242 if [ ${array[$t_bot]} = "$ALIVE1" ]
243 then
244 let "count += 1"
245 fi
246 fi
247
248 done
249
250
251 if [ ${array[$cell_number]} = "$ALIVE1" ]
252 then
253 let "count -= 1" # Make sure value of tested cell itself
254 fi #+ is not counted.
255
256
257 return $count
258
259 }
260
261 next_gen () # Update generation array.
262 {
263
264 local array
265 local i=0
266
267 array=( `echo "$1"` ) # Convert passed arg to array.
268
269 while [ "$i" -lt "$cells" ]
270 do
271 IsAlive "$1" $i ${array[$i]} # Is cell alive?
272 if [ $? -eq "$ALIVE" ]
273 then # If alive, then
274 array[$i]=. #+ represent the cell as a period.
275 else
276 array[$i]="_" # Otherwise underscore
277 fi #+ (which will later be converted to space).
278 let "i += 1"
279 done
280
281
282 # let "generation += 1" # Increment generation count.
283 # Why was the above line commented out?
284
285
286 # Set variable to pass as parameter to "display" function.
287 avar=`echo ${array[@]}` # Convert array back to string variable.
288 display "$avar" # Display it.
289 echo; echo
290 echo "Generation $generation - $alive alive"
291
292 if [ "$alive" -eq 0 ]
293 then
294 echo
295 echo "Premature exit: no more cells alive!"
296 exit $NONE_ALIVE # No point in continuing
297 fi #+ if no live cells.
298
299 }
300
301
302 # =========================================================
303
304 # main ()
305
306 # Load initial array with contents of startup file.
307 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\
308 sed -e 's/\./\. /g' -e 's/_/_ /g'` )
309 # Delete lines containing '#' comment character.
310 # Remove linefeeds and insert space between elements.
311
312 clear # Clear screen.
313
314 echo # Title
315 echo "======================="
316 echo " $GENERATIONS generations"
317 echo " of"
318 echo "\"Life in the Slow Lane\""
319 echo "======================="
320
321
322 # -------- Display first generation. --------
323 Gen0=`echo ${initial[@]}`
324 display "$Gen0" # Display only.
325 echo; echo
326 echo "Generation $generation - $alive alive"
327 # -------------------------------------------
328
329
330 let "generation += 1" # Increment generation count.
331 echo
332
333 # ------- Display second generation. -------
334 Cur=`echo ${initial[@]}`
335 next_gen "$Cur" # Update & display.
336 # ------------------------------------------
337
338 let "generation += 1" # Increment generation count.
339
340 # ------ Main loop for displaying subsequent generations ------
341 while [ "$generation" -le "$GENERATIONS" ]
342 do
343 Cur="$avar"
344 next_gen "$Cur"
345 let "generation += 1"
346 done
347 # ==============================================================
348
349 echo
350
351 exit 0 # END
352
353
354
355 # The grid in this script has a "boundary problem."
356 # The the top, bottom, and sides border on a void of dead cells.
357 # Exercise: Change the script to have the grid wrap around,
358 # + so that the left and right sides will "touch,"
359 # + as will the top and bottom.
360 #
361 # Exercise: Create a new "gen0" file to seed this script.
362 # Use a 12 x 16 grid, instead of the original 10 x 10 one.
363 # Make the necessary changes to the script,
364 #+ so it will run with the altered file.
365 #
366 # Exercise: Modify this script so that it can determine the grid size
367 #+ from the "gen0" file, and set any variables necessary
368 #+ for the script to run.
369 # This would make unnecessary any changes to variables
370 #+ in the script for an altered grid size. |
Example A-11. Data file for "Game of Life"
1 # gen0 2 # 3 # This is an example "generation 0" start-up file for "life.sh". 4 # -------------------------------------------------------------- 5 # The "gen0" file is a 10 x 10 grid using a period (.) for live cells, 6 #+ and an underscore (_) for dead ones. We cannot simply use spaces 7 #+ for dead cells in this file because of a peculiarity in Bash arrays. 8 # [Exercise for the reader: explain this.] 9 # 10 # Lines beginning with a '#' are comments, and the script ignores them. 11 __.__..___ 12 ___._.____ 13 ____.___.. 14 _._______. 15 ____._____ 16 ..__...___ 17 ____._____ 18 ___...____ 19 __.._..___ 20 _..___..__ |
+++
The following two scripts are by Mark Moraes of the University of Toronto. See the file Moraes-COPYRIGHT for permissions and restrictions. This file is included in the combined HTML/source tarball of the ABS Guide.
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
23 # in 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]... \
49 [-f remfile:localfile]... [-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 # ==> Signals: HUP INT (Ctl-C) QUIT TERM
61 # ==> Delete tempfile in case of abnormal exit from script.
62 echo "user anonymous ${USER-gnu}@${SITE} > ${TMPFILE}"
63 # ==> Added quotes (recommended in complex echoes).
64 echo binary >> ${TMPFILE}
65 for i in $* # ==> Parse command line args.
66 do
67 case $i in
68 -v) verbflag=-v; echo hash >> ${TMPFILE}; shift;;
69 -h) remhost=$2; shift 2;;
70 -d) echo cd $2 >> ${TMPFILE};
71 if [ x${verbflag} != x ]; then
72 echo pwd >> ${TMPFILE};
73 fi;
74 shift 2;;
75 -c) echo lcd $2 >> ${TMPFILE}; shift 2;;
76 -m) echo mget "$2" >> ${TMPFILE}; shift 2;;
77 -f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
78 echo get ${f1} ${f2} >> ${TMPFILE}; shift 2;;
79 --) shift; break;;
80 esac
81 # ==> 'lcd' and 'mget' are ftp commands. See "man ftp" . . .
82 done
83 if [ $# -ne 0 ]; then
84 echo $usage
85 exit $E_BADARGS
86 # ==> Changed from "exit 2" to conform with style standard.
87 fi
88 if [ x${verbflag} != x ]; then
89 ftpflags="${ftpflags} -v"
90 fi
91 if [ x${remhost} = x ]; then
92 remhost=prep.ai.mit.edu
93 # ==> Change to match appropriate ftp site.
94 fi
95 echo quit >> ${TMPFILE}
96 # ==> All commands saved in tempfile.
97
98 ftp ${ftpflags} ${remhost} < ${TMPFILE}
99 # ==> Now, tempfile batch processed by ftp.
100
101 rm -f ${TMPFILE}
102 # ==> Finally, tempfile deleted (you may wish to copy it to a logfile).
103
104
105 # ==> Exercises:
106 # ==> ---------
107 # ==> 1) Add error checking.
108 # ==> 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 +
5 #+ by Antek Sawicki <tenox@tenox.tc>,
6 #+ who generously gave usage permission to the ABS Guide author.
7 #
8 # ==> Comments added by document author ==>
9
10
11 MATRIX="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
12 # ==> Password will consist of alphanumeric characters.
13 LENGTH="8"
14 # ==> May change 'LENGTH' for longer password.
15
16
17 while [ "${n:=1}" -le "$LENGTH" ]
18 # ==> Recall that := is "default substitution" operator.
19 # ==> So, if 'n' has not been initialized, set it to 1.
20 do
21 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
22 # ==> Very clever, but tricky.
23
24 # ==> Starting from the innermost nesting...
25 # ==> ${#MATRIX} returns length of array MATRIX.
26
27 # ==> $RANDOM%${#MATRIX} returns random number between 1
28 # ==> and [length of MATRIX] - 1.
29
30 # ==> ${MATRIX:$(($RANDOM%${#MATRIX})):1}
31 # ==> returns expansion of MATRIX at random position, by length 1.
32 # ==> See {var:pos:len} parameter substitution in Chapter 9.
33 # ==> and the associated examples.
34
35 # ==> PASS=... simply pastes this result onto previous PASS (concatenation).
36
37 # ==> To visualize this more clearly, uncomment the following line
38 # echo "$PASS"
39 # ==> to see PASS being built up,
40 # ==> one character at a time, each iteration of the loop.
41
42 let n+=1
43 # ==> Increment 'n' for next pass.
44 done
45
46 echo "$PASS" # ==> Or, redirect to a file, as desired.
47
48 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 # ==> Is it necessary to delete the pipe before exiting the script?
31 # ==> How could that be done?
32
33
34 exit 0 |
+
Stéphane Chazelas contributed the following script to demonstrate that generating prime numbers does not require arrays.
Example A-16. Generating prime numbers using the modulo operator
1 #!/bin/bash
2 # primes.sh: Generate prime numbers, without using arrays.
3 # Script contributed by Stephane Chazelas.
4
5 # This does *not* use the classic "Sieve of Eratosthenes" algorithm,
6 #+ but instead uses the more intuitive method of testing each candidate number
7 #+ for factors (divisors), using the "%" modulo operator.
8
9
10 LIMIT=1000 # Primes 2 - 1000
11
12 Primes()
13 {
14 (( n = $1 + 1 )) # Bump to next integer.
15 shift # Next parameter in list.
16 # echo "_n=$n i=$i_"
17
18 if (( n == LIMIT ))
19 then echo $*
20 return
21 fi
22
23 for i; do # "i" gets set to "@", previous values of $n.
24 # echo "-n=$n i=$i-"
25 (( i * i > n )) && break # Optimization.
26 (( n % i )) && continue # Sift out non-primes using modulo operator.
27 Primes $n $@ # Recursion inside loop.
28 return
29 done
30
31 Primes $n $@ $n # Recursion outside loop.
32 # Successively accumulate positional parameters.
33 # "$@" is the accumulating list of primes.
34 }
35
36 Primes 1
37
38 exit 0
39
40 # Uncomment lines 16 and 24 to help figure out what is going on.
41
42 # Compare the speed of this algorithm for generating primes
43 #+ with the Sieve of Eratosthenes (ex68.sh).
44
45 # Exercise: Rewrite this script without recursion, for faster execution. |
+
This is Rick Boivie's revision of Jordi Sanfeliu's tree script.
Example A-17. tree: Displaying a directory tree
1 #!/bin/bash
2 # tree.sh
3
4 # Written by Rick Boivie.
5 # Used with permission.
6 # This is a revised and simplified version of a script
7 #+ by Jordi Sanfeliu (and patched by Ian Kjos).
8 # This script replaces the earlier version used in
9 #+ previous releases of the Advanced Bash Scripting Guide.
10
11 # ==> Comments added by the author of this document.
12
13
14 search () {
15 for dir in `echo *`
16 # ==> `echo *` lists all the files in current working directory,
17 #+ ==> without line breaks.
18 # ==> Similar effect to for dir in *
19 # ==> but "dir in `echo *`" will not handle filenames with blanks.
20 do
21 if [ -d "$dir" ] ; then # ==> If it is a directory (-d)...
22 zz=0 # ==> Temp variable, keeping track of directory level.
23 while [ $zz != $1 ] # Keep track of inner nested loop.
24 do
25 echo -n "| " # ==> Display vertical connector symbol,
26 # ==> with 2 spaces & no line feed in order to indent.
27 zz=`expr $zz + 1` # ==> Increment zz.
28 done
29
30 if [ -L "$dir" ] ; then # ==> If directory is a symbolic link...
31 echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'`
32 # ==> Display horiz. connector and list directory name, but...
33 # ==> delete date/time part of long listing.
34 else
35 echo "+---$dir" # ==> Display horizontal connector symbol...
36 # ==> and print directory name.
37 numdirs=`expr $numdirs + 1` # ==> Increment directory count.
38 if cd "$dir" ; then # ==> If can move to subdirectory...
39 search `expr $1 + 1` # with recursion ;-)
40 # ==> Function calls itself.
41 cd ..
42 fi
43 fi
44 fi
45 done
46 }
47
48 if [ $# != 0 ] ; then
49 cd $1 # move to indicated directory.
50 #else # stay in current directory
51 fi
52
53 echo "Initial directory = `pwd`"
54 numdirs=0
55
56 search 0
57 echo "Total directories = $numdirs"
58
59 exit 0 |
Noah Friedman gave permission to use his string function script, which essentially reproduces some of the C-library string manipulation functions.
Example A-18. string functions: C-style 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-19. 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 |
Stéphane Chazelas demonstrates object-oriented programming in a Bash script.
Example A-20. Object-oriented database
1 #!/bin/bash
2 # obj-oriented.sh: Object-oriented programming in a shell script.
3 # Script by Stephane Chazelas.
4
5 # Important Note:
6 # --------- ----
7 # If running this script under version 3 or later of Bash,
8 #+ replace all periods in function names with a "legal" character,
9 #+ for example, an underscore.
10
11
12 person.new() # Looks almost like a class declaration in C++.
13 {
14 local obj_name=$1 name=$2 firstname=$3 birthdate=$4
15
16 eval "$obj_name.set_name() {
17 eval \"$obj_name.get_name() {
18 echo \$1
19 }\"
20 }"
21
22 eval "$obj_name.set_firstname() {
23 eval \"$obj_name.get_firstname() {
24 echo \$1
25 }\"
26 }"
27
28 eval "$obj_name.set_birthdate() {
29 eval \"$obj_name.get_birthdate() {
30 echo \$1
31 }\"
32 eval \"$obj_name.show_birthdate() {
33 echo \$(date -d \"1/1/1970 0:0:\$1 GMT\")
34 }\"
35 eval \"$obj_name.get_age() {
36 echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 ))
37 }\"
38 }"
39
40 $obj_name.set_name $name
41 $obj_name.set_firstname $firstname
42 $obj_name.set_birthdate $birthdate
43 }
44
45 echo
46
47 person.new self Bozeman Bozo 101272413
48 # Create an instance of "person.new" (actually passing args to the function).
49
50 self.get_firstname # Bozo
51 self.get_name # Bozeman
52 self.get_age # 28
53 self.get_birthdate # 101272413
54 self.show_birthdate # Sat Mar 17 20:13:33 MST 1973
55
56 echo
57
58 # typeset -f
59 #+ to see the created functions (careful, it scrolls off the page).
60
61 exit 0 |
Mariusz Gniazdowski contributes a hash library for use in scripts.
Example A-21. Library of hash functions
1 # Hash:
2 # Hash function library
3 # Author: Mariusz Gniazdowski <mgniazd-at-gmail.com>
4 # Date: 2005-04-07
5
6 # Functions making emulating hashes in Bash a little less painful.
7
8
9 # Limitations:
10 # * Only global variables are supported.
11 # * Each hash instance generates one global variable per value.
12 # * Variable names collisions are possible
13 #+ if you define variable like __hash__hashname_key
14 # * Keys must use chars that can be part of a Bash variable name
15 #+ (no dashes, periods, etc.).
16 # * The hash is created as a variable:
17 # ... hashname_keyname
18 # So if somone will create hashes like:
19 # myhash_ + mykey = myhash__mykey
20 # myhash + _mykey = myhash__mykey
21 # Then there will be a collision.
22 # (This should not pose a major problem.)
23
24
25 Hash_config_varname_prefix=__hash__
26
27
28 # Emulates: hash[key]=value
29 #
30 # Params:
31 # 1 - hash
32 # 2 - key
33 # 3 - value
34 function hash_set {
35 eval "${Hash_config_varname_prefix}${1}_${2}=\"${3}\""
36 }
37
38
39 # Emulates: value=hash[key]
40 #
41 # Params:
42 # 1 - hash
43 # 2 - key
44 # 3 - value (name of global variable to set)
45 function hash_get_into {
46 eval "$3=\"\$${Hash_config_varname_prefix}${1}_${2}\""
47 }
48
49
50 # Emulates: echo hash[key]
51 #
52 # Params:
53 # 1 - hash
54 # 2 - key
55 # 3 - echo params (like -n, for example)
56 function hash_echo {
57 eval "echo $3 \"\$${Hash_config_varname_prefix}${1}_${2}\""
58 }
59
60
61 # Emulates: hash1[key1]=hash2[key2]
62 #
63 # Params:
64 # 1 - hash1
65 # 2 - key1
66 # 3 - hash2
67 # 4 - key2
68 function hash_copy {
69 eval "${Hash_config_varname_prefix}${1}_${2}\
70 =\"\$${Hash_config_varname_prefix}${3}_${4}\""
71 }
72
73
74 # Emulates: hash[keyN-1]=hash[key2]=...hash[key1]
75 #
76 # Copies first key to rest of keys.
77 #
78 # Params:
79 # 1 - hash1
80 # 2 - key1
81 # 3 - key2
82 # . . .
83 # N - keyN
84 function hash_dup {
85 local hashName="$1" keyName="$2"
86 shift 2
87 until [ ${#} -le 0 ]; do
88 eval "${Hash_config_varname_prefix}${hashName}_${1}\
89 =\"\$${Hash_config_varname_prefix}${hashName}_${keyName}\""
90 shift;
91 done;
92 }
93
94
95 # Emulates: unset hash[key]
96 #
97 # Params:
98 # 1 - hash
99 # 2 - key
100 function hash_unset {
101 eval "unset ${Hash_config_varname_prefix}${1}_${2}"
102 }
103
104
105 # Emulates something similar to: ref=&hash[key]
106 #
107 # The reference is name of the variable in which value is held.
108 #
109 # Params:
110 # 1 - hash
111 # 2 - key
112 # 3 - ref - Name of global variable to set.
113 function hash_get_ref_into {
114 eval "$3=\"${Hash_config_varname_prefix}${1}_${2}\""
115 }
116
117
118 # Emulates something similar to: echo &hash[key]
119 #
120 # That reference is name of variable in which value is held.
121 #
122 # Params:
123 # 1 - hash
124 # 2 - key
125 # 3 - echo params (like -n for example)
126 function hash_echo_ref {
127 eval "echo $3 \"${Hash_config_varname_prefix}${1}_${2}\""
128 }
129
130
131
132 # Emulates something similar to: $$hash[key](param1, param2, ...)
133 #
134 # Params:
135 # 1 - hash
136 # 2 - key
137 # 3,4, ... - Function parameters
138 function hash_call {
139 local hash key
140 hash=$1
141 key=$2
142 shift 2
143 eval "eval \"\$${Hash_config_varname_prefix}${hash}_${key} \\\"\\\$@\\\"\""
144 }
145
146
147 # Emulates something similar to: isset(hash[key]) or hash[key]==NULL
148 #
149 # Params:
150 # 1 - hash
151 # 2 - key
152 # Returns:
153 # 0 - there is such key
154 # 1 - there is no such key
155 function hash_is_set {
156 eval "if [[ \"\${${Hash_config_varname_prefix}${1}_${2}-a}\" = \"a\" &&
157 \"\${${Hash_config_varname_prefix}${1}_${2}-b}\" = \"b\" ]]
158 then return 1; else return 0; fi"
159 }
160
161
162 # Emulates something similar to:
163 # foreach($hash as $key => $value) { fun($key,$value); }
164 #
165 # It is possible to write different variations of this function.
166 # Here we use a function call to make it as "generic" as possible.
167 #
168 # Params:
169 # 1 - hash
170 # 2 - function name
171 function hash_foreach {
172 local keyname oldIFS="$IFS"
173 IFS=' '
174 for i in $(eval "echo \${!${Hash_config_varname_prefix}${1}_*}"); do
175 keyname=$(eval "echo \${i##${Hash_config_varname_prefix}${1}_}")
176 eval "$2 $keyname \"\$$i\""
177 done
178 IFS="$oldIFS"
179 }
180
181 # NOTE: In lines 103 and 116, ampersand changed.
182 # But, it doesn't matter, because these are comment lines anyhow. |
Here is an example script using the foregoing hash library.
Example A-22. Colorizing text using hash functions
1 #!/bin/bash
2 # hash-example.sh: Colorizing text.
3 # Author: Mariusz Gniazdowski <mgniazd-at-gmail.com>
4
5 . Hash.lib # Load the library of functions.
6
7 hash_set colors red "\033[0;31m"
8 hash_set colors blue "\033[0;34m"
9 hash_set colors light_blue "\033[1;34m"
10 hash_set colors light_red "\033[1;31m"
11 hash_set colors cyan "\033[0;36m"
12 hash_set colors light_green "\033[1;32m"
13 hash_set colors light_gray "\033[0;37m"
14 hash_set colors green "\033[0;32m"
15 hash_set colors yellow "\033[1;33m"
16 hash_set colors light_purple "\033[1;35m"
17 hash_set colors purple "\033[0;35m"
18 hash_set colors reset_color "\033[0;00m"
19
20
21 # $1 - keyname
22 # $2 - value
23 try_colors() {
24 echo -en "$2"
25 echo "This line is $1."
26 }
27 hash_foreach colors try_colors
28 hash_echo colors reset_color -en
29
30 echo -e '\nLet us overwrite some colors with yellow.\n'
31 # It's hard to read yellow text on some terminals.
32 hash_dup colors yellow red light_green blue green light_gray cyan
33 hash_foreach colors try_colors
34 hash_echo colors reset_color -en
35
36 echo -e '\nLet us delete them and try colors once more . . .\n'
37
38 for i in red light_green blue green light_gray cyan; do
39 hash_unset colors $i
40 done
41 hash_foreach colors try_colors
42 hash_echo colors reset_color -en
43
44 hash_set other txt "Other examples . . ."
45 hash_echo other txt
46 hash_get_into other txt text
47 echo $text
48
49 hash_set other my_fun try_colors
50 hash_call other my_fun purple "`hash_echo colors purple`"
51 hash_echo colors reset_color -en
52
53 echo; echo "Back to normal?"; echo
54
55 exit $?
56
57 # On some terminals, the "light" colors print in bold,
58 # and end up looking darker than the normal ones.
59 # Why is this?
60 |
An example illustrating the mechanics of hashing, but from a different point of view.
Example A-23. More on hash functions
1 #!/bin/bash
2 # $Id: ha.sh,v 1.2 2005/04/21 23:24:26 oliver Exp $
3 # Copyright 2005 Oliver Beckstein
4 # Released under the GNU Public License
5 # Author of script granted permission for inclusion in ABS Guide.
6 # (Thank you!)
7
8 #----------------------------------------------------------------
9 # pseudo hash based on indirect parameter expansion
10 # API: access through functions:
11 #
12 # create the hash:
13 #
14 # newhash Lovers
15 #
16 # add entries (note single quotes for spaces)
17 #
18 # addhash Lovers Tristan Isolde
19 # addhash Lovers 'Romeo Montague' 'Juliet Capulet'
20 #
21 # access value by key
22 #
23 # gethash Lovers Tristan ----> Isolde
24 #
25 # show all keys
26 #
27 # keyshash Lovers ----> 'Tristan' 'Romeo Montague'
28 #
29 #
30 # Convention: instead of perls' foo{bar} = boing' syntax,
31 # use
32 # '_foo_bar=boing' (two underscores, no spaces)
33 #
34 # 1) store key in _NAME_keys[]
35 # 2) store value in _NAME_values[] using the same integer index
36 # The integer index for the last entry is _NAME_ptr
37 #
38 # NOTE: No error or sanity checks, just bare bones.
39
40
41 function _inihash () {
42 # private function
43 # call at the beginning of each procedure
44 # defines: _keys _values _ptr
45 #
46 # usage: _inihash NAME
47 local name=$1
48 _keys=_${name}_keys
49 _values=_${name}_values
50 _ptr=_${name}_ptr
51 }
52
53 function newhash () {
54 # usage: newhash NAME
55 # NAME should not contain spaces or '.'
56 # Actually: it must be a legal name for a Bash variable.
57 # We rely on Bash automatically recognising arrays.
58 local name=$1
59 local _keys _values _ptr
60 _inihash ${name}
61 eval ${_ptr}=0
62 }
63
64
65 function addhash () {
66 # usage: addhash NAME KEY 'VALUE with spaces'
67 # arguments with spaces need to be quoted with single quotes ''
68 local name=$1 k="$2" v="$3"
69 local _keys _values _ptr
70 _inihash ${name}
71
72 #echo "DEBUG(addhash): ${_ptr}=${!_ptr}"
73
74 eval let ${_ptr}=${_ptr}+1
75 eval "$_keys[${!_ptr}]=\"${k}\""
76 eval "$_values[${!_ptr}]=\"${v}\""
77 }
78
79 function gethash () {
80 # usage: gethash NAME KEY
81 # returns boing
82 # ERR=0 if entry found, 1 otherwise
83 # That's not a proper hash --
84 #+ we simply linearly search through the keys.
85 local name=$1 key="$2"
86 local _keys _values _ptr
87 local k v i found h
88 _inihash ${name}
89
90 # _ptr holds the highest index in the hash
91 found=0
92
93 for i in $(seq 1 ${!_ptr}); do
94 h="\${${_keys}[${i}]}" # safer to do it in two steps
95 eval k=${h} # (especially when quoting for spaces)
96 if [ "${k}" = "${key}" ]; then found=1; break; fi
97 done;
98
99 [ ${found} = 0 ] && return 1;
100 # else: i is the index that matches the key
101 h="\${${_values}[${i}]}"
102 eval echo "${h}"
103 return 0;
104 }
105
106 function keyshash () {
107 # usage: keyshash NAME
108 # returns list of all keys defined for hash name
109 local name=$1 key="$2"
110 local _keys _values _ptr
111 local k i h
112 _inihash ${name}
113
114 # _ptr holds the highest index in the hash
115 for i in $(seq 1 ${!_ptr}); do
116 h="\${${_keys}[${i}]}" # Safer to do it in two steps
117 eval k=${h} # (especially when quoting for spaces)
118 echo -n "'${k}' "
119 done;
120 }
121
122
123 # -----------------------------------------------------------------------
124
125 # Now, let's test it.
126 # (Per comments at the beginning of the script.)
127 newhash Lovers
128 addhash Lovers Tristan Isolde
129 addhash Lovers 'Romeo Montague' 'Juliet Capulet'
130
131 # Output results.
132 echo
133 gethash Lovers Tristan # Isolde
134 echo
135 keyshash Lovers # 'Tristan' 'Romeo Montague'
136 echo; echo
137
138
139 exit 0
140
141 # Exercise:
142 # --------
143
144 # Add error checks to the functions. |
Now for a script that installs and mounts those cute USB keychain solid-state "hard drives."
Example A-24. Mounting USB keychain storage devices
1 #!/bin/bash
2 # ==> usb.sh
3 # ==> Script for mounting and installing pen/keychain USB storage devices.
4 # ==> Runs as root at system startup (see below).
5 # ==>
6 # ==> Newer Linux distros (2004 or later) autodetect
7 # ==> and install USB pen drives, and therefore don't need this script.
8 # ==> But, it's still instructive.
9
10 # This code is free software covered by GNU GPL license version 2 or above.
11 # Please refer to http://www.gnu.org/ for the full license text.
12 #
13 # Some code lifted from usb-mount by Michael Hamilton's usb-mount (LGPL)
14 #+ see http://users.actrix.co.nz/michael/usbmount.html
15 #
16 # INSTALL
17 # -------
18 # Put this in /etc/hotplug/usb/diskonkey.
19 # Then look in /etc/hotplug/usb.distmap, and copy all usb-storage entries
20 #+ into /etc/hotplug/usb.usermap, substituting "usb-storage" for "diskonkey".
21 # Otherwise this code is only run during the kernel module invocation/removal
22 #+ (at least in my tests), which defeats the purpose.
23 #
24 # TODO
25 # ----
26 # Handle more than one diskonkey device at one time (e.g. /dev/diskonkey1
27 #+ and /mnt/diskonkey1), etc. The biggest problem here is the handling in
28 #+ devlabel, which I haven't yet tried.
29 #
30 # AUTHOR and SUPPORT
31 # ------------------
32 # Konstantin Riabitsev, <icon linux duke edu>.
33 # Send any problem reports to my email address at the moment.
34 #
35 # ==> Comments added by ABS Guide author.
36
37
38
39 SYMLINKDEV=/dev/diskonkey
40 MOUNTPOINT=/mnt/diskonkey
41 DEVLABEL=/sbin/devlabel
42 DEVLABELCONFIG=/etc/sysconfig/devlabel
43 IAM=$0
44
45 ##
46 # Functions lifted near-verbatim from usb-mount code.
47 #
48 function allAttachedScsiUsb {
49 find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f |
50 xargs grep -l 'Attached: Yes'
51 }
52 function scsiDevFromScsiUsb {
53 echo $1 | awk -F"[-/]" '{ n=$(NF-1);
54 print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }'
55 }
56
57 if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
58 ##
59 # lifted from usbcam code.
60 #
61 if [ -f /var/run/console.lock ]; then
62 CONSOLEOWNER=`cat /var/run/console.lock`
63 elif [ -f /var/lock/console.lock ]; then
64 CONSOLEOWNER=`cat /var/lock/console.lock`
65 else
66 CONSOLEOWNER=
67 fi
68 for procEntry in $(allAttachedScsiUsb); do
69 scsiDev=$(scsiDevFromScsiUsb $procEntry)
70 # Some bug with usb-storage?
71 # Partitions are not in /proc/partitions until they are accessed
72 #+ somehow.
73 /sbin/fdisk -l $scsiDev >/dev/null
74 ##
75 # Most devices have partitioning info, so the data would be on
76 #+ /dev/sd?1. However, some stupider ones don't have any partitioning
77 #+ and use the entire device for data storage. This tries to
78 #+ guess semi-intelligently if we have a /dev/sd?1 and if not, then
79 #+ it uses the entire device and hopes for the better.
80 #
81 if grep -q `basename $scsiDev`1 /proc/partitions; then
82 part="$scsiDev""1"
83 else
84 part=$scsiDev
85 fi
86 ##
87 # Change ownership of the partition to the console user so they can
88 #+ mount it.
89 #
90 if [ ! -z "$CONSOLEOWNER" ]; then
91 chown $CONSOLEOWNER:disk $part
92 fi
93 ##
94 # This checks if we already have this UUID defined with devlabel.
95 # If not, it then adds the device to the list.
96 #
97 prodid=`$DEVLABEL printid -d $part`
98 if ! grep -q $prodid $DEVLABELCONFIG; then
99 # cross our fingers and hope it works
100 $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null
101 fi
102 ##
103 # Check if the mount point exists and create if it doesn't.
104 #
105 if [ ! -e $MOUNTPOINT ]; then
106 mkdir -p $MOUNTPOINT
107 fi
108 ##
109 # Take care of /etc/fstab so mounting is easy.
110 #
111 if ! grep -q "^$SYMLINKDEV" /etc/fstab; then
112 # Add an fstab entry
113 echo -e \
114 "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \
115 >> /etc/fstab
116 fi
117 done
118 if [ ! -z "$REMOVER" ]; then
119 ##
120 # Make sure this script is triggered on device removal.
121 #
122 mkdir -p `dirname $REMOVER`
123 ln -s $IAM $REMOVER
124 fi
125 elif [ "${ACTION}" = "remove" ]; then
126 ##
127 # If the device is mounted, unmount it cleanly.
128 #
129 if grep -q "$MOUNTPOINT" /etc/mtab; then
130 # unmount cleanly
131 umount -l $MOUNTPOINT
132 fi
133 ##
134 # Remove it from /etc/fstab if it's there.
135 #
136 if grep -q "^$SYMLINKDEV" /etc/fstab; then
137 grep -v "^$SYMLINKDEV" /etc/fstab > /etc/.fstab.new
138 mv -f /etc/.fstab.new /etc/fstab
139 fi
140 fi
141
142 exit 0 |
A script that converts a text file to HTML format.
Example A-25. Converting to HTML
1 #!/bin/bash
2 # tohtml.sh
3
4 # Convert a text file to HTML format.
5 # Author: Mendel Cooper
6 # License: GPL3
7 # Usage: sh tohtml.sh < textfile > htmlfile
8 # Script can easily be modified to accept source and target filenames.
9
10 # Assumptions:
11 # 1) Paragraphs in (target) text file are separated by a blank line.
12 # 2) Jpeg images (*.jpg) are located in "images" subdirectory.
13 # 3) Emphasized (italic) phrases begin with a space+underscore
14 #+ or are the first character on the line,
15 #+ and end with an underscore+space or underscore+end-of-line.
16
17
18 # Settings
19 FNTSIZE=2 # Small-medium font size
20 IMGDIR="images" # Image directory
21 # Headers
22 HDR01='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
23 HDR02='<!-- Converted to HTML by ***tohtml.sh*** script -->'
24 HDR03='<!-- script author: M. Leo Cooper <thegrendel@theriver.com> -->'
25 HDR10='<html>'
26 HDR11='<head>'
27 HDR11a='</head>'
28 HDR12a='<title>'
29 HDR12b='</title>'
30 HDR121='<META NAME="GENERATOR" CONTENT="tohtml.sh script">'
31 HDR13='<body bgcolor="#dddddd">' # Change background color to suit.
32 HDR14a='<font size='
33 HDR14b='>'
34 # Footers
35 FTR10='</body>'
36 FTR11='</html>'
37 # Tags
38 BOLD="<b>"
39 CENTER="<center>"
40 END_CENTER="</center>"
41 LF="<br>"
42
43
44 write_headers ()
45 {
46 echo "$HDR01"
47 echo
48 echo "$HDR02"
49 echo "$HDR03"
50 echo
51 echo
52 echo "$HDR10"
53 echo "$HDR11"
54 echo "$HDR121"
55 echo "$HDR11a"
56 echo "$HDR13"
57 echo
58 echo -n "$HDR14a"
59 echo -n "$FNTSIZE"
60 echo "$HDR14b"
61 echo
62 echo "$BOLD" # Everything in bold (more easily readable).
63 }
64
65
66 process_text ()
67 {
68 while read line # Read one line at a time.
69 do
70 {
71 if [ ! "$line" ] # Blank line?
72 then # Then new paragraph must follow.
73 echo
74 echo "$LF" # Insert two <br> tags.
75 echo "$LF"
76 echo
77 continue # Skip the underscore test.
78 else # Otherwise . . .
79
80 if [[ "$line" =~ "\[*jpg\]" ]] # Is a graphic?
81 then # Strip away brackets.
82 temp=$( echo "$line" | sed -e 's/\[//' -e 's/\]//' )
83 line=""$CENTER" <img src="\"$IMGDIR"/$temp\"> "$END_CENTER" "
84 # Add image tag.
85 # And, center it.
86 fi
87
88 fi
89
90
91 echo "$line" | grep -q _
92 if [ "$?" -eq 0 ] # If line contains underscore ...
93 then
94 # ===================================================
95 # Convert underscored phrase to italics.
96 temp=$( echo "$line" |
97 sed -e 's/ _/ <i>/' -e 's/_ /<\/i> /' |
98 sed -e 's/^_/<i>/' -e 's/_$/<\/i>/' )
99 # Process only underscores prefixed by space,
100 #+ followed by space, or at beginning or end of line.
101 # Do not convert underscores embedded within a word!
102 line="$temp"
103 # Slows script execution. Can be optimized?
104 # ===================================================
105 fi
106
107
108
109 echo
110 echo "$line"
111 echo
112 } # End while
113 done
114 } # End process_text ()
115
116
117 write_footers () # Termination tags.
118 {
119 echo "$FTR10"
120 echo "$FTR11"
121 }
122
123
124 # main () {
125 # =========
126 write_headers
127 process_text
128 write_footers
129 # =========
130 # }
131
132 exit $?
133
134 # Exercises:
135 # ---------
136 # 1) Fixup: Check for closing underscore before a comma or period.
137 # 2) Add a test for the presence of a closing underscore
138 #+ in phrases to be italicized. |
Here is something to warm the hearts of webmasters and mistresses everywhere: a script that saves weblogs.
Example A-26. Preserving weblogs
1 #!/bin/bash 2 # archiveweblogs.sh v1.0 3 4 # Troy Engel <tengel@fluid.com> 5 # Slightly modified by document author. 6 # Used with permission. 7 # 8 # This script will preserve the normally rotated and 9 #+ thrown away weblogs from a default RedHat/Apache installation. 10 # It will save the files with a date/time stamp in the filename, 11 #+ bzipped, to a given directory. 12 # 13 # Run this from crontab nightly at an off hour, 14 #+ as bzip2 can suck up some serious CPU on huge logs: 15 # 0 2 * * * /opt/sbin/archiveweblogs.sh 16 17 18 PROBLEM=66 19 20 # Set this to your backup dir. 21 BKP_DIR=/opt/backups/weblogs 22 23 # Default Apache/RedHat stuff 24 LOG_DAYS="4 3 2 1" 25 LOG_DIR=/var/log/httpd 26 LOG_FILES="access_log error_log" 27 28 # Default RedHat program locations 29 LS=/bin/ls 30 MV=/bin/mv 31 ID=/usr/bin/id 32 CUT=/bin/cut 33 COL=/usr/bin/column 34 BZ2=/usr/bin/bzip2 35 36 # Are we root? 37 USER=`$ID -u` 38 if [ "X$USER" != "X0" ]; then 39 echo "PANIC: Only root can run this script!" 40 exit $PROBLEM 41 fi 42 43 # Backup dir exists/writable? 44 if [ ! -x $BKP_DIR ]; then 45 echo "PANIC: $BKP_DIR doesn't exist or isn't writable!" 46 exit $PROBLEM 47 fi 48 49 # Move, rename and bzip2 the logs 50 for logday in $LOG_DAYS; do 51 for logfile in $LOG_FILES; do 52 MYFILE="$LOG_DIR/$logfile.$logday" 53 if [ -w $MYFILE ]; then 54 DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7` 55 $MV $MYFILE $BKP_DIR/$logfile.$DTS 56 $BZ2 $BKP_DIR/$logfile.$DTS 57 else 58 # Only spew an error if the file exits (ergo non-writable). 59 if [ -f $MYFILE ]; then 60 echo "ERROR: $MYFILE not writable. Skipping." 61 fi 62 fi 63 done 64 done 65 66 exit 0 |
How do you keep the shell from expanding and reinterpreting strings?
Example A-27. Protecting literal strings
1 #! /bin/bash
2 # protect_literal.sh
3
4 # set -vx
5
6 :<<-'_Protect_Literal_String_Doc'
7
8 Copyright (c) Michael S. Zick, 2003; All Rights Reserved
9 License: Unrestricted reuse in any form, for any purpose.
10 Warranty: None
11 Revision: $ID$
12
13 Documentation redirected to the Bash no-operation.
14 Bash will '/dev/null' this block when the script is first read.
15 (Uncomment the above set command to see this action.)
16
17 Remove the first (Sha-Bang) line when sourcing this as a library
18 procedure. Also comment out the example use code in the two
19 places where shown.
20
21
22 Usage:
23 _protect_literal_str 'Whatever string meets your ${fancy}'
24 Just echos the argument to standard out, hard quotes
25 restored.
26
27 $(_protect_literal_str 'Whatever string meets your ${fancy}')
28 as the right-hand-side of an assignment statement.
29
30 Does:
31 As the right-hand-side of an assignment, preserves the
32 hard quotes protecting the contents of the literal during
33 assignment.
34
35 Notes:
36 The strange names (_*) are used to avoid trampling on
37 the user's chosen names when this is sourced as a
38 library.
39
40 _Protect_Literal_String_Doc
41
42 # The 'for illustration' function form
43
44 _protect_literal_str() {
45
46 # Pick an un-used, non-printing character as local IFS.
47 # Not required, but shows that we are ignoring it.
48 local IFS=$'\x1B' # \ESC character
49
50 # Enclose the All-Elements-Of in hard quotes during assignment.
51 local tmp=$'\x27'$@$'\x27'
52 # local tmp=$'\''$@$'\'' # Even uglier.
53
54 local len=${#tmp} # Info only.
55 echo $tmp is $len long. # Output AND information.
56 }
57
58 # This is the short-named version.
59 _pls() {
60 local IFS=$'x1B' # \ESC character (not required)
61 echo $'\x27'$@$'\x27' # Hard quoted parameter glob
62 }
63
64 # :<<-'_Protect_Literal_String_Test'
65 # # # Remove the above "# " to disable this code. # # #
66
67 # See how that looks when printed.
68 echo
69 echo "- - Test One - -"
70 _protect_literal_str 'Hello $user'
71 _protect_literal_str 'Hello "${username}"'
72 echo
73
74 # Which yields:
75 # - - Test One - -
76 # 'Hello $user' is 13 long.
77 # 'Hello "${username}"' is 21 long.
78
79 # Looks as expected, but why all of the trouble?
80 # The difference is hidden inside the Bash internal order
81 #+ of operations.
82 # Which shows when you use it on the RHS of an assignment.
83
84 # Declare an array for test values.
85 declare -a arrayZ
86
87 # Assign elements with various types of quotes and escapes.
88 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
89
90 # Now list that array and see what is there.
91 echo "- - Test Two - -"
92 for (( i=0 ; i<${#arrayZ[*]} ; i++ ))
93 do
94 echo Element $i: ${arrayZ[$i]} is: ${#arrayZ[$i]} long.
95 done
96 echo
97
98 # Which yields:
99 # - - Test Two - -
100 # Element 0: zero is: 4 long. # Our marker element
101 # Element 1: 'Hello ${Me}' is: 13 long. # Our "$(_pls '...' )"
102 # Element 2: Hello ${You} is: 12 long. # Quotes are missing
103 # Element 3: \'Pass: \' is: 10 long. # ${pw} expanded to nothing
104
105 # Now make an assignment with that result.
106 declare -a array2=( ${arrayZ[@]} )
107
108 # And print what happened.
109 echo "- - Test Three - -"
110 for (( i=0 ; i<${#array2[*]} ; i++ ))
111 do
112 echo Element $i: ${array2[$i]} is: ${#array2[$i]} long.
113 done
114 echo
115
116 # Which yields:
117 # - - Test Three - -
118 # Element 0: zero is: 4 long. # Our marker element.
119 # Element 1: Hello ${Me} is: 11 long. # Intended result.
120 # Element 2: Hello is: 5 long. # ${You} expanded to nothing.
121 # Element 3: 'Pass: is: 6 long. # Split on the whitespace.
122 # Element 4: ' is: 1 long. # The end quote is here now.
123
124 # Our Element 1 has had its leading and trailing hard quotes stripped.
125 # Although not shown, leading and trailing whitespace is also stripped.
126 # Now that the string contents are set, Bash will always, internally,
127 #+ hard quote the contents as required during its operations.
128
129 # Why?
130 # Considering our "$(_pls 'Hello ${Me}')" construction:
131 # " ... " -> Expansion required, strip the quotes.
132 # $( ... ) -> Replace with the result of..., strip this.
133 # _pls ' ... ' -> called with literal arguments, strip the quotes.
134 # The result returned includes hard quotes; BUT the above processing
135 #+ has already been done, so they become part of the value assigned.
136 #
137 # Similarly, during further usage of the string variable, the ${Me}
138 #+ is part of the contents (result) and survives any operations
139 # (Until explicitly told to evaluate the string).
140
141 # Hint: See what happens when the hard quotes ($'\x27') are replaced
142 #+ with soft quotes ($'\x22') in the above procedures.
143 # Interesting also is to remove the addition of any quoting.
144
145 # _Protect_Literal_String_Test
146 # # # Remove the above "# " to disable this code. # # #
147
148 exit 0 |
What if you want the shell to expand and reinterpret strings?
Example A-28. Unprotecting literal strings
1 #! /bin/bash
2 # unprotect_literal.sh
3
4 # set -vx
5
6 :<<-'_UnProtect_Literal_String_Doc'
7
8 Copyright (c) Michael S. Zick, 2003; All Rights Reserved
9 License: Unrestricted reuse in any form, for any purpose.
10 Warranty: None
11 Revision: $ID$
12
13 Documentation redirected to the Bash no-operation. Bash will
14 '/dev/null' this block when the script is first read.
15 (Uncomment the above set command to see this action.)
16
17 Remove the first (Sha-Bang) line when sourcing this as a library
18 procedure. Also comment out the example use code in the two
19 places where shown.
20
21
22 Usage:
23 Complement of the "$(_pls 'Literal String')" function.
24 (See the protect_literal.sh example.)
25
26 StringVar=$(_upls ProtectedSringVariable)
27
28 Does:
29 When used on the right-hand-side of an assignment statement;
30 makes the substitions embedded in the protected string.
31
32 Notes:
33 The strange names (_*) are used to avoid trampling on
34 the user's chosen names when this is sourced as a
35 library.
36
37
38 _UnProtect_Literal_String_Doc
39
40 _upls() {
41 local IFS=$'x1B' # \ESC character (not required)
42 eval echo $@ # Substitution on the glob.
43 }
44
45 # :<<-'_UnProtect_Literal_String_Test'
46 # # # Remove the above "# " to disable this code. # # #
47
48
49 _pls() {
50 local IFS=$'x1B' # \ESC character (not required)
51 echo $'\x27'$@$'\x27' # Hard quoted parameter glob
52 }
53
54 # Declare an array for test values.
55 declare -a arrayZ
56
57 # Assign elements with various types of quotes and escapes.
58 arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )
59
60 # Now make an assignment with that result.
61 declare -a array2=( ${arrayZ[@]} )
62
63& |