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 ARGS=1 7 E_BADARGS=65 8 E_NOFILE=66 9 10 if [ $# -ne $ARGS ] # Correct number of arguments passed to script? 11 then 12 echo "Usage: `basename $0` filename" 13 exit $E_BADARGS 14 fi 15 16 if [ -f "$1" ] # Check if file exists. 17 then 18 file_name=$1 19 else 20 echo "File \"$1\" does not exist." 21 exit $E_NOFILE 22 fi 23 24 MAXWIDTH=70 # Width to fold long lines to. 25 26 sed ' 27 s/^>// 28 s/^ *>// 29 s/^ *// 30 s/ *// 31 ' $1 | fold -s --width=$MAXWIDTH 32 # -s option to "fold" breaks lines at whitespace, if possible. 33 34 # This script was inspired by an article in a well-known trade journal 35 # extolling a 164K Windows utility with similar functionality. 36 37 exit 0 |
Example A-3. rn: A simple-minded file rename utility
This script is a modification of Example 12-14.
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 # Exercise for reader: 45 # What type of files will this not work on? 46 # How to fix this? |
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 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://metalab.unc.edu /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-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 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 for the reader: 47 # Change the above "case" statement to also accept "yes" and "Yes" as input. 48 49 exit 0 |
Example A-6. 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 # Exercise for the reader: Implement more rigorous date checking. 49 } 50 51 52 strip_leading_zero () # Better to strip possible leading zero(s) 53 { # from day and/or month 54 val=${1#0} # since otherwise Bash will interpret them 55 return $val # as octal values (POSIX.2, sect 2.9.2.1). 56 } 57 58 59 day_index () # Gauss' Formula: 60 { # Days from Jan. 3, 1600 to date passed as param. 61 62 day=$1 63 month=$2 64 year=$3 65 66 let "month = $month - 2" 67 if [ "$month" -le 0 ] 68 then 69 let "month += 12" 70 let "year -= 1" 71 fi 72 73 let "year -= $REFYR" 74 let "indexyr = $year / $CENTURY" 75 76 77 let "Days = $DIY*$year + $year/$LEAPCYCLE - $indexyr + $indexyr/$LEAPCYCLE + $ADJ_DIY*$month/$MIY + $day - $DIM" 78 # For an in-depth explanation of this algorithm, see 79 # http://home.t-online.de/home/berndt.schwerdtfeger/cal.htm 80 81 82 if [ "$Days" -gt "$MAXRETVAL" ] # If greater than 256, 83 then # then change to negative value 84 let "dindex = 0 - $Days" # which can be returned from function. 85 else let "dindex = $Days" 86 fi 87 88 return $dindex 89 90 } 91 92 93 calculate_difference () # Difference between to 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 day_index $day $month $year 125 date1=$? 126 127 abs $date1 # Make sure it's positive 128 date1=$value # by getting absolute value. 129 130 Parse_Date $2 131 check_date $day $month $year 132 133 strip_leading_zero $day 134 day=$? 135 strip_leading_zero $month 136 month=$? 137 138 day_index $day $month $year 139 date2=$? 140 141 abs $date2 # Make sure it's positive. 142 date2=$value 143 144 calculate_difference $date1 $date2 145 146 abs $diff # Make sure it's positive. 147 diff=$value 148 149 echo $diff 150 151 exit 0 152 # Compare this script with the implementation of Gauss' Formula in C at 153 # http://buschencrew.hypermart.net/software/datedif |
+
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-7. 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 for the reader: 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-8. 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 for reader: 100 # ==> 1) Add error checking. 101 # ==> 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-9. 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-10. 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-11. Generating prime numbers using the modulo operator
1 #!/bin/bash 2 # primes.sh: Generate prime numbers, without using arrays. 3 4 # This does *not* use the classic "Sieve of Erastosthenes" algorithm, 5 #+ but instead uses the more intuitive method of testing each candidate number 6 #+ for factors (divisors), using the "%" modulo operator. 7 # 8 # Script contributed by Stephane Chazelas, 9 10 11 LIMIT=1000 # Primes 2 - 1000 12 13 Primes() 14 { 15 (( n = $1 + 1 )) # Bump to next integer. 16 shift # Next parameter in list. 17 # echo "_n=$n i=$i_" 18 19 if (( n == LIMIT )) 20 then echo $* 21 return 22 fi 23 24 for i; do # "i" gets set to "@", previous values of $n. 25 # echo "-n=$n i=$i-" 26 (( i * i > n )) && break # Optimization. 27 (( n % i )) && continue # Sift out non-primes using modulo operator. 28 Primes $n $@ # Recursion inside loop. 29 return 30 done 31 32 Primes $n $@ $n # Recursion outside loop. 33 # Successively accumulate positional parameters. 34 # "$@" is the accumulating list of primes. 35 } 36 37 Primes 1 38 39 exit 0 40 41 # Uncomment lines 17 and 25 to help figure out what is going on. 42 43 # Compare the speed of this algorithm for generating primes 44 # with the Sieve of Erastosthenes (ex68.sh). 45 46 # Exercise: Rewrite this script without recursion, for faster execution. |
+
Jordi Sanfeliu gave permission to use his elegant tree script.
Example A-12. tree: Displaying a directory tree
1 #!/bin/sh 2 # @(#) tree 1.1 30/11/95 by Jordi Sanfeliu 3 # email: mikaku@arrakis.es 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 to reader: 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-13. 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 for reader: 267 # Add code to test all the other string functions above. 268 269 270 exit 0 |
Stephane Chazelas demonstrates object-oriented programming a Bash script.
Example A-14. 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 |