/procThe /proc directory
is actually a pseudo-filesystem. The files in /proc mirror currently running
system and kernel processes and contain
information and statistics about them. bash$ cat /proc/devices
Character devices:
1 mem
2 pty
3 ttyp
4 ttyS
5 cua
7 vcs
10 misc
14 sound
29 fb
36 netlink
128 ptm
136 pts
162 raw
254 pcmcia
Block devices:
1 ramdisk
2 fd
3 ide0
9 md
bash$ cat /proc/interrupts
CPU0
0: 84505 XT-PIC timer
1: 3375 XT-PIC keyboard
2: 0 XT-PIC cascade
5: 1 XT-PIC soundblaster
8: 1 XT-PIC rtc
12: 4231 XT-PIC PS/2 Mouse
14: 109373 XT-PIC ide0
NMI: 0
ERR: 0
bash$ cat /proc/partitions
major minor #blocks name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq
3 0 3007872 hda 4472 22260 114520 94240 3551 18703 50384 549710 0 111550 644030
3 1 52416 hda1 27 395 844 960 4 2 14 180 0 800 1140
3 2 1 hda2 0 0 0 0 0 0 0 0 0 0 0
3 4 165280 hda4 10 0 20 210 0 0 0 0 0 210 210
...
bash$ cat /proc/loadavg
0.13 0.42 0.27 2/44 1119
bash$ cat /proc/apm
1.16 1.2 0x03 0x01 0xff 0x80 -1% -1 ?
|
Shell scripts may extract data from certain of the files in
/proc.
FS=iso # ISO filesystem support in kernel?
grep $FS /proc/filesystems # iso9660 |
kernel_version=$( awk '{ print $3 }' /proc/version ) |
CPU=$( awk '/model name/ {print $4}' < /proc/cpuinfo )
if [ $CPU = Pentium ]
then
run_some_commands
...
else
run_different_commands
...
fi |
devfile="/proc/bus/usb/devices"
USB1="Spd=12"
USB2="Spd=480"
bus_speed=$(grep Spd $devfile | awk '{print $9}')
if [ "$bus_speed" = "$USB1" ]
then
echo "USB 1.1 port found."
# Do something appropriate for USB 1.1.
fi |
The /proc directory
contains subdirectories with unusual numerical
names. Every one of these names maps to the process ID of a currently running
process. Within each of these subdirectories, there are
a number of files that hold useful information about the
corresponding process. The stat and
status files keep running statistics
on the process, the cmdline file holds
the command-line arguments the process was invoked with, and
the exe file is a symbolic link to the
complete path name of the invoking process. There are a few
more such files, but these seem to be the most interesting
from a scripting standpoint. Example 27-2. Finding the process associated with a PID #!/bin/bash
# pid-identifier.sh: Gives complete path name to process associated with pid.
ARGNO=1 # Number of arguments the script expects.
E_WRONGARGS=65
E_BADPID=66
E_NOSUCHPROCESS=67
E_NOPERMISSION=68
PROCFILE=exe
if [ $# -ne $ARGNO ]
then
echo "Usage: `basename $0` PID-number" >&2 # Error message >stderr.
exit $E_WRONGARGS
fi
pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 )
# Checks for pid in "ps" listing, field #1.
# Then makes sure it is the actual process, not the process invoked by this script.
# The last "grep $1" filters out this possibility.
if [ -z "$pidno" ] # If, after all the filtering, the result is a zero-length string,
then # no running process corresponds to the pid given.
echo "No such process running."
exit $E_NOSUCHPROCESS
fi
# Alternatively:
# if ! ps $1 > /dev/null 2>&1
# then # no running process corresponds to the pid given.
# echo "No such process running."
# exit $E_NOSUCHPROCESS
# fi
# To simplify the entire process, use "pidof".
if [ ! -r "/proc/$1/$PROCFILE" ] # Check for read permission.
then
echo "Process $1 running, but..."
echo "Can't get read permission on /proc/$1/$PROCFILE."
exit $E_NOPERMISSION # Ordinary user can't access some files in /proc.
fi
# The last two tests may be replaced by:
# if ! kill -0 $1 > /dev/null 2>&1 # '0' is not a signal, but
# this will test whether it is possible
# to send a signal to the process.
# then echo "PID doesn't exist or you're not its owner" >&2
# exit $E_BADPID
# fi
exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
# Or exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' )
#
# /proc/pid-number/exe is a symbolic link
# to the complete path name of the invoking process.
if [ -e "$exe_file" ] # If /proc/pid-number/exe exists...
then # the corresponding process exists.
echo "Process #$1 invoked by $exe_file."
else
echo "No such process running."
fi
# This elaborate script can *almost* be replaced by
# ps ax | grep $1 | awk '{ print $5 }'
# However, this will not work...
# because the fifth field of 'ps' is argv[0] of the process,
# not the executable file path.
#
# However, either of the following would work.
# find /proc/$1/exe -printf '%l\n'
# lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'
# Additional commentary by Stephane Chazelas.
exit 0 |
Example 27-3. On-line connect status #!/bin/bash
PROCNAME=pppd # ppp daemon
PROCFILENAME=status # Where to look.
NOTCONNECTED=65
INTERVAL=2 # Update every 2 seconds.
pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' )
# Finding the process number of 'pppd', the 'ppp daemon'.
# Have to filter out the process lines generated by the search itself.
#
# However, as Oleg Philon points out,
#+ this could have been considerably simplified by using "pidof".
# pidno=$( pidof $PROCNAME )
#
# Moral of the story:
#+ When a command sequence gets too complex, look for a shortcut.
if [ -z "$pidno" ] # If no pid, then process is not running.
then
echo "Not connected."
exit $NOTCONNECTED
else
echo "Connected."; echo
fi
while [ true ] # Endless loop, script can be improved here.
do
if [ ! -e "/proc/$pidno/$PROCFILENAME" ]
# While process running, then "status" file exists.
then
echo "Disconnected."
exit $NOTCONNECTED
fi
netstat -s | grep "packets received" # Get some connect statistics.
netstat -s | grep "packets delivered"
sleep $INTERVAL
echo; echo
done
exit 0
# As it stands, this script must be terminated with a Control-C.
# Exercises:
# ---------
# Improve the script so it exits on a "q" keystroke.
# Make the script more user-friendly in other ways. |
| In general, it is dangerous to
write to the files in /proc, as this can corrupt the
filesystem or crash the machine. |
Debugging | Debugging is twice as hard as writing the code in the first
place. Therefore, if you write the code as cleverly as possible,
you are, by definition, not smart enough to debug it. | | Brian Kernighan |
The Bash shell contains no debugger, nor even any
debugging-specific commands or constructs.
Syntax errors or outright typos in the script generate cryptic
error messages that are often of no help in debugging a
non-functional script. Example 29-1. A buggy script #!/bin/bash
# ex74.sh
# This is a buggy script.
# Where, oh where is the error?
a=37
if [$a -gt 27 ]
then
echo $a
fi
exit 0 |
Output from script:
./ex74.sh: [37: command not found |
What's wrong with the above script (hint: after the
if)?Example 29-2. Missing keyword #!/bin/bash
# missing-keyword.sh: What error message will this generate?
for a in 1 2 3
do
echo "$a"
# done # Required keyword 'done' commented out in line 7.
exit 0 |
Output from script:
missing-keyword.sh: line 10: syntax error: unexpected end of file
|
Note that the error message does not necessarily
reference the line in which the error occurs, but the line where the
Bash interpreter finally becomes aware of the error.
Error messages may disregard comment lines in a script when
reporting the line number of a syntax error. What if the script executes, but does not work as expected? This is the
all too familiar logic error. Example 29-3. test24, another buggy script #!/bin/bash
# This script is supposed to delete all filenames in current directory
#+ containing embedded spaces.
# It doesn't work.
# Why not?
badname=`ls | grep ' '`
# Try this:
# echo "$badname"
rm "$badname"
exit 0 |
Try to find out what's wrong with Example 29-3
by uncommenting the echo "$badname" line. Echo
statements are useful for seeing whether what you expect is
actually what you get. In this particular case, rm "$badname"
will not give the desired results because
$badname should not be quoted. Placing it
in quotes ensures that rm has only one
argument (it will match only one filename). A partial fix
is to remove to quotes from $badname and
to reset $IFS to contain only a newline,
IFS=$'\n'. However, there are simpler
ways of going about it.
# Correct methods of deleting filenames containing spaces.
rm *\ *
rm *" "*
rm *' '*
# Thank you. S.C. |
Summarizing the symptoms of a buggy script,
It bombs with a "syntax error" message, or It runs, but does not work as expected
(logic error). It runs, works as expected, but has nasty side effects
(logic bomb).
Tools for debugging non-working scripts include
echo statements at
critical points in the script to trace the variables,
and otherwise give a snapshot of what is going on. | Even better is an echo that echoes
only when debug is on.
### debecho (debug-echo), by Stefano Falsetto ###
### Will echo passed parameters only if DEBUG is set to a value. ###
debecho () {
if [ ! -z "$DEBUG" ]; then
echo "$1" >&2
# ^^^ to stderr
fi
}
DEBUG=on
Whatever=whatnot
debecho $Whatever # whatnot
DEBUG=
Whatever=notwhat
debecho $Whatever # (Will not echo.) |
|
using the tee filter
to check processes or data flows at critical points. setting option flags -n -v -x sh -n scriptname checks for
syntax errors without actually running the script. This is
the equivalent of inserting set -n or
set -o noexec into the script. Note
that certain types of syntax errors can slip past this
check. sh -v scriptname echoes each
command before executing it. This is the equivalent of
inserting set -v or set
-o verbose in the script. The -n and -v
flags work well together. sh -nv
scriptname gives a verbose syntax check. sh -x scriptname echoes the result each
command, but in an abbreviated manner. This is the equivalent of
inserting set -x or
set -o xtrace in the script. Inserting set -u or
set -o nounset in the script runs it, but
gives an unbound variable error message
at each attempt to use an undeclared variable. Using an "assert" function to test a
variable or condition at critical points in a script. (This is
an idea borrowed from C.) Example 29-4. Testing a condition with an "assert" #!/bin/bash
# assert.sh
assert () # If condition false,
{ #+ exit from script with error message.
E_PARAM_ERR=98
E_ASSERT_FAILED=99
if [ -z "$2" ] # Not enough parameters passed.
then
return $E_PARAM_ERR # No damage done.
fi
lineno=$2
if [ ! $1 ]
then
echo "Assertion failed: \"$1\""
echo "File \"$0\", line $lineno"
exit $E_ASSERT_FAILED
# else
# return
# and continue executing script.
fi
}
a=5
b=4
condition="$a -lt $b" # Error message and exit from script.
# Try setting "condition" to something else,
#+ and see what happens.
assert "$condition" $LINENO
# The remainder of the script executes only if the "assert" does not fail.
# Some commands.
# ...
echo "This statement echoes only if the \"assert\" does not fail."
# ...
# Some more commands.
exit 0 |
trapping at exit. The exit command in a script triggers a
signal 0, terminating the
process, that is, the script itself.
It is often useful to trap the
exit, forcing a "printout"
of variables, for example. The trap
must be the first command in the script.
Trapping signals - trap
Specifies an action on receipt of a signal; also
useful for debugging.
| A
signal is simply a message
sent to a process, either by the kernel or another
process, telling it to take some specified action
(usually to terminate). For example, hitting a
Control-C,
sends a user interrupt, an INT signal, to a running
program. |
trap '' 2
# Ignore interrupt 2 (Control-C), with no action specified.
trap 'echo "Control-C disabled."' 2
# Message when Control-C pressed. |
Example 29-5. Trapping at exit #!/bin/bash
# Hunting variables with a trap.
trap 'echo Variable Listing --- a = $a b = $b' EXIT
# EXIT is the name of the signal generated upon exit from a script.
#
# The command specified by the "trap" doesn't execute until
#+ the appropriate signal is sent.
echo "This prints before the \"trap\" --"
echo "even though the script sees the \"trap\" first."
echo
a=39
b=36
exit 0
# Note that commenting out the 'exit' command makes no difference,
#+ since the script exits in any case after running out of commands. |
Example 29-6. Cleaning up after Control-C #!/bin/bash
# logon.sh: A quick 'n dirty script to check whether you are on-line yet.
umask 177 # Make sure temp files are not world readable.
TRUE=1
LOGFILE=/var/log/messages
# Note that $LOGFILE must be readable
#+ (as root, chmod 644 /var/log/messages).
TEMPFILE=temp.$$
# Create a "unique" temp file name, using process id of the script.
# Using 'mktemp' is an alternative.
# For example:
# TEMPFILE=`mktemp temp.XXXXXX`
KEYWORD=address
# At logon, the line "remote IP address xxx.xxx.xxx.xxx"
# appended to /var/log/messages.
ONLINE=22
USER_INTERRUPT=13
CHECK_LINES=100
# How many lines in log file to check.
trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
# Cleans up the temp file if script interrupted by control-c.
echo
while [ $TRUE ] #Endless loop.
do
tail -$CHECK_LINES $LOGFILE> $TEMPFILE
# Saves last 100 lines of system log file as temp file.
# Necessary, since newer kernels generate many log messages at log on.
search=`grep $KEYWORD $TEMPFILE`
# Checks for presence of the "IP address" phrase,
#+ indicating a successful logon.
if [ ! -z "$search" ] # Quotes necessary because of possible spaces.
then
echo "On-line"
rm -f $TEMPFILE # Clean up temp file.
exit $ONLINE
else
echo -n "." # The -n option to echo suppresses newline,
#+ so you get continuous rows of dots.
fi
sleep 1
done
# Note: if you change the KEYWORD variable to "Exit",
#+ this script can be used while on-line
#+ to check for an unexpected logoff.
# Exercise: Change the script, per the above note,
# and prettify it.
exit 0
# Nick Drage suggests an alternate method:
while true
do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
echo -n "." # Prints dots (.....) until connected.
sleep 2
done
# Problem: Hitting Control-C to terminate this process may be insufficient.
#+ (Dots may keep on echoing.)
# Exercise: Fix this.
# Stephane Chazelas has yet another alternative:
CHECK_INTERVAL=1
while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
do echo -n .
sleep $CHECK_INTERVAL
done
echo "On-line"
# Exercise: Discuss the relative strengths and weaknesses
# of each of these various approaches. |
| The DEBUG argument to
trap causes a specified action to execute
after every command in a script. This permits tracing variables,
for example.
Example 29-7. Tracing a variable #!/bin/bash
trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
# Echoes the value of $variable after every command.
variable=29
echo "Just initialized \"\$variable\" to $variable."
let "variable *= 3"
echo "Just multiplied \"\$variable\" by 3."
exit $?
# The "trap 'command1 . . . command2 . . .' DEBUG" construct is
#+ more appropriate in the context of a complex script,
#+ where placing multiple "echo $variable" statements might be
#+ clumsy and time-consuming.
# Thanks, Stephane Chazelas for the pointer.
Output of script:
VARIABLE-TRACE> $variable = ""
VARIABLE-TRACE> $variable = "29"
Just initialized "$variable" to 29.
VARIABLE-TRACE> $variable = "29"
VARIABLE-TRACE> $variable = "87"
Just multiplied "$variable" by 3.
VARIABLE-TRACE> $variable = "87" |
|
Of course, the trap command has other uses
aside from debugging. Example 29-8. Running multiple processes (on an SMP box) #!/bin/bash
# multiple-processes.sh: Run multiple processes on an SMP box.
# Script written by Vernia Damiano.
# Used with permission.
# Must call script with at least one integer parameter
#+ (number of concurrent processes).
# All other parameters are passed through to the processes started.
INDICE=8 # Total number of process to start
TEMPO=5 # Maximum sleep time per process
E_BADARGS=65 # No arg(s) passed to script.
if [ $# -eq 0 ] # Check for at least one argument passed to script.
then
echo "Usage: `basename $0` number_of_processes [passed params]"
exit $E_BADARGS
fi
NUMPROC=$1 # Number of concurrent process
shift
PARAMETRI=( "$@" ) # Parameters of each process
function avvia() {
local temp
local index
temp=$RANDOM
index=$1
shift
let "temp %= $TEMPO"
let "temp += 1"
echo "Starting $index Time:$temp" "$@"
sleep ${temp}
echo "Ending $index"
kill -s SIGRTMIN $$
}
function parti() {
if [ $INDICE -gt 0 ] ; then
avvia $INDICE "${PARAMETRI[@]}" &
let "INDICE--"
else
trap : SIGRTMIN
fi
}
trap parti SIGRTMIN
while [ "$NUMPROC" -gt 0 ]; do
parti;
let "NUMPROC--"
done
wait
trap - SIGRTMIN
exit $?
: <<SCRIPT_AUTHOR_COMMENTS
I had the need to run a program, with specified options, on a number of
different files, using a SMP machine. So I thought [I'd] keep running
a specified number of processes and start a new one each time . . . one
of these terminates.
The "wait" instruction does not help, since it waits for a given process
or *all* process started in background. So I wrote [this] bash script
that can do the job, using the "trap" instruction.
--Vernia Damiano
SCRIPT_AUTHOR_COMMENTS |
| trap '' SIGNAL (two adjacent
apostrophes) disables SIGNAL for the remainder of the
script. trap SIGNAL restores
the functioning of SIGNAL once more. This is useful to
protect a critical portion of a script from an undesirable
interrupt. |
trap '' 2 # Signal 2 is Control-C, now disabled.
command
command
command
trap 2 # Reenables Control-C
|
Gotchas | Turandot: Gli enigmi sono tre, la morte una! Caleph: No, no! Gli enigmi sono tre, una la vita! | | Puccini |
Assigning reserved words or characters to variable names.
case=value0 # Causes problems.
23skidoo=value1 # Also problems.
# Variable names starting with a digit are reserved by the shell.
# Try _23skidoo=value1. Starting variables with an underscore is o.k.
# However . . . using just the underscore will not work.
_=25
echo $_ # $_ is a special variable set to last arg of last command.
xyz((!*=value2 # Causes severe problems.
# As of version 3 of Bash, periods are not allowed within variable names. |
Using a hyphen or other reserved characters in a variable name (or
function name).
var-1=23
# Use 'var_1' instead.
function-whatever () # Error
# Use 'function_whatever ()' instead.
# As of version 3 of Bash, periods are not allowed within function names.
function.whatever () # Error
# Use 'functionWhatever ()' instead. |
Using the same name for a variable and a function. This can make a
script difficult to understand.
do_something ()
{
echo "This function does something with \"$1\"."
}
do_something=do_something
do_something do_something
# All this is legal, but highly confusing. |
Using whitespace inappropriately.
In contrast to other programming languages, Bash can be quite
finicky about whitespace.
var1 = 23 # 'var1=23' is correct.
# On line above, Bash attempts to execute command "var1"
# with the arguments "=" and "23".
let c = $a - $b # 'let c=$a-$b' or 'let "c = $a - $b"' are correct.
if [ $a -le 5] # if [ $a -le 5 ] is correct.
# if [ "$a" -le 5 ] is even better.
# [[ $a -le 5 ]] also works. |
Assuming uninitialized variables (variables before a value is
assigned to them) are "zeroed out". An
uninitialized variable has a value of "null",
not zero.
#!/bin/bash
echo "uninitialized_var = $uninitialized_var"
# uninitialized_var = |
Mixing up = and -eq in
a test. Remember, = is for comparing literal
variables and -eq for integers.
if [ "$a" = 273 ] # Is $a an integer or string?
if [ "$a" -eq 273 ] # If $a is an integer.
# Sometimes you can mix up -eq and = without adverse consequences.
# However . . .
a=273.0 # Not an integer.
if [ "$a" = 273 ]
then
echo "Comparison works."
else
echo "Comparison does not work."
fi # Comparison does not work.
# Same with a=" 273" and a="0273".
# Likewise, problems trying to use "-eq" with non-integer values.
if [ "$a" -eq 273.0 ]
then
echo "a = $a"
fi # Aborts with an error message.
# test.sh: [: 273.0: integer expression expected |
Misusing string comparison
operators. Example 31-1. Numerical and string comparison are not equivalent #!/bin/bash
# bad-op.sh: Trying to use a string comparison on integers.
echo
number=1
# The following "while loop" has two errors:
#+ one blatant, and the other subtle.
while [ "$number" < 5 ] # Wrong! Should be: while [ "$number" -lt 5 ]
do
echo -n "$number "
let "number += 1"
done
# Attempt to run this bombs with the error message:
#+ bad-op.sh: line 10: 5: No such file or directory
# Within single brackets, "<" must be escaped,
#+ and even then, it's still wrong for comparing integers.
echo "---------------------"
while [ "$number" \< 5 ] # 1 2 3 4
do #
echo -n "$number " # This *seems to work, but . . .
let "number += 1" #+ it actually does an ASCII comparison,
done #+ rather than a numerical one.
echo; echo "---------------------"
# This can cause problems. For example:
lesser=5
greater=105
if [ "$greater" \< "$lesser" ]
then
echo "$greater is less than $lesser"
fi # 105 is less than 5
# In fact, "105" actually is less than "5"
#+ in a string comparison (ASCII sort order).
echo
exit 0 |
Sometimes variables within "test" brackets
([ ]) need to be quoted (double quotes). Failure to do so may
cause unexpected behavior. See Example 7-6, Example 16-5, and Example 9-6. Commands issued from a script may fail to execute because
the script owner lacks execute permission for them. If a user
cannot invoke a command from the command line, then putting it
into a script will likewise fail. Try changing the attributes of
the command in question, perhaps even setting the suid bit
(as root, of course). Attempting to use - as a redirection
operator (which it is not) will usually result in an unpleasant
surprise.
command1 2> - | command2 # Trying to redirect error output of command1 into a pipe...
# ...will not work.
command1 2>& - | command2 # Also futile.
Thanks, S.C. |
Using Bash version 2+
functionality may cause a bailout with error messages. Older
Linux machines may have version 1.XX of Bash as the default
installation.
#!/bin/bash
minimum_version=2
# Since Chet Ramey is constantly adding features to Bash,
# you may set $minimum_version to 2.XX, or whatever is appropriate.
E_BAD_VERSION=80
if [ "$BASH_VERSION" \< "$minimum_version" ]
then
echo "This script works only with Bash, version $minimum or greater."
echo "Upgrade strongly recommended."
exit $E_BAD_VERSION
fi
... |
Using Bash-specific functionality in a Bourne shell script
(#!/bin/sh) on a non-Linux machine
may cause unexpected behavior. A Linux system usually aliases
sh to bash, but this does
not necessarily hold true for a generic UNIX machine. Using undocumented features in Bash turns out to be a
dangerous practice. In previous releases of this
book there were several scripts that depended on the
"feature" that, although the maximum value
of an exit or return value was 255, that limit
did not apply to negative integers.
Unfortunately, in version 2.05b and later, that loophole
disappeared. See Example 23-9. A script with DOS-type newlines (\r\n)
will fail to execute, since #!/bin/bash\r\n
is not recognized, not the same as the
expected #!/bin/bash\n. The fix is to
convert the script to UNIX-style newlines.
#!/bin/bash
echo "Here"
unix2dos $0 # Script changes itself to DOS format.
chmod 755 $0 # Change back to execute permission.
# The 'unix2dos' command removes execute permission.
./$0 # Script tries to run itself again.
# But it won't work as a DOS file.
echo "There"
exit 0 |
A shell script headed by #!/bin/sh
will not run in full Bash-compatibility mode. Some Bash-specific
functions might be disabled. Scripts that need complete
access to all the Bash-specific extensions should start with
#!/bin/bash. Putting whitespace in front of
the terminating limit string of a here document will cause unexpected
behavior in a script.
A script may not export variables back
to its parent process, the shell,
or to the environment. Just as we learned in biology, a child
process can inherit from a parent, but not vice versa.
WHATEVER=/home/bozo
export WHATEVER
exit 0 |
bash$ echo $WHATEVER
bash$ |
Sure enough, back at the command prompt, $WHATEVER remains unset.
Setting and manipulating variables in a subshell, then attempting
to use those same variables outside the scope of the subshell will
result an unpleasant surprise. Example 31-2. Subshell Pitfalls #!/bin/bash
# Pitfalls of variables in a subshell.
outer_variable=outer
echo
echo "outer_variable = $outer_variable"
echo
(
# Begin subshell
echo "outer_variable inside subshell = $outer_variable"
inner_variable=inner # Set
echo "inner_variable inside subshell = $inner_variable"
outer_variable=inner # Will value change globally?
echo "outer_variable inside subshell = $outer_variable"
# Will 'exporting' make a difference?
# export inner_variable
# export outer_variable
# Try it and see.
# End subshell
)
echo
echo "inner_variable outside subshell = $inner_variable" # Unset.
echo "outer_variable outside subshell = $outer_variable" # Unchanged.
echo
exit 0
# What happens if you uncomment lines 19 and 20?
# Does it make a difference? |
Piping
echo output to a read may produce unexpected
results. In this scenario, the read
acts as if it were running in a subshell. Instead, use
the set command (as in Example 11-16). Example 31-3. Piping the output of echo to a read #!/bin/bash
# badread.sh:
# Attempting to use 'echo and 'read'
#+ to assign variables non-interactively.
a=aaa
b=bbb
c=ccc
echo "one two three" | read a b c
# Try to reassign a, b, and c.
echo
echo "a = $a" # a = aaa
echo "b = $b" # b = bbb
echo "c = $c" # c = ccc
# Reassignment failed.
# ------------------------------
# Try the following alternative.
var=`echo "one two three"`
set -- $var
a=$1; b=$2; c=$3
echo "-------"
echo "a = $a" # a = one
echo "b = $b" # b = two
echo "c = $c" # c = three
# Reassignment succeeded.
# ------------------------------
# Note also that an echo to a 'read' works within a subshell.
# However, the value of the variable changes *only* within the subshell.
a=aaa # Starting all over again.
b=bbb
c=ccc
echo; echo
echo "one two three" | ( read a b c;
echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
# a = one
# b = two
# c = three
echo "-----------------"
echo "Outside subshell: "
echo "a = $a" # a = aaa
echo "b = $b" # b = bbb
echo "c = $c" # c = ccc
echo
exit 0 |
In fact, as Anthony Richardson points out, piping to
any loop can cause a similar problem.
# Loop piping troubles.
# This example by Anthony Richardson,
#+ with addendum by Wilbert Berendsen.
foundone=false
find $HOME -type f -atime +30 -size 100k |
while true
do
read f
echo "$f is over 100KB and has not been accessed in over 30 days"
echo "Consider moving the file to archives."
foundone=true
# ------------------------------------
echo "Subshell level = $BASH_SUBSHELL"
# Subshell level = 1
# Yes, we're inside a subshell.
# ------------------------------------
done
# foundone will always be false here since it is
#+ set to true inside a subshell
if [ $foundone = false ]
then
echo "No files need archiving."
fi
# =====================Now, here is the correct way:=================
foundone=false
for f in $(find $HOME -type f -atime +30 -size 100k) # No pipe here.
do
echo "$f is over 100KB and has not been accessed in over 30 days"
echo "Consider moving the file to archives."
foundone=true
done
if [ $foundone = false ]
then
echo "No files need archiving."
fi
# ==================And here is another alternative==================
# Places the part of the script that reads the variables
#+ within a code block, so they share the same subshell.
# Thank you, W.B.
find $HOME -type f -atime +30 -size 100k | {
foundone=false
while read f
do
echo "$f is over 100KB and has not been accessed in over 30 days"
echo "Consider moving the file to archives."
foundone=true
done
if ! $foundone
then
echo "No files need archiving."
fi
} |
A related problem occurs when trying to write the
stdout of a tail -f
piped to grep.
tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
# The "error.log" file will not have anything written to it. |
-- Using "suid" commands within scripts is risky,
as it may compromise system security.
Using shell scripts for CGI programming may be problematic. Shell
script variables are not "typesafe", and this can cause
undesirable behavior as far as CGI is concerned. Moreover, it is
difficult to "cracker-proof" shell scripts. Bash does not handle the double slash
(//) string correctly. Bash scripts written for Linux or BSD systems may need
fixups to run on a commercial UNIX machine. Such scripts
often employ GNU commands and filters which have greater
functionality than their generic UNIX counterparts. This is
particularly true of such text processing utilites as tr. | Danger is near thee -- Beware, beware, beware, beware. Many brave hearts are asleep in the deep. So beware -- Beware. | | A.J. Lamb and H.W. Petrie |
Shell Wrappers
A "wrapper" is a shell script that embeds
a system command or utility, that saves a set of parameters
passed to that command.
Wrapping a script around a complex command line
simplifies invoking it. This is expecially useful
with 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 33-1. shell wrapper #!/bin/bash
# This is a simple script that removes blank lines from a file.
# No argument checking.
#
# You might wish to add something like:
#
# E_NOARGS=65
# if [ -z "$1" ]
# then
# echo "Usage: `basename $0` target-file"
# exit $E_NOARGS
# fi
# Same as
# sed -e '/^$/d' filename
# invoked from the command line.
sed -e /^$/d "$1"
# The '-e' means an "editing" command follows (optional here).
# '^' is the beginning of line, '$' is the end.
# This match 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 0 |
Example 33-2. A slightly more complex shell wrapper #!/bin/bash
# "subst", a script that substitutes one pattern for
#+ another in a file,
#+ i.e., "subst Smith Jones letter.txt".
ARGS=3 # Script requires 3 arguments.
E_BADARGS=65 # Wrong number of arguments passed to script.
if [ $# -ne "$ARGS" ]
# Test number of arguments to script (always a good idea).
then
echo "Usage: `basename $0` old-pattern new-pattern filename"
exit $E_BADARGS
fi
old_pattern=$1
new_pattern=$2
if [ -f "$3" ]
then
file_name=$3
else
echo "File \"$3\" does not exist."
exit $E_BADARGS
fi
# 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 literature on 'sed' for an in-depth explanation.
exit 0 # Successful invocation of the script returns 0. |
Example 33-3. A generic shell wrapper that writes to a logfile #!/bin/bash
# Generic shell wrapper that performs an operation
#+ and logs it.
# Must set the following two variables.
OPERATION=
# Can be a complex chain of commands,
#+ for example an awk script or a pipe . . .
LOGFILE=
# 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 33-4. A shell wrapper around an awk script #!/bin/bash
# pr-ascii.sh: Prints a table of ASCII characters.
START=33 # Range of printable ASCII characters (decimal).
END=125
echo " Decimal Hex Character" # Header.
echo " ------- --- ---------"
for ((i=START; i<=END; i++))
do
echo $i | awk '{printf(" %3d %2x %c\n", $1, $1, $1)}'
# The Bash printf builtin will not work in this context:
# printf "%c" "$i"
done
exit 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 33-5. A shell wrapper around another awk script #!/bin/bash
# Adds up a specified column (of numbers) in the target file.
ARGS=2
E_WRONGARGS=65
if [ $# -ne "$ARGS" ] # Check for proper no. of command line args.
then
echo "Usage: `basename $0` filename column-number"
exit $E_WRONGARGS
fi
filename=$1
column_number=$2
# Passing shell variables to the awk part of the script is a bit tricky.
# See the awk documentation for more details.
# A multi-line awk script is 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 single
do-it-all tool, a Swiss army knife, there is Perl. Perl
combines the capabilities of sed and
awk, and throws in a large subset of
C, to boot. It is modular and contains support
for everything ranging from object-oriented programming up to and
including the kitchen sink. Short Perl scripts lend themselves to
embedding in shell scripts, and there may even be some substance
to the claim that Perl can totally replace shell scripting
(though the author of this document remains skeptical). Example 33-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 is an embedded Perl script.\n";'
# 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, either
the Bash part or the Perl part will execute. Example 33-7. Bash and Perl scripts combined #!/bin/bash
# bashandperl.sh
echo "Greetings from the Bash part of the script."
# More Bash commands may follow here.
exit 0
# End of Bash part of the script.
# =======================================================
#!/usr/bin/perl
# This part of the script must be invoked with -x option.
print "Greetings from the Perl part of the script.\n";
# More Perl commands may follow here.
# End of Perl part of the script. |
bash$ bash bashandperl.sh
Greetings from the Bash part of the script.
bash$ perl -x bashandperl.sh
Greetings from the Perl part of the script.
|
|