Syntax Checking Example Syntax Checking Protect Yourself Example File Exists Example Verify Reading Keyboard Input Example Dot List Example Overwrite Where's My Print Gone? Temporary File Generation Filenames

Input & Output

As we saw in Pipes, Lists & Redirects, UNIX is flexible about where it takes its input and where it puts its output. This should also be true of any scripts you create. Have a care for the poor user who is trying to make use of your script. Give him a few clues if he goes wrong, tell him what's happening if there's likely to be a delay, etc. One of the worst things you can do in a script is take someone's input then display a frozen screen for 10 seconds. That is the most stressfull 10 seconds in that users life! Has it hung? Did I do it right? Is it still waiting for something from me? The best way to handle this is with some simple checks at the start of the script to test for syntax and immediate feedback when the user has entered something. If at all possible, make the script flexible enough to be able to handle alternate usages.

Syntax Checking:

When you write your script, you know its internal structure and specification. You know how it works and what it expects as input. Don't expect the users to be that aware, they usually assume you have done everything for them anyway, you might just as well - it'll save time later on!

If a script has 2 mandatory arguments (say input and output) then it's just plain silly to try and run it with only one. Its also not too bright to run it when there are three (er... like which two should it choose?). Thankfully there is a very simple check you can do to see if a script has been started with the correct number of arguments, and it relates to $# - a shell variable which is set when a script is invoked. It carries the number of arguments or passed parameters that were on the command line when it was executed. The following listing is something I used to use on most of my early scripts. A better solution can be found in the section on Functions.

Example syntax checking

