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.
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.
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.
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.
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:
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.
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:
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:
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.
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
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.
- Always locate the temporary files in the /tmp directory
- Always start the name with something unique for the scripts
I create
- Always include the calling script name in the file
- Always use an extension that indicates contents
- Allow several users to call the same script simultaneously
- 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:
tmp | temporary file - content could be anything |
lst | file contains a list of something |
dat | file contains data |
mnu | file contains a menu |
sql | file contains SQL_Plus* statements |
txt | file contains text |
log | file is a log of process or user activity |
ftp | file 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.
|