Cover V14, i12
dec2005.tar

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.