if [ $# -ne 2 ]
then
    echo "Error in $0 - Invalid Argument Count"
    echo "Syntax:  $0 input_file output_file"
    exit
fi

This segment of code can be placed at the start of any script to check the users usage of the script. The $# parameter is compared to the constant 2 (in this case 2 arguments was the correct number of parameters for the script). If there is a match, nothing happens and the script procedes. If there is a mismatch, then the two lines are echoed out to the screen and the script exits promptly. Notice the messages that I have output. The first one says there is an error in this script (you might call a subscript, so it's as well to put the $0 name in here - the currently executing one) and you have supplied the wrong number of arguments. The next line tells the user what to do about it - here is the correct syntax. Most users can cope with this and it is only 6 lines of code. Don't forget to revise the argument names and the count constant when you use this code in one of your own scripts.

Protect Yourself:

You think that's all there is to it? Come on, users are much more clever! How about one of these tests next? Could save you time later explaining why the users latest work has gone missing.

Example file exists

if [ ! -f $infile ]
then
    echo "Input file [$infile] not found - Aborting"
    exit
fi

Note the use of the test not flag (!) in front of the -f (file exists) flag. Or how about:

Example verify

if [ -f $outfile ]
then
    echo "Output file [$outfile] already exists"
    /usr/5bin/echo "Okay to overwrite? ( y/n ) : \c"
    read answer
    if [ "$answer" = "n" ] || [ "$answer" = "N" ]
    then
	echo "Aborting"
	exit
    fi
fi

Well, it might work. But there is a failing with this second example. The test of the user answer tests for a "NO!" response. If the user misses the key, it will carry on. Far better to test for a "YES!" response which if missed just fails safe. Revise the test line to:

if [ "£answer" != "y" ] && [ "$answer" != "Y" ]

Note the use of the logical and (&&) instead of the logical or (||) here.

Reading Keyboard Input:

There is also a second fault in Example Verify but in this case it is less relevent as the script is about to exit anyway. Whenever you use the read statement to get some user interaction into the script, remember to tell the user that the input has been accepted. This is most commonly done by simply putting a blank echo on the next line thus:

read $answer
echo ""

This guarentees the user sees the cursor move after (s)he hits the return key. Try not to overuse the clear command, as users tend to get upset when the text they were just about to pick up with the mouse gently scrolls off the top of the screen. I also try to indicate something is happening when the processing time is going to be a bit lengthy. A simple "please wait" can be useful, or if you are running a loop which takes a few seconds to complete each cycle, try echoing out a dot list using something like:

Example dot list

for next_dir in `ls -1 $source_dirs`
do
    /usr/5bin/echo ".\c"
    cd $next_dir
    for file in `ls -1 *log`
    do
	mv $file $destination_dir/.
    done
    cd ..
done

Note that the destination directory is terminated here with a "/." symbol. This is another safety feature which protects against spelling errors or missing directories. Without this symbol, UNIX will simply move all the files into a new file called $destination_dir, with the result that all the files are removed except the last one processed, which is just moved (all the rest are overwritten). With this symbol (/.), UNIX will output an error message if the directory does not exist and will not move any files. That's much easier than restoring a backup!

There are several versions of the echo command on the system and most shells have one of their own. However, the one you find in /usr/5bin is very useful as it gives you control of special characters which the others don't. Check out the man pages yourself, but the \c tells echo to "forget the newline" at the end. This outputs a line of dots which gets longer as the loop cycles through. If you really wanted to be flash, you could check the length of the loop first and output an arrow on the line above to give the user a clue as to how long (s)he has to wait:

Processing             v
Please Wait ........

There is another trick which I have used when delivering status messages to the user. This makes use of a variable containing backspaces to overwrite a message repeatedly until the task is complete. It works like this:

Example overwrite

bspaces='^H^H^H^H^H^H^H^H^H^H^H^H'	# 12 backspaces
myecho='/usr/5bin/echo'			# Special Echo
/usr/5bin/echo "This process will run for $more seconds\c"
while $more				# Loop exits at 0
do
    more=`expr $more - 1`		# Decrement Counter
    sleep 1				# Do Something
    $myecho "$bspaces$more seconds\c"	# Overwrite string
done
echo ""					# Terminate string

First create a backspace string making sure the length in backspaces will cover the part you wish to overwrite. Next, echo out the first message minus a newline. Then enter the loop construct to do the process (sleep in this case!). Notice the while is not using test here but just looks at a variable. If you remember when we looked at while earlier, you should recall that while exits when test returns a zero. Well the loop counter $more can do that when it has been decremented enough, so we can do away with the test - another saving. Inside the loop, the counter is decremented and the next echo statement nimbly backspaces over the last 12 characters of the original message line, then echos out the new 12 characters over the top. On leaving the loop, the echo is terminated with a blank string, which provides the final newline.

Where's My Print Gone?

Is a common cry from users when they haven't got a clue where their default printer is located. You can be a real help here by setting each users set-up files to include a default printer environment variable and make sure all your printing scripts know how to use it. In the users .cshrc or .profile (whichever they use), put a line like:

setenv PRINTER hplj2		(.cshrc)

or...

export PRINTER=hplj2		(.profile)

Then in any script you can use this variable as the destination printer for the users output. Just use something like:

lpr -P$PRINTER $filename

Temporary File Generation:

Of course, the same thing applies to temporary files too. Sometimes you can get away with storing a list in a variable, sometimes you can't. When that happens you probably create a temporary file. And where do you create this file exactly? Well hopefully, you will have the good sence to put it in the /tmp directory where it will not clog up the users home directory. Do you remember to delete them after use too? I do hope your not one of those who rely on a system re-boot to empty out the /tmp directory. There is a simple solution to these problems and as always, it requires a bit of thought to work out a consistant but flexible scheme. This is what I worked out.

Filenames

  1. Always locate the temporary files in the /tmp directory
  2. Always start the name with something unique for the scripts I create
  3. Always include the calling script name in the file
  4. Always use an extension that indicates contents
  5. Allow several users to call the same script simultaneously
  6. Allow a script to create several such files without conflict

Number 1 is easy, I do this all the time. For number 2, I chose the characters db_ as most of my scripts are related to database work. Number 3 is easy, the $0 parameter is always available to lend a hand here. For number 4, I have settled on a three character extension (most users have seen DOS) and I try to stick to a firm scheme of mnemonics to indicate contents as follows:

tmptemporary file - content could be anything
lstfile contains a list of something
datfile contains data
mnufile contains a menu
sqlfile contains SQL_Plus* statements
txtfile contains text
logfile is a log of process or user activity
ftpfile contains ftp commands

For number 5, there is another useful script variable called $$ which is the process id. No two users or shells or tasks can have the same process id concurrently. Lastly, you need to be able to cope with an additional character or two in the filename passed as an argument. So put the lot together and what have we got? We have this:

a_temp_file="/tmp/db_$0_$$_$arg.tmp"
rm -f $a_temp_file
touch $a_temp_file

The rm and touch commands make sure we always start in a known condition, the file is there but empty. See the section on functions to see how this is packaged up into a single call such as:

tmp0=`s_tmp 0`

Notice with this scheme, how easy it is to clean up at the end of a script. You just need to remove the files which contain the script name plus the $$. And if two people are using the script concurrently, the $$ will guarentee that they both have unique filenames.

Home Next Preface Introduction Basic Shells Shell Syntax Built-In Commands Command Substitution Startup & Environment Pipes, Lists & Redirection Input & Output Using Files Design Considerations Functions Debugging Putting It All Together Appendix Code Examples
Page 209
This page was brought to you by rhreepe@injunea.demon.co.uk