| Chapter 36. Miscellany | Nobody really knows what the Bourne shell's grammar is. Even examination of the source code is little help. --Tom Duff |
36.1. Interactive and non-interactive shells and scriptsAn interactive shell reads commands from user input on a tty. Among other things, such a shell reads startup files on activation, displays a prompt, and enables job control by default. The user can interact with the shell. A shell running a script is always a non-interactive shell. All the same, the script can still access its tty. It is even possible to emulate an interactive shell in a script. #!/bin/bashMY_PROMPT='$ 'while :do echo -n "$MY_PROMPT" read line eval "$line" doneexit 0# This example script, and much of the above explanation supplied by# St�phane Chazelas (thanks again). | Let us consider an interactive script to be one that requires input from the user, usually with read statements (see Example 15-3). "Real life" is actually a bit messier than that. For now, assume an interactive script is bound to a tty, a script that a user has invoked from the console or an xterm. Init and startup scripts are necessarily non-interactive, since they must run without human intervention. Many administrative and system maintenance scripts are likewise non-interactive. Unvarying repetitive tasks cry out for automation by non-interactive scripts. Non-interactive scripts can run in the background, but interactive ones hang, waiting for input that never comes. Handle that difficulty by having an expect script or embedded here document feed input to an interactive script running as a background job. In the simplest case, redirect a file to supply input to a read statement (read variable <file). These particular workarounds make possible general purpose scripts that run in either interactive or non-interactive modes. If a script needs to test whether it is running in an interactive shell, it is simply a matter of finding whether the prompt variable, $PS1 is set. (If the user is being prompted for input, then the script needs to display a prompt.) if [ -z $PS1 ] # no prompt?### if [ -v PS1 ] # On Bash 4.2+ ...then # non-interactive ...else # interactive ...fi | Alternatively, the script can test for the presence of option "i" in the $- flag. case $- in*i*) # interactive shell;*) # non-interactive shell;# (Courtesy of "UNIX F.A.Q.," 1993) | However, John Lange describes an alternative method, using the -t test operator. # Test for a terminal!fd=0 # stdin# As we recall, the -t test option checks whether the stdin, [ -t 0 ],#+ or stdout, [ -t 1 ], in a given script is running in a terminal.if [ -t "$fd" ]then echo interactiveelse echo non-interactivefi# But, as John points out:# if [ -t 0 ] works ... when you're logged in locally# but fails when you invoke the command remotely via ssh.# So for a true test you also have to test for a socket.if [[ -t "$fd" || -p /dev/stdin ]]then echo interactiveelse echo non-interactivefi | | Scripts may be forced to run in interactive mode with the -i option or with a #!/bin/bash -i header. Be aware that this can cause erratic script behavior or show error messages even when no error is present. |
36.2. Shell Wrappers
A wrapper is a shell script that embedsa system command or utility, that accepts and passes a set ofparameters to that command. Wrapping a script around a complex command-linesimplifies invoking it. This is expecially usefulwith sed and awk. A sed or awk script would normally be invoked from the command-line by a sed -e 'commands' or awk 'commands'.Embedding such a script in a Bash script permits calling it more simply, and makes it reusable. This also enables combining the functionality of sed and awk, for example piping the output of a set of sed commands to awk. As a saved executable file, you can then repeatedly invoke it in its original form or modified, without the inconvenience of retyping it on the command-line. Example 36-1. shell wrapper #!/bin/bash# This simple script removes blank lines from a file.# No argument checking.## You might wish to add something like:## E_NOARGS=85# if [ -z "$1" ]# then# echo "Usage: `basename $0` target-file" # exit $E_NOARGS# fised -e /^$/d "$1" # Same as# sed -e '/^$/d' filename# invoked from the command-line.# The '-e' means an "editing" command follows (optional here).# '^' indicates the beginning of line, '$' the end.# This matches lines with nothing between the beginning and the end --#+ blank lines.# The 'd' is the delete command.# Quoting the command-line arg permits#+ whitespace and special characters in the filename.# Note that this script doesn't actually change the target file.# If you need to do that, redirect its output.exit | Example 36-2. A slightly more complex shellwrapper #!/bin/bash# subst.sh: a script that substitutes one pattern for#+ another in a file,#+ i.e., "sh subst.sh Smith Jones letter.txt".# Jones replaces Smith.ARGS=3 # Script requires 3 arguments.E_BADARGS=85 # Wrong number of arguments passed to script.if [ $# -ne "$ARGS" ]then echo "Usage: `basename $0` old-pattern new-pattern filename" exit $E_BADARGSfiold_pattern=$1new_pattern=$2if [ -f "$3" ]then file_name=$3else echo "File "$3" does not exist." exit $E_BADARGSfi# -----------------------------------------------# Here is where the heavy work gets done.sed -e "s/$old_pattern/$new_pattern/g" $file_name# -----------------------------------------------# 's' is, of course, the substitute command in sed,#+ and /pattern/ invokes address matching.# The 'g,' or global flag causes substitution for EVERY#+ occurence of $old_pattern on each line, not just the first.# Read the 'sed' docs for an in-depth explanation.exit $? # Redirect the output of this script to write to a file. | Example 36-3. A generic shell wrapper thatwrites to a logfile #!/bin/bash# logging-wrapper.sh# Generic shell wrapper that performs an operation#+ and logs it.DEFAULT_LOGFILE=logfile.txt# Set the following two variables.OPERATION=# Can be a complex chain of commands,#+ for example an awk script or a pipe . . .LOGFILE=if [ -z "$LOGFILE" ]then # If not set, default to ... LOGFILE="$DEFAULT_LOGFILE" fi# Command-line arguments, if any, for the operation.OPTIONS="$@" # Log it.echo "`date` + `whoami` + $OPERATION "$@"" >> $LOGFILE# Now, do it.exec $OPERATION "$@" # It's necessary to do the logging before the operation.# Why? | Example 36-4. A shell wrapper around an awkscript #!/bin/bash# pr-ascii.sh: Prints a table of ASCII characters.START=33 # Range of printable ASCII characters (decimal).END=127 # Will not work for unprintable characters (> 127).echo " Decimal Hex Character" # Header.echo " ------- --- ---------" for ((i=START; i<=END; i++))do echo $i | awk '{printf(" %3d %2x %c", $1, $1, $1)}'# The Bash printf builtin will not work in this context:# printf "%c" "$i" doneexit 0# Decimal Hex Character# ------- --- ---------# 33 21 !# 34 22 " # 35 23 ## 36 24 $## . . .## 122 7a z# 123 7b {# 124 7c |# 125 7d }# Redirect the output of this script to a file#+ or pipe it to "more": sh pr-asc.sh | more | Example 36-5. A shell wrapper around anotherawk script #!/bin/bash# Adds up a specified column (of numbers) in the target file.# Floating-point (decimal) numbers okay, because awk can handle them.ARGS=2E_WRONGARGS=85if [ $# -ne "$ARGS" ] # Check for proper number of command-line args.then echo "Usage: `basename $0` filename column-number" exit $E_WRONGARGSfifilename=$1column_number=$2# Passing shell variables to the awk part of the script is a bit tricky.# One method is to strong-quote the Bash-script variable#+ within the awk script.# $'$BASH_SCRIPT_VAR'# ^ ^# This is done in the embedded awk script below.# See the awk documentation for more details.# A multi-line awk script is here invoked by# awk '# ...# ...# ...# '# Begin awk script.# -----------------------------awk '{ total += $'"${column_number}"'}END { print total} ' "$filename" # -----------------------------# End awk script.# It may not be safe to pass shell variables to an embedded awk script,#+ so Stephane Chazelas proposes the following alternative:# ---------------------------------------# awk -v column_number="$column_number" '# { total += $column_number# }# END {# print total# }' "$filename" # ---------------------------------------exit 0 | For those scripts needing a singledo-it-all tool, a Swiss army knife, there isPerl. Perl combines thecapabilities of sed and awk, and throws in a large subset ofC, to boot. It is modular and contains supportfor everything ranging from object-oriented programming up to andincluding the kitchen sink. Short Perl scripts lend themselves toembedding within shell scripts, and there may be some substanceto the claim that Perl can totally replace shell scripting(though the author of the ABS Guide remainsskeptical).
Example 36-6. Perl embedded in a Bash script #!/bin/bash# Shell commands may precede the Perl script.echo "This precedes the embedded Perl script within "$0"." echo "===============================================================" perl -e 'print "This line prints from an embedded Perl script.";'# Like sed, Perl also uses the "-e" option.echo "===============================================================" echo "However, the script may also contain shell and system commands." exit 0 | It is even possible to combine a Bash script and Perl script within the same file. Depending on how the script is invoked, eitherthe Bash part or the Perl part will execute.
Example 36-7. Bash and Perl scripts combined #!/bin/bash# bashandperl.shecho "Greetings from the Bash part of the script, $0." # More Bash commands may follow here.exit# End of Bash part of the script.# =======================================================#!/usr/bin/perl# This part of the script must be invoked with# perl -x bashandperl.shprint "Greetings from the Perl part of the script, $0.";# Perl doesn't seem to like "echo" ...# More Perl commands may follow here.# End of Perl part of the script. | bash$ bash bashandperl.shGreetings from the Bash part of the script.bash$ perl -x bashandperl.shGreetings from the Perl part of the script. | One interesting example of a complex shell wrapper is Martin Matusiak's undvd script, which provides an easy-to-use command-line interface to the complex mencoder utility. Another example is Itzchak Rehberg's Ext3Undel, a set of scripts to recover deleted file on an ext3 filesystem.
36.3. Tests and Comparisons: AlternativesFor tests, the [[ ]] construct may be more appropriate than [ ]. Likewise, arithmetic comparisons might benefit from the (( )) construct. a=8# All of the comparisons below are equivalent.test "$a" -lt 16 && echo "yes, $a < 16" # "and list" /bin/test "$a" -lt 16 && echo "yes, $a < 16" [ "$a" -lt 16 ] && echo "yes, $a < 16" [[ $a -lt 16 ]] && echo "yes, $a < 16" # Quoting variables within(( a < 16 )) && echo "yes, $a < 16" # [[ ]] and (( )) not necessary.city="New York" # Again, all of the comparisons below are equivalent.test "$city" < Paris && echo "Yes, Paris is greater than $city" # Greater ASCII order./bin/test "$city" < Paris && echo "Yes, Paris is greater than $city" [ "$city" < Paris ] && echo "Yes, Paris is greater than $city" [[ $city < Paris ]] && echo "Yes, Paris is greater than $city" # Need not quote $city.# Thank you, S.C. |
36.4. Recursion: a script calling itself
Can a script recursively call itself? Indeed. Example 36-8. A (useless) script that recursively calls itself #!/bin/bash# recurse.sh# Can a script recursively call itself?# Yes, but is this of any practical use?# (See the following.)RANGE=10MAXVAL=9i=$RANDOMlet "i %= $RANGE" # Generate a random number between 0 and $RANGE - 1.if [ "$i" -lt "$MAXVAL" ]then echo "i = $i" ./$0 # Script recursively spawns a new instance of itself.fi # Each child script does the same, until #+ a generated $i equals $MAXVAL.# Using a "while" loop instead of an "if/then" test causes problems.# Explain why.exit 0# Note:# ----# This script must have execute permission for it to work properly.# This is the case even if it is invoked by an "sh" command.# Explain why. | Example 36-9. A (useful) script that recursively calls itself #!/bin/bash# pb.sh: phone book# Written by Rick Boivie, and used with permission.# Modifications by ABS Guide author.MINARGS=1 # Script needs at least one argument.DATAFILE=./phonebook # A data file in current working directory #+ named "phonebook" must exist.PROGNAME=$0E_NOARGS=70 # No arguments error.if [ $# -lt $MINARGS ]; then echo "Usage: "$PROGNAME" data-to-look-up" exit $E_NOARGSfi if [ $# -eq $MINARGS ]; then grep $1 "$DATAFILE" # 'grep' prints an error message if $DATAFILE not present.else ( shift; "$PROGNAME" $* ) | grep $1 # Script recursively calls itself.fiexit 0 # Script exits here. # Therefore, it's o.k. to put #+ non-hashmarked comments and data after this point.# ------------------------------------------------------------------------Sample "phonebook" datafile:John Doe 1555 Main St., Baltimore, MD 21228 (410) 222-3333Mary Moe 9899 Jones Blvd., Warren, NH 03787 (603) 898-3232Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678Zoe Zenobia 4481 N. Baker St., San Francisco, SF 94338 (415) 501-1631# ------------------------------------------------------------------------$bash pb.sh RoeRichard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678$bash pb.sh Roe SamSam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678# When more than one argument is passed to this script,#+ it prints *only* the line(s) containing all the arguments. | Example 36-10. Another (useful) script that recursively calls itself #!/bin/bash# usrmnt.sh, written by Anthony Richardson# Used in ABS Guide with permission.# usage: usrmnt.sh# description: mount device, invoking user must be listed in the# MNTUSERS group in the /etc/sudoers file.# ----------------------------------------------------------# This is a usermount script that reruns itself using sudo.# A user with the proper permissions only has to type# usermount /dev/fd0 /mnt/floppy# instead of# sudo usermount /dev/fd0 /mnt/floppy# I use this same technique for all of my#+ sudo scripts, because I find it convenient.# ----------------------------------------------------------# If SUDO_COMMAND variable is not set we are not being run through#+ sudo, so rerun ourselves. Pass the user's real and group id . . .if [ -z "$SUDO_COMMAND" ]then mntusr=$(id -u) grpusr=$(id -g) sudo $0 $* exit 0fi# We will only get here if we are being run by sudo./bin/mount $* -o uid=$mntusr,gid=$grpusrexit 0# Additional notes (from the author of this script): # -------------------------------------------------# 1) Linux allows the "users" option in the /etc/fstab# file so that any user can mount removable media.# But, on a server, I like to allow only a few# individuals access to removable media.# I find using sudo gives me more control.# 2) I also find sudo to be more convenient than# accomplishing this task through groups.# 3) This method gives anyone with proper permissions# root access to the mount command, so be careful# about who you allow access.# You can get finer control over which access can be mounted# by using this same technique in separate mntfloppy, mntcdrom,# and mntsamba scripts. | | Too many levels of recursion can exhaust the script's stack space, causing a segfault. |
36.5. "Colorizing" Scripts
The ANSI escape sequences set screen attributes, such as bold text, and color of foreground and background. DOS batch files commonly used ANSI escape codes for color output, and so can Bash scripts. Example 36-11. A "colorized" address database #!/bin/bash# ex30a.sh: "Colorized" version of ex30.sh.# Crude address databaseclear # Clear the screen.echo -n " " echo -e 'E[37;44m'" 33[1mContact List 33[0m" # White on blue backgroundecho; echoecho -e " 33[1mChoose one of the following persons: 33[0m" # Boldtput sgr0 # Reset attributes.echo "(Enter only the first letter of name.)" echoecho -en 'E[47;34m'" 33[1mE 33[0m" # Bluetput sgr0 # Reset colors to "normal." echo "vans, Roland" # "[E]vans, Roland" echo -en 'E[47;35m'" 33[1mJ 33[0m" # Magentatput sgr0echo "ambalaya, Mildred" echo -en 'E[47;32m'" 33[1mS 33[0m" # Greentput sgr0echo "mith, Julie" echo -en 'E[47;31m'" 33[1mZ 33[0m" # Redtput sgr0echo "ane, Morris" echoread personcase "$person" in# Note variable is quoted. "E" | "e" ) # Accept upper or lowercase input. echo echo "Roland Evans" echo "4321 Flash Dr." echo "Hardscrabble, CO 80753" echo "(303) 734-9874" echo "(303) 734-9892 fax" echo "[email protected]" echo "Business partner & old friend" ; "J" | "j" ) echo echo "Mildred Jambalaya" echo "249 E. 7th St., Apt. 19" echo "New York, NY 10009" echo "(212) 533-2814" echo "(212) 533-9972 fax" echo "[email protected]" echo "Girlfriend" echo "Birthday: Feb. 11" ;# Add info for Smith & Zane later. * ) # Default option. # Empty input (hitting RETURN) fits here, too. echo echo "Not yet in database." ;esactput sgr0 # Reset colors to "normal." echoexit 0 | Example 36-12. Drawing a box #!/bin/bash# Draw-box.sh: Drawing a box using ASCII characters.# Script by Stefano Palmeri, with minor editing by document author.# Minor edits suggested by Jim Angstadt.# Used in the ABS Guide with permission.######################################################################### draw_box function doc #### The "draw_box" function lets the user#+ draw a box in a terminal. ## Usage: draw_box ROW COLUMN HEIGHT WIDTH [COLOR] # ROW and COLUMN represent the position #+ of the upper left angle of the box you're going to draw.# ROW and COLUMN must be greater than 0#+ and less than current terminal dimension.# HEIGHT is the number of rows of the box, and must be > 0. # HEIGHT + ROW must be <= than current terminal height. # WIDTH is the number of columns of the box and must be > 0.# WIDTH + COLUMN must be <= than current terminal width.## E.g.: If your terminal dimension is 20x80,# draw_box 2 3 10 45 is good# draw_box 2 3 19 45 has bad HEIGHT value (19+2 > 20)# draw_box 2 3 18 78 has bad WIDTH value (78+3 > 80)## COLOR is the color of the box frame.# This is the 5th argument and is optional.# 0=black 1=red 2=green 3=tan 4=blue 5=purple 6=cyan 7=white.# If you pass the function bad arguments,#+ it will just exit with code 65,#+ and no messages will be printed on stderr.## Clear the terminal before you start to draw a box.# The clear command is not contained within the function.# This allows the user to draw multiple boxes, even overlapping ones.### end of draw_box function doc ### ######################################################################draw_box(){#=============#HORZ="-" VERT="|" CORNER_CHAR="+" MINARGS=4E_BADARGS=65#=============#if [ $# -lt "$MINARGS" ]; then # If args are less than 4, exit. exit $E_BADARGSfi# Looking for non digit chars in arguments.# Probably it could be done better (exercise for the reader?).if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then exit $E_BADARGSfiBOX_HEIGHT=`expr $3 - 1` # -1 correction needed because angle char "+" BOX_WIDTH=`expr $4 - 1` #+ is a part of both box height and width.T_ROWS=`tput lines` # Define current terminal dimension T_COLS=`tput cols` #+ in rows and columns. if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then # Start checking if arguments exit $E_BADARGS #+ are correct.fiif [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then exit $E_BADARGSfiif [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then exit $E_BADARGSfiif [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then exit $E_BADARGSfiif [ $3 -lt 1 ] || [ $4 -lt 1 ]; then exit $E_BADARGSfi # End checking arguments.plot_char(){ # Function within a function. echo -e "E[${1};${2}H"$3}echo -ne "E[3${5}m" # Set box frame color, if defined.# start drawing the boxcount=1 # Draw vertical lines usingfor (( r=$1; count<=$BOX_HEIGHT; r++)); do #+ plot_char function. plot_char $r $2 $VERT let count=count+1done count=1c=`expr $2 + $BOX_WIDTH`for (( r=$1; count<=$BOX_HEIGHT; r++)); do plot_char $r $c $VERT let count=count+1done count=1 # Draw horizontal lines usingfor (( c=$2; count<=$BOX_WIDTH; c++)); do #+ plot_char function. plot_char $1 $c $HORZ let count=count+1done count=1r=`expr $1 + $BOX_HEIGHT`for (( c=$2; count<=$BOX_WIDTH; c++)); do plot_char $r $c $HORZ let count=count+1done plot_char $1 $2 $CORNER_CHAR # Draw box angles.plot_char $1 `expr $2 + $BOX_WIDTH` $CORNER_CHARplot_char `expr $1 + $BOX_HEIGHT` $2 $CORNER_CHARplot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` $CORNER_CHARecho -ne "E[0m" # Restore old colors.P_ROWS=`expr $T_ROWS - 1` # Put the prompt at bottom of the terminal.echo -e "E[${P_ROWS};1H" } # Now, let's try drawing a box.clear # Clear the terminal.R=2 # RowC=3 # ColumnH=10 # HeightW=45 # Width col=1 # Color (red)draw_box $R $C $H $W $col # Draw the box.exit 0# Exercise:# --------# Add the option of printing text within the drawn box. | The simplest, and perhaps most useful ANSI escape sequence is bold text, |
| |