Introduction to Shell Programming
Introduction to Shell Programming
Programming
Chapter 8
Shell Programming
Introduction
The Basics
A Basic Program
It is traditional at this stage to write the standard "Hello World" program. To
do this in a shell program is so obscenely easy that we’re going to examine
something a bit more complex − a hello world program that knows who you
are...
To create your shell program, you must first edit a file − name it something
like "hello", "hello world" or something equally as imaginative − just don’t
call it "test" − we will explain why later.
In the editor, type the following (or you could go to the 85321 website/CD−
ROM and cut and paste the text from the appropriate web page)
PATH=$PATH:.
Or, execute the program with an explicit path:
./helloworld
This format isn’t set in stone, but use common sense and write fairly self
documenting programs.
Version Control Systems
Those of you studying software engineering may be familiar
with the term, version control. Version control allows you to
keep copies of files including a list of who made what changes
and what those changes were. Version control systems can be
very useful for keeping track of source code and is just about
compulsory for any large programming project.
Linux comes with CVS (Concurrent Versions System) a
widely used version control system. While version control
may not seem all that important it can save a lot of heartache.
Many large sites will actually keep copies of system
configuration files in a version control system.
Line three, echo "Hello $LOGNAME, I hope you have a nice
day!" is actually a command. The echo command prints text to the screen.
Normal shell rules for interpreting special characters apply for the echo
statement, so you should generally enclose most text in "". The only tricky bit
about this line is the $LOGNAME . What is this?
$LOGNAME is a shell variable; you can see it and others by typing "set" at the
shell prompt. In the context of our program, the shell substitutes the
$LOGNAME value with the username of the person running the program, so the
output looks something like:
Hello jamiesob, I hope you have a nice day!
David Jones, Bruce Jamieson (25/02/00) Page 5
85321, Systems Administration Chapter 8: Shell
Programming
All variables are referenced for output by placing a "$" sign in front of them −
we will examine this in the next section.
Exercises
Why?
When placing the output of a command into a shell variable, the shell removes
all the end−of−line markers, leaving a string separated only by spaces. The
Exercise
Type in the above program and run it. Explain what is happening.
Would the above program work if "ls −al" was used rather than
"ls" − Why/why not?
Predefined Variables
There are many predefined shell variables, most established during your login.
Examples include $LOGNAME, $HOSTNAME and $TERM − these names are
not always standard from system to system (for example, $LOGNAME can also
be called $USER). There are however, several standard predefined shell
variables you should be familiar with. These include:
$$ (The current process ID)
$? (The exits status of last command)
How would these be useful?
$$
$$ is extremely useful in creating unique temporary files. You will often find
the following in shell programs:
some command > /tmp/temp.$$
.
.
some commands using /tmp/temp.$$>
.
.
rm /tmp/temp.$$
/tmp/temp.$$ would always be a unique file − this allows several people
to run the same shell script simultaneously. Since one of the only unique
things about a process is its PID (Process−Identifier), this is an ideal
component in a temporary file name. It should be noted at this point that
temporary files are generally located in the /tmp directory.
$?
$? becomes important when you need to know if the last command that was
executed was successful. All programs have a numeric exit status − on UNIX
systems 0 indicates that the program was successful, any other number
indicates a failure. We will examine how to use this value at a later point in
time.
Is there a way you can show if your programs succeeded or failed? Yes! This
is done via the use of the exit command. If placed as the last command in
your shell program, it will enable you to indicate, to the calling program, the
exit status of your script.
Exercise
Explain line by line what this program is doing. What would happen if
the user didn’t enter any parameters? How could you fix this?
Why?
The shell only has 9 command−line parameters defined at any one time $1 to
$9. When the shell sees "$10" it interprets this as "$1" with a "0" after it.
This is where $10 in the above results in a0. The a is the value of $1 with the
0 added.
On the otherhand $* allows you to see all the parameters you typed!
So how do you access $10, $11 etc. To our rescue comes the shift
command. shift works by removing the first parameter from the parameter
list and shuffling the parameters along. Thus $2 becomes $1, $3 becomes $2
etc. Finally, (what was originally) the tenth parameter becomes $9.
However, beware! Once you’ve run shift, you have lost the original value
of $1 forever − it is also removed from $* and $@. shift is executed by,
well, placing the word "shift" in your shell script, for example:
#!/bin/bash
echo $1 $2 $3
David Jones, Bruce Jamieson (25/02/00) Page 9
85321, Systems Administration Chapter 8: Shell
Programming
shift
echo $1 $2 $3
Exercise
$@
Is expanded to all the command−line parameters joined as a single word
David Jones, Bruce Jamieson (25/02/00) Page 10
85321, Systems Administration Chapter 8: Shell
Programming
with usually a space seperating them (the separating character can be
changed).
$*
Expands to all the command−line parameters BUT each command−line
parameter is treated as if it is surrounded by double quotes "". This is
especially important when one of the parameters contains a space.
Let’s modify the our example script so that $@ and $* are surrounded by ""
#for name in "$*"
for name in "$@"
do
echo param is $name
done
Now look at what happens when we run it using the same parameters as
before. Again the $@ version is executed first then the $* version.
[david@faile david]$ [Link] hello "how are you" today 1 2 3
param is hello
param is how are you
param is today
param is 1
param is 2
param is 3
[david@faile david]$ [Link] hello "how are you" today 1 2 3
param is hello how are you today 1 2 3
With the second example, where $* is used, the difference is obvious. The
first example, where $@ is used, shows the advantage of $@. The second
parameter is maintained as a single parameter.
Character Purpose
\a alert (bell)
\b backspace
\c don’t display the trailing newline
\n new line
\r carriage return
\t horizontal tab
\v vertical tab
\\ backslash
\nnn the character with ASCII number nnn (octal)
Table 8 .2
ec h o bac ksla sh o ptio ns
(This program would be useful for those with a very short memory)
At the moment, we’ve only examined reading from STDIN (standard input
a.k.a. the keyboard) and STDOUT (standard output a.k.a. the screen) − if we
want to be really clever we can change this.
What do you think the following does?
read X < afile
or what about
echo $X > anotherfile
If you said that the first read the contents of afile into a variable $X and the
second wrote the value of $X to anotherfile you’d almost be correct. The
read operation will only read the first line (up to the end−of−line marker)
from afile − it doesn’t read the entire file.
You can also use the ">>" and "<<" redirection operators.
Exercises
would do? What do you think $X would hold if the input was:
Dear Sir
I have no idea why your computer blew up.
Kind regards, me.
END
Scenario
So far we have been dealing with very simple examples − mainly due to the
fact we’ve been dealing with very simple commands. Shell scripting was not
invented so you could write programs that ask you your name then display it.
For this reason, we are going to be developing a real program that has a useful
purpose. We will do this section by section as we examine more shell
programming concepts. While you are reading each section, you should
consider how the information could assist in writing part of the program.
The actual problem is as follows:
You’ve been appointed as a system administrator to an academic department
within a small (anonymous) regional university. The previous system
administrator left in rather a hurry after it was found that department’’s main
server had being playing host to plethora of pornography, warez (pirate
software) and documentation regarding interesting alternative uses for various
farm chemicals.
There is some concern that the previous sys admin wasn’t the only individual
within the department who had been availing themselves to such wonderful
and diverse resources on the Internet. You have been instructed to identify
those persons who have been visiting "undesirable" Internet sites and advise
them of the department’s policy on accessing inappropriate material
(apparently there isn’t one, but you’ve been advised to improvise). Ideally,
you will produce a report of people accessing restricted sites, exactly which
sites and the number of times they visited them.
To assist you, a network monitoring program produces a datafile containing a
list of users and sites they have accessed, an example of which is listed below:
FILE: netwatch
jamiesob [Link]
tonsloye [Link]
tonsloye [Link]
root [Link]
jamiesob [Link]
jamiesob [Link]
jamiesob [Link]
tonsloye [Link]
tonsloye [Link]
janesk [Link]
[Link]
[Link]
[Link]
[Link]
It is your task to develop a shell script that will fulfil these requirements (at
the same time ignoring the privacy, ethics and censorship issues at hand :)
(Oh, it might also be an idea to get Yahoo! to remove the link to your main
server under the /Computers/Software/Hackz/Warez/Sites listing... ;)
And if you require even more complexity, you can issue the if command as:
if command
then
do other commands
elif anothercommand
do other commands
fi
To test these structures, you may wish to use the true and false UNIX
commands. true always sets $? to 0 and false sets $? to 1 after
executing.
Remember: if tests the exit code of a command − it isn’t used to compare
values; to do this, you must use the test command in combination with the
if structure − test will be discussed in the next section.
What if you wanted to test the output of two commands? In this case, you can
use the shell’s && and || operators. These are effectively "smart" AND and
OR operators.
The && works as follows:
command1 && command2
command2 will only be executed if command1 succeeds.
The || works as follows:
command1 || command2
command2 will only be executed if command1 fails.
These are sometimes referred to as "short circuit" operators in other languages.
Given our problem, one of the first things we should do in our program is to
check if our datafiles exist. How would we do this?
#!/bin/bash
# FILE: scanit
if ls netwatch && ls netnasties
then
echo "Found netwatch and netnasties!"
else
echo "Can not find one of the data files − exiting"
exit 1
fi
Exercise
Enter the code above and run the program. Notice that the output from
the ls commands (and the errors) appear on the screen − this isn’t a
very good thing. Modify the code so the only output to the screen is
one of the echo messages.
Testing Testing...
Perhaps the most useful command available to shell programs is the test
command. It is also the command that causes the most problems for first time
shell programmers − the first program they ever write is usually
(imaginatively) called test − they attempt to run it − and nothing happens −
Expressions, expressions!
So far we’ve only examined expressions containing string based comparisons.
The following tables list all the different types of comparisons you can
perform with the test command.
Expression True if
−z string length of string is 0
−n string length of string is not 0
string1 = string2 if the two strings are identical
string != string2 if the two strings are NOT identical
String if string is not NULL
Ta ble 8.3
Str ing ba se d te sts
Expression True if
int1 −eq int2 first int is equal to second
int1 −ne int2 first int is not equal to second
int1 −gt int2 first int is greater than second
int1 −ge int2 first int is greater than or equal to second
int1 −lt int2 first int is less than second
int1 −le int2 first int is less than or equal to second
Table 8 .4
Nume ric te sts
Expression True if
−r file File exists and is readable
−w file file exists and is writable
−x file file exists and is executable
−f file file exists and is a regular file
−d file file exists and is directory
−h file file exists and is a symbolic link
−c file file exists and is a character special file
−b file file exists and is a block special file
−p file file exists and is a named pipe
−u file file exists and it is setuid
−g file file exists and it is setgid
−k file file exists and the sticky bit is set
−s file file exists and its size is greater than 0
Table 8 .5
File te sts
Expression Purpose
! reverse the result of an expression
−a AND operator
−o OR operator
( expr ) group an expression, parentheses have special
meaning to the shell so to use them in the test
command you must quote them
Table 8 .6
Lo gic oper ator s with test
Exercise
Modify the code for scanit so it uses the test command to see if the
datafiles exists.
Exercise
Write a shell script that inputs a date and converts it into a long date
form. For example:
$~ > mydate 12/3/97
12th of March 1997
$~ > mydate
Enter the date: 1/11/74
1st of November 1974
while
The format of the while construct is:
while command
do
commands
done
(while command is true, commands are executed)
Example
while [ $1 ]
do
echo $1
Exercise
Modify the above code so that the site is compared with all sites in the
prohibited sites file (netnasties). Do this by using another
while loop. If the user has visited a prohibited site, then echo a
message to the screen.
for
The format of the for construct is:
for variable in list_of_variables
do
commands
done
(for each value in list_of_variables, "commands" are executed)
Example
We saw earlier in this chapter examples of the for command showing the
difference between $* and $@.
Another example
for count in 10 9 8 7 6 5 4 3 2 1
do
echo −n "$count.."
done
echo
David Jones, Bruce Jamieson (25/02/00) Page 21
85321, Systems Administration Chapter 8: Shell
Programming
Modifying scanit
for checkuser in $*
do
while read buffer
do
while read checksite
do
user=‘echo $buffer | cut −d" " −f1‘
site=‘echo $buffer | cut −d" " −f2‘
if [ "$user"="$checkuser" −a "$site"="$checksite"
]
then
echo "$user visited the prohibited site $site"
fi
done < netnasties
done < netwatch
done
David Jones, Bruce Jamieson (25/02/00) Page 22
85321, Systems Administration Chapter 8: Shell
Programming
Can you see the problem?
How do we identify the problem? Well let’s start by thinking about what the
problem is. The problem is that it is showing too many lines. The script is not
excluding lines which should not be displayed. Where are the lines displayed?
The only place is within the if command. This seems to imply that the problem
is that the if command isn’t working. It is matching too many times, in fact it
is matching all of the lines.
The problem is that if command is wrong or not working as expected.
How is it wrong?
Common mistakes with the if command include
So what is happening
So what is actually happening? Why is the test always returning true. We
know this because the script displays a line for all the users and all the sites.
David Jones, Bruce Jamieson (25/02/00) Page 23
85321, Systems Administration Chapter 8: Shell
Programming
To find the solution to this problem we need to take a look at the manual page
for the test command. On current Linux computers you can type man test and
you will see a manual page for this command. However, it isn’t the one you
should look at.
Type the following command which test. It should tell you where the
executable program for test is located. Trouble is that on current Linux
computers it won’t. That’s because there isn’t one. Instead the test command is
actually provided by the shell, in this case bash. To find out about the test
command you need to look at the man page for bash.
The other approach would be to look at Table 8.3 from chapter 8 of the 85321
textbook. In particular the last entry which says that if the expression in a test
command is a string then the test command will return true if the string is
non−zero (i.e. it has some characters).
Here are some examples to show what this actually means.
In these examples I’m using the test command by itself and then using the
echo command to have a look at the value of the $? shell variable. The $? shell
variable holds the return status of the previous command.
For the test command if the return status is 0 then the expression was true. If it
is 1 then the expression as false.
Exercises
What will be the return status of the following test commands? Why?
["hello"]
[ $HOME ]
[ "‘hello‘" ]
The above code is very inefficient IO wise − for every entry in the netwatch
file, the entire netnasties file is read in. Modify the code so that the while
loop reading the netnasties file is replaced by a for loop. (Hint: what does:
BADSITES=‘cat netnasties‘
do?)
EXTENSION: What other IO inefficiencies does the code have? Fix them.
Solution in C
#include <stdio.h>
void main( void )
{
int line_count = 0;
FILE *infile;
char line[500];
infile = fopen( "the_file", "r" );
while ( ! feof( infile ) )
{
fgets( line, 500, infile );
line_count++;
}
printf( "Number of lines is %d\n", line_count−1 );
}
Pretty simple to understand? Open the file, read the file line by line, increment
a variable for each line and then display the variable when we reach the end of
the file.
wc −l the_file
David Jones, Bruce Jamieson (25/02/00) Page 27
This may appear to be a fairly trivial example. However, it does emphasise a
very important point. You don’t want to use the shell commands like a
normal procedural programming language. You want to make use of the
available UNIX commands where ever possible.
The lesson to draw from these figures is that solutions using the C program
and the wc command have the same efficiency but using the wc command is
much quicker.
The shell programming solution which was written like a C program is
horrendously inefficient. It is tens of thousands of times slower than the other
two solutions and uses an enormous amount of resources.
The problem
Obviously using while loops to read a file line by line in a shell program is
inefficient and should be avoided. However, if you think like a C programmer
you don’t know any different.
When writing shell programs you need to modify how you program to make
use of the strengths and avoid the weaknesses of shell scripting. Where
possible you should use existing UNIX commands.
see if the user has visited one of the sites listed in the netnasties file
To word it another way, you are searching for lines in a file which match a
certain criteria. What UNIX command does that?
Number of processes
Another factor to keep in mind is the number of processes your shell script
creates. Every UNIX command in a shell script will create a new process.
Creating a new process is quite a time and resource consuming job performed
by the operating system. One thing you want to do is to reduce the number of
new processes created.
Let’s take a look at the shell program solution to our problem
count=0
while read line
do
count=‘expr $count + 1‘
done < the_file
echo Number of lines is $count
For a file with 1911 lines this shell program is going to create about 1913
processes. 1 process for the echo command at the end, one process to for a
new shell to run the script and 1911 processes for the expr command. Every
time the script reads a line it will create a new process to run the expr
command. So the longer the file the less efficient this script is going to get.
One way to address this problem somewhat is to use the support that the bash
shell provides for arithmetic. By using the shell’s arithmetic functions we can
avoid creating a new process because the shell process will do it.
Our new shell script looks like this
count=0
while read line
do
count=$[ $count + 1 ]
done < /var/log/messages
We have a slightly bigger file but even so the speed is much, much better.
However, the speed is still no where as good as simply using the wc
command.
85321, Systems Administration Chapter 8: Shell
Programming
until
The format of the until construct is:
until command
do
commands
done
("commands" are executed until "command" is true)
Example
Redirection
Not just the while − do − done loops can have IO redirection; it is possible
to perform piping, output to files and input from files on if, for and until
as well. For example:
if true
then
read x
read y
read x
fi < afile
This code will read the first three lines from afile. Pipes can also be used:
read BUFFER
while [ "$BUFFER" != "" ]
do
echo $BUFFER
read BUFFER
done | todos > tmp.$$
This code uses a non−standard command called todos. todos converts
UNIX text files to DOS textfiles by making the EOL (End−Of−Line) character
equivalent to CR (Carriage−Return) LF (Line−Feed). This code takes STDIN
(until the user enters a blank line) and pipes it into todos, which in turn
converts it to a DOS style text file ( tmp.$$ ) . In all, a totally useless
program, but it does demonstrate the possibilities of piping.
Functional Functions
A symptom of most usable programming languages is the existence of
functions. Theoretically, functions provide the ability to break your code into
reusable, logical compartments that are the by product of top−down design. In
practice, they vastly improve the readability of shell programs, making it
easier to modify and debug them.
An alternative to functions is the grouping of code into separate shell scripts
and calling these from your program. This isn’t as efficient as functions, as
functions are executed in the same process that they were called from;
however other shell programs are launched in a separate process space − this is
inefficient on memory and CPU resources.
function_name()
{
somecommands
}
Functions are called by:
function_name parameter_list
YES! Shell functions support parameters. $1 to $9 represent the first nine
parameters passed to the function and $* represents the entire parameter list.
The value of $0 isn’t changed. For example:
#!/bin/bash
# FILE: catfiles
catfile()
{
for file in $*
do
cat $file
done
}
FILELIST=‘ls $1‘
cd $1
catfile $FILELIST
This is a highly useless example (cat * would do the same thing) but you
can see how the "main" program calls the function.
local
Shell functions also support the concept of declaring "local" variables. The
local command is used to do this. For example:
#!/bin/bash
testvars()
{
local localX="testvars localX"
X="testvars X"
local GlobalX="testvars GlobalX"
echo "testvars: localX= $localX X= $X GlobalX= $GlobalX"
}
X="Main X"
GlobalX="Main GLobalX"
echo "Main 1: localX= $localX X= $X GlobalX= $GlobalX"
testvars
echo "Main 2: localX= $localX X= $X GlobalX= $GlobalX"
The output looks like:
Main 1: localX= X= Main X GlobalX= Main GLobalX
testvars: localX= testvars localX X= testvars X GlobalX= testvars GlobalX
Main 2: localX= X= testvars X GlobalX= Main GLobalX
Exercise
What does the wctree program do? Why are certain variables
declared as local? What would happen if they were not? Modify
the program so it will only "recurs" 3 times.
readmsg()
{
read line < $$ # read a line from the file given by the PID
David Jones, Bruce Jamieson (25/02/00) Page 35
85321, Systems Administration Chapter 8: Shell
Programming
echo "$ID − got $line!" # of my *this* process ($$)
if [ $CHILD ]
then
writemsg $line # if I have children, send them message
fi
}
writemsg()
{
echo $* > $CHILD # Write line to the file given by PID
kill −1 $CHILD # of my child. Then signal the child.
}
stop()
{
kill −15 $CHILD # tell my child to stop
if [ $CHILD ]
then
wait $CHILD # wait until they are dead
rm $CHILD # remove the message file
fi
exit 0
}
# Main Program
if [ $# −eq 1 ]
then
NUMCHILD=‘expr $1 − 1‘
saymsg $NUMCHILD $1 & # Launch another child
CHILD=$!
ID=0
touch $CHILD # Create empty message file
echo "I am the parent and have child $CHILD"
else
if [ $1 −ne 0 ] # Must I create children?
then
NUMCHILD=‘expr $1 − 1‘ # Yep, deduct one from the number
saymsg $NUMCHILD $2 & # to be created, then launch them
CHILD=$!
ID=‘expr $2 − $1‘
touch $CHILD # Create empty message file
echo "I am $ID and have child $CHILD"
else
ID=‘expr $2 − $1‘ # I don’t need to create children
echo "I am $ID and am the last child"
Exercise
Method 1 − set
Issuing the truly inspired command of:
set −x
within your program will do wonderful things. As your program executes,
each code line will be printed to the screen − that way you can find your
mistakes, err, well, a little bit quicker. Turning tracing off is a good idea once
your program works − this is done by:
set +x
Method 2 −
Exercise
jamiesob: [Link] 3
tonsloye: [Link] 1
tonsloye: [Link] 3
tonsloye: [Link] 1
Step−by−step
In this section, we will examine a complex shell programming problem and
work our way through the solution.
The problem
This problem is an adaptation of the problem used in the 1997 shell
programming assignment for systems administration:
Problem Definition
Your department’’s FTP server provides anonymous FTP access to the /pub
area of the filesystem − this area contains subdirectories (given by unit code)
which contain resource materials for the various subjects offered. You suspect
that this service isn’t being used any more with the advent of the WWW,
however, before you close this service and use the file space for something
more useful, you need to prove this.
What you require is a program that will parse the FTP logfile and produce
usage statistics on a given subject. This should include:
Background information
A cut down version of the FTP log will be examined by our program − it will
consist of:
Expected interaction
How would you solve this problem? What would you do first?
Break it up
What does the program have to do? What are its major parts? Let’s look at
the functionality again − our program must:
list the number unique machines who have used the area and how many
times
To do this, our program must first:
Read parameters from the command line, picking out the subject we are
interested in
go through the other parameters one by one, acting on each one, calling the
appropriate function
Terminate/clean up
So, this looks like a program containing three functions. Or is it?
We extract the first parameter from the command line. This is our subject.
We might want to check if there is a first parameter − is it blank?
At the end of our program, we should remove any temporary files we use.
Pseudo Code
If we were to pseudo code the above steps, we’d get something like:
# Check to see if the first parameter is blank
if first_parameter = ""
then
echo "No unit specified"
exit
fi
# Find all the entries we’re interested in, place this in a TEMPFILE
# Right − for every other parameter on the command line, we perform
# some
for ACTION in other_parameters
do
# Decide if it is a valid action − act on it or give a error
done
# Remove Temp file
rm TEMPFILE
Notice the use of the variables LOGFILE and TEMPFILE? These would
have to be defined somewhere above the code segment.
We remove the first parameter from the command line and assign it to
another variable. We do this using the shift command.
We use grep to find all the entries in the original log file that refer to the
subject we are interested in. We store these entries in a temporary file.
We use the case command to decide what to do with the action. We could
have just as easily used a series of IF−THEN−ELSE−ELIF−FI
statements − this becomes horrendous to code and read after about three
conditions so case is a better option.
As you will see, we’ve introduced calls to functions for each command −
this again breaks to code up into bite size pieces (excuse the pun ;) to code.
This follows the top−down design style.
Now might be a good time to revise what was required of our program − in
particular, this function.
We need to produce a listing of all the people who have accessed files relating
to the subject of interest and how many times they’ve accessed files.
Because we’ve separated out the entries of interest from the log file, we need
no longer concern ourselves with the actual files and if they relate to the
subject. We now are just interested in the users.
Reviewing the log file format:
[Link] 2345 /pub/85349/[Link]
flipper@[Link]
[Link] 112 /pub/81120/[Link] sloth@[Link]
We see that user information is stored in the fourth field. If we pseudo code
what we want to do, it would look something like:
for every_user_in the file
do
go_through_the_file_and_count_occurences
print this out
done
Expanding this a bit more, we get:
extract_users_from_file
for user in user_list
do
count = 0
while read log_file
do
if user = current_entry
then
count = count + 1
fi
done
echo user count
done
Let’s code this:
getUserList()
{
cut −f4 $TEMPFILE | sort > $[Link]
userList=‘uniq $[Link]‘
for user in $userList
do
{
count=0
while read X
do
if echo $X | grep $user > /dev/null
then
count=‘expr $count + 1‘
fi
done
} < $TEMPFILE
echo $user $count
done
rm $[Link]
}
Some points about this code:
The first cut extracts a user list and places it in a temp file. A unique list
of users is then created and placed into a variable.
For every user in the list, the file is read through and each line searched for
the user string. We pipe the output into /dev/null.
rm $[Link]
}
Much better! We’ve replaced the while loop with a simple grep command −
however, there are still problems:
We don’t need the temporary file
Can we wipe out a few more steps?
Next cut:
getUserList()
{
userList=‘cut −f4 $TEMPFILE | sort | uniq‘
for user in $userList
do
echo $user ‘grep $user $TEMPFILE | wc −l‘
done
}
Beautiful!
Or is it.
What about:
echo ‘cut−f4 $TEMPFILE | sort | uniq −c‘
This does the same thing...or does it? If we didn’t care what our output looked
like, then this’d be ok − find out what’s wrong with this code by trying it and
David Jones, Bruce Jamieson (25/02/00) Page 49
85321, Systems Administration Chapter 8: Shell
Programming
the previous segment − compare the results. Hint: uniq −c produces a
count of every sequential occurrence of an item in a list. What would happen
if we removed the sort? How could we fix our output “problem”?
This function requires a the total number of unique hosts which have accessed
the files. Again, as we’ve already separated out the entries of interest into a
temporary file, we can just concentrate on the hosts field (field number one).
If we were to pseudo code this:
create_unique_host list
count = 0
for host in host_list
do
count = count + 1
done
echo count
From the previous function, we can see that a direct translation from pseudo
code to shell isn’t always efficient. Could we skip a few steps and try the
efficient code first? Remember − we should try to use existing UNIX
commands.
How do we create a unique list? The hint is in the word unique − the uniq
command is useful in extracting unique listings.
What are we going to use as the input to the uniq command? We want a list
of all hosts that accessed the files − the host is stored in the first field of every
line in the file. Next hint − when we see the word “field” we can immediately
assume we’re going to use the cut command. Do we have to give cut any
parameters? In this case, no. cut assumes (by default) that fields are
separated by tabs − in our case, this is true. However, if the delimiter was
anything else, we’d have to use a “−d” switch, followed by the delimiter.
Next step − what about the output from uniq? Where does this go? We said
that we wanted a count of the unique hosts − another hint − counting usually
means using the wc command. The wc command (or word count command)
counts characters, words and lines. If the output from the uniq command
was one host per line, then a count of the lines would reveal the number of
unique hosts.
So what do we have?
cut –f1
uniq
wc −l
Right − how do we get input and save output for each command?
A first cut approach might be:
cat $TEMPFILE | cut −f1 > $[Link]
cat $[Link] | uniq > $[Link]
COUNT=‘cat $[Link] | wc −l‘
echo $COUNT
This is very inefficient; there are several reasons for this:
We cat a file THREE times to get the count. We don’t even have to use
cat if we really try.
We use temp files to store results − we could use a shell variable (as in the
second last line) but is there any need for this? Remember, file IO is much
David Jones, Bruce Jamieson (25/02/00) Page 50
85321, Systems Administration Chapter 8: Shell
Programming
slower than assignments to variables, which, depending on the situation, is
slower again that using pipes.
uniq then removes all duplicate host from the list − this is piped into wc.
The final function we have to write (Yes! We are nearly finished) counts the
total byte count of the files that have been accessed. This is actually a fairly
simple thing to do, but as you’ll see, using shell scripting to do this can be
very inefficient.
First, some pseudo code:
total = 0
while read line from file
do
extract the byte field
add this to the total
done
echo total
In shell, this looks something like:
getBytes()
{
bytes=0
while read X
do
bytefield=‘echo $X | cut −f2‘
bytes=‘expr $bytes + $bytefield‘
done < $TEMPFILE
echo $bytes
}
...which is very inefficient (remember: looping is bad!). In this case, every
iteration of the loop causes three new processes to be created, two for the first
line, one for the second − creating processes takes time!
The following is a bit better:
getBytes()
{
list=‘cut −f2 $TEMPFILE ‘
bytes=0
for number in $list
do
bytes=‘expr $bytes + $number‘
David Jones, Bruce Jamieson (25/02/00) Page 51
85321, Systems Administration Chapter 8: Shell
Programming
done
echo $bytes
}
The above segment of code still has looping, but is more efficient with the use
of a list of values which must be added up. However, we can get smarter:
getBytes()
{
numstr=‘cut −f2 $TEMPFILE | sed "s/$/ + /g"‘
expr $numstr 0
}
Do you see what we’ve done? The cut operation produces a list of numbers,
one per line. When this is piped into sed, the end−of−line is substituted with
“ + “ − note the spaces. This is then combined into a single line string and
stored in the variable numstr. We then get the expr of this string − why do
we put the 0 on the end?
Two reasons:
After the sed operation, there is an extra “+” on the end − for example, if the
input was:
2
3
4
2 +
3 +
4 +
2 + 3 + 4 +
#
# FILE: scanlog
# PURPOSE: Scan FTP log
# AUTHOR: Bruce Jamieson
# HISTORY: DEC 1997 Created
#
# To do : Truly astounding things.
# Apart from that, process a FTP log and produce stats
#−−−−−−−−−−−−−−−−−−−−−−−−−−
# globals
LOGFILE="[Link]"
TEMPFILE="/tmp/scanlog.$$"
# functions
#−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
# getAccessCount
# − display number of unique machines that have accessed the page
getAccessCount()
{
echo ‘cut −f1 $TEMPFILE | uniq | wc −l‘
}
#−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
# getUserList
# − display the list of users who have acessed this page
getUserList()
{
userList=‘cut −f4 $TEMPFILE | sort | uniq‘
for user in $userList
do
echo $user ‘grep $user $TEMPFILE | wc −l‘
done
}
#−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
# getBytes
# − calculate the amount of bytes transferred
getBytes()
{
numstr=‘cut −f2 $TEMPFILE | sed "s/$/ + /g"‘
expr $numstr 0
}
#−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
# process_action
# Based on the passed string, calls one of three functions
#
process_action()
{
# Translate to upper case
theAction=‘echo $1 | tr [a−z] [A−Z]‘
# Now, Check what we have
David Jones, Bruce Jamieson (25/02/00) Page 53
85321, Systems Administration Chapter 8: Shell
Programming
case $theAction in
BYTES) getBytes ;;
USERS) getUserList ;;
HOSTS) getAccessCount ;;
*) echo "Unknown command $theAction" ;;
esac
}
#−−−− Main
#
if [ "$1" = "" ]
then
echo "No unit specified"
exit 1
fi
UNIT=$1
# Remove $1 from the parm line
shift
# Find all the entries we’re interested in
grep "/pub/$UNIT" $LOGFILE > $TEMPFILE
# Right − for every parameter on the command line, we perform some
for ACTION in $@
do
process_action "$ACTION"
done
# Remove Temp file
rm $TEMPFILE
# We’re finished!
Final notes
Throughout this chapter we have examined shell programming concepts
including:
variables
comments
condition statements
functions
recursion
traps
efficiency, and
structure
Be aware that different shells support different syntax − this chapter has dealt
with bourne shell programming only. As a final issue, you should at some
time examine the Perl programming language as it offers the full functionality
of shell programming but with added, compiled−code like features − it is often
useful in some of the more complex system administration tasks.
Review Questions
David Jones, Bruce Jamieson (25/02/00) Page 54
85321, Systems Administration Chapter 8: Shell
Programming
8.1
Write a function that equates the username in the scanit program with the
user’s full name and contact details from the /etc/passwd file. Modify
scanit so its output looks something like:
(Hint: the fifth field of the passwd file usually contains the full name and
phone extension (sometimes))
8.2
8.3
References
Kochan S.G. et al "UNIX Shell Programming" SAMS 1993, USA
Jones, D "Shell Programming" WWW Notes
Newmarch, J "Shell Programming"
[Link]
Source of scanit
#!/bin/bash
#
# AUTHOR: Bruce Jamieson
checkfile()
{
# Goes through the netwatch file and saves user/site
# combinations involving sites that are in the "restricted"
# list
while read buffer
do
username=‘echo $buffer | cut −d" " −f1‘
site=‘echo $buffer | cut −d" " −f2 | sed s/\\\.//g‘
for checksite in $badsites
do
checksite=‘echo $checksite | sed s/\\\.//g‘
# echo $checksite $site
if [ "$site" = "$checksite" ]
then
usersite="$username$checksite"
if eval [ \$$usersite ]
then
eval $usersite=\‘expr \$$usersite + 1\‘
else
eval $usersite=1
fi
fi
done
done < netwatch
}
produce_report()
{
# Goes through all possible combinations of users and
# restricted sites − if a variable exists with the combination,
# it is reported
for user in $*
do
for checksite in $badsites
do
writesite=‘echo $checksite‘
checksite=‘echo $checksite | sed s/\\\.//g‘
usersite="$user$checksite"
if eval [ \$$usersite ]
then
eval echo "$user: $writesite \$$usersite"
usercount=‘expr $usercount + 1‘
fi
done
done
}
get_passwd_users()
{
# Creates a user list based on the /etc/passwd file
while read buffer
do
username=‘echo $buffer | cut −d":" −f1‘
David Jones, Bruce Jamieson (25/02/00) Page 56
85321, Systems Administration Chapter 8: Shell
Programming
the_user_list=‘echo $username $the_user_list‘
done < /etc/passwd
}
check_data_files()
{
if [ −r netwatch −a −r netnasties ]
then
return 0
else
return 1
fi
}
# Main Program
# Uncomment the next line for debug mode
#set −x
if check_data_files
then
echo "Datafiles found"
else
echo "One of the datafiles missing − exiting"
exit 1
fi
usercount=0
badsites=‘cat netnasties‘
if [ $1 ]
then
the_user_list=$*
else
get_passwd_users
fi
echo
echo "*** Restricted Site Report ***"
echo
echo The following is a list of prohibited sites, users who have
echo visited them and on how many occasions
echo
checkfile
produce_report $the_user_list
echo
if [ $usercount −eq 0 ]
then
echo "There were no users found accessing prohibited sites!"
else
echo "$usercount prohibited user/site combinations found."
fi
echo
echo
# END scanit