John
& Ed's Scripting Screwups
Ed Schaefer and John Spurgeon
We believe that you don't learn anything or do something without
making mistakes. Because this is our last column of the year, we'd
like to discuss this year's blunders and enhancements:
- We enhanced the ls_floppy script that was part of our "Manipulating
Floppy Disk Images on Solaris" column.
- We plugged a security hole in the share screen section of our
"Using Screen in Scripts" column.
- We fixed an exit code/return value bug in our semaphore script
as described in "Queuing Jobs with qjob".
- We changed the pushd/popd functions as presented in Bill Rosenblatt's
Learning the Korn Shell. This isn't really our bug, but
it keeps with the theme of the column.
Floppy Enhancement
Our "Manipulating Floppy Disk Images on Solaris" column presented
three shell scripts for managing the floppy drive. Originally, the
scripts were developed with Solaris 7. When upgrading to Solaris
9, the ls_floppy script failed because one of the floppy commands,
volrmmount, wasn't making a temporary directory quickly enough.
The script's next command was executing before the directory existed.
To fix the problem, we inserted a five-second sleep after executing
the volrmmount command and before executing the change directory
command:
.
.
sleep 5
if cd $dir
then
# list contents of the floppy
ls $options
.
.
Using Screen Security Problem
Our major blunder of the year was in the April, "Using Screen
in Scripts" column. Sharp-eyed readers Kevin Turner and Rod Knowlton
pointed out a security hole in the "Using sudo in Share Screens"
sections. Our share-screen script allows users to share screens.
Unfortunately, when sharing a root-generated screen, simply by pressing
"Ctrl-A c", another screen is generated with root access -- very
undesirable.
To solve our share screen problem, we have abandoned screen. Presently,
we are planning to use the Expect script kibitz. The kibitz script,
bundled with the new versions of expect, allows two users to interact
with one shell. The kibitz script is easy to use. From the kibitz
MAN page:
To start kibitz, user1 runs kibitz with the argument of the user
to kibitz. For example:
kibitz user2
kibitz starts a new shell (or another program, if given on the
command line), while prompting user2 to run kibitz. If user2 runs
kibitz as directed, the keystrokes of both users become the input
of the shell. Similarly, both users receive the output from the
shell.
Finally, the online screen documentation link in our original
column is dead; see the reference section for a replacement.
Return Code Bug
In the "Queuing Jobs with qjob" column, we reported fixing a bug
in our semaphore script but didn't mention what that bug was. The
bug is obscure enough that we feel compelled to review it.
The gist of the problem is that we misidentified the exit code/return
value of a function when used in conjunction with the negation operator:
if ! keep_trying
then
exit $?
fi
We assumed that if the keep_trying function returns a value greater
than zero, the not comparison evaluates to true, and
the exit code equals the keep_trying function's return value. Unfortunately,
this is a bad assumption. The negation operator resets the exit code
masking whatever the keep_trying function's return value was.
Consider this example:
keep_trying()
{
return 5
}
if ! keep_trying
then
echo $? # echo's 0
fi
Because keep_trying's return value is false, the negation operator
sets the exit code to 0, true, or not false.
Remember that negation returns true when evaluating not
false and false when evaluating not true. If this
statement is confusing, execute the following comparisons:
if ! false
then
echo $? # echo's 0
fi
if ! true
then
:
else
echo $? # echo's 1
fi
If you really want the function's return value, do not use negation,
as in this example:
keep_trying()
{
return 5
}
if keep_trying
then
echo "true: $?"
else
echo "false: $?" # echo's 5
fi
The pushd & popd Problem
We like Bill Rosenblatt's Learning the Korn Shell, but
we disagree with his implementation of pushd and popd.
The pushd and popd functions "enable you to move to
another directory temporarily and have the shell remember where
you were". (Rosenblatt's functions are a subset of the C shell functions
of the same name.)
Basically, executing a push with an argument saves the argument
and changes to that destination; executing a pop enough times --
no matter how many times push was executed -- changes to the original
directory.
Rosenblatt emulates pushing and popping directory names by storing
them in shell variable DIRSTACK. When implementing Rosenblatt's
functions, we discovered the following three issues:
1. In the pushd function, every argument pushed onto the
stack must be delimited by a space in order to pop it off:
DIRSTACK=${DIRSTACK#* }
Unfortunately, the first argument pushed onto the stack is not space
delimited; thus, the first argument never gets popped.
2. In the pushd function, Rosenblatt executes a change
directory before popping, which incorrectly initializes the stack.
This causes the stack not to return to the original directory.
3. In the popd function, Rosenblatt checks for a null stack
before changing to the destination directory. Ultimately, this executes
a change directory command with a null argument placing users back
in their home directory, which is probably not the original directory.
Setting the Environment
In the rest of the column, we'll explain our fixes to Rosenblatt's
functions. Rosenblatt presents his solutions as functions. For demonstration
purposes, we are presenting his code (Listing 1, br_pushd, Listing
2, br_popd) and our solution (Listing 3, our_pushd, Listing 4, our_popd)
as scripts.
We've also created four aliases that easily source the scripts:
br_pushd='. /home/jspurgeo/sandbox/br_pushd'
br_popd='. /home/jspurgeo/sandbox/br_popd'
our_pushd='. /home/jspurgeo/sandbox/our_pushd'
our_popd='. /home/jspurgeo/sandbox/our_popd'
To ease debugging, the following alias prints the DIRSTACK shell variable
surrounded by double-quotes:
print_stack='echo DIRSTACK=\"$DIRSTACK\"'
The Terminating Space Problem
Assuming our original directory is /home/jspurgeo/sandbox, using
Rosenblatt's example, push a directory onto the stack:
$ br_pushd /etc
/etc /etc
Executing multiply br_popd calls verifies that the stack never
empties and never returns to the original directory. Execute the print_stack
alias to view the stack:
DIRSTACK="/etc"
The Korn shell expression requires a space at the end of the first
argument. In the pushd function, to obtain the terminating
space, change this code:
DIRSTACK="$dirname ${DIRSTACK:-$PWD}"
to:
DIRSTACK="$dirname ${DIRSTACK:-$PWD }"
With this change, execute the test again:
$ br_pushd /etc
/etc /etc
Execute br_popd, view the stack with print_stack, and
verify a space exists:
DIRSTACK="/etc "
Now, execute br_popd twice, and note receiving a "stack empty"
message, and that a call to print_stack displays an empty stack:
DIRSTACK=""
The Stack Initialization Problem
You probably noticed that although the stack empties, it doesn't
ultimately return to the original directory. This happens because
the stack is initialized with the wrong directory. Simply flip the
following two lines:
cd $dirname
DIRSTACK="$dirname ${DIRSTACK:-$PWD }"
to read:
DIRSTACK="$dirname ${DIRSTACK:-$PWD }"
cd $dirname
Now, execute the push test again:
$ br_pushd /etc
/etc /home/jspurgeo/sandbox
Note that the stack reads "/etc /home/jspurgeo/sandbox" where before
the output read "/etc /etc".
The Null Stack Check Problem
Running the push/pop test again shows the user ultimately back
in his home directory:
$ pwd
/home/jspurgeo/sandbox
$ br_pushd /etc
/etc /home/jspurgeo/sandbox
$ br_popd
/home/jspurgeo/sandbox
$ print_stack
DIRSTACK="/home/jspurgeo/sandbox "
$ br_popd
/home/jspurgeo
$ br_popd
stack empty, still in /home/jspurgeo.
Ending in the user's home directory happens because the Unix cd
command is executed with a null argument when the stack empties. This
happens because Rosenblatt performs the stack null check before popping
an argument from the stack:
if [[ -n $DIRSTACK ]]; then
DIRSTACK=${DIRSTACK#* }
.
.
Fix the problem by popping the argument before performing the null
check:
DIRSTACK=${DIRSTACK#* }
if [[ -n $DIRSTACK ]]; then
.
.
Perform the push/pop test again and verify returning to the original
directory:
$ pwd
/home/jspurgeo/sandbox
$ br_pushd /etc
/etc /home/jspurgeo/sandbox
$ br_popd
/home/jspurgeo/sandbox
$ br_popd
stack empty, still in /home/jspurgeo/sandbox.
References
Online Man page for screen -- http://www.delorie.com/gnu/docs/screen/screen.html
Manipulating Floppy Disk Images on Solaris -- http://www.samag.com/documents/s=9598/sam0504j/
Queuing Jobs with qjob -- http://www.samag.com/documents/s=9820/sam0508h/0508h.htm
Rosenblatt, Bill. 1993. Learning the Korn Shell. Sebastopol,
CA: O'Reilly & Associates.
Using Screen in Scripts -- http://www.samag.com/documents/s=9598/sam0504k/0504k.htm
John Spurgeon is a software developer and systems administrator
for Intel's Factory Information Control Systems, IFICS, in Aloha,
Oregon. Outside of work, he enjoys turfgrass management, triathlons,
ultra-marathon cycling, and spending time with his family.
Ed Schaefer is a frequent contributor to Sys Admin.
He is a software developer and DBA for Intel's Factory Information
Control Systems, IFICS, in Aloha, Oregon. Ed also hosts the monthly
Shell Corner column on UnixReview.com. He can be reached at: shellcorner@comcast.net. |