Classic Shell Scripting - Arnold Robbins [190]
IFS='
'
# Customize PATH to get BSD-style ps first
PATH=/usr/ucb:/usr/bin:/bin
export PATH
HEADFLAGS="-n 20"
PSFLAGS=aux
SLEEPFLAGS=5
SORTFLAGS='-k3nr -k1,1 -k2n'
HEADER="`ps $PSFLAGS | head -n 1`"
while true
do
clear
uptime
echo "$HEADER"
ps $PSFLAGS |
sed -e 1d |
sort $SORTFLAGS |
head $HEADFLAGS
sleep $SLEEPFLAGS
done
We save command options in HEADFLAGS, PSFLAGS, SLEEPFLAGS, and SORTFLAGS to facilitate site-specific customization.
An explanatory header for the simple-top output is helpful, but since it varies somewhat between ps implementations, we do not hardcode it in the script; but instead, we just call ps once, saving it in the variable HEADER.
The remainder of the program is an infinite loop that is terminated by one of the keyboard interrupt characters mentioned earlier. The clear command at the start of each loop iteration uses the setting of the TERM environment variable to determine the escape sequences that it then sends to standard output to clear the screen, leaving the cursor in the upper-left corner. uptime reports the load average, and echo supplies the column headers. The pipeline filters ps output, using sed to remove the header line, then sorts the output by CPU usage, username, and process ID, and shows only the first 20 lines. The final sleep command in the loop body produces a short delay that is still relatively long compared to the time required for one loop iteration so that the system load imposed by the script is minor.
Sometimes, you would like to know who is using the system, and how many and what processes they are running, without all of the extra details supplied by the verbose form of ps output. The puser script in Example 13-2 produces a report that looks like this:
$ puser
Show users and their processes
albert 3 -tcsh
3 /etc/sshd
2 /bin/sh
1 /bin/ps
1 /usr/bin/ssh
1 xload
daemon 1 /usr/lib/nfs/statd
root 4 /etc/sshd
3 /usr/lib/ssh/sshd
3 /usr/sadm/lib/smc/bin/smcboot
2 /usr/lib/saf/ttymon
1 /etc/init
1 /usr/lib/autofs/automountd
1 /usr/lib/dmi/dmispd
...
victoria 4 bash
2 /usr/bin/ssh
2 xterm
The report is sorted by username, and to reduce clutter and enhance visibility, usernames are shown only when they change.
Example 13-2. The puser script
#! /bin/sh -
# Show a sorted list of users with their counts of active
# processes and process names, optionally limiting the
# display to a specified set of users (actually, egrep(1)
# username patterns).
#
# Usage:
# puser [ user1 user2 ... ]
IFS='
'
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
EGREPFLAGS=
while test $# -gt 0
do
if test -z "$EGREPFLAGS"
then
EGREPFLAGS="$1"
else
EGREPFLAGS="$EGREPFLAGS|$1"
fi
shift
done
if test -z "$EGREPFLAGS"
then
EGREPFLAGS="."
else
EGREPFLAGS="^ *($EGREPFLAGS) "
fi
case "`uname -s`" in
*BSD | Darwin) PSFLAGS="-a -e -o user,ucomm -x" ;;
*) PSFLAGS="-e -o user,comm" ;;
esac
ps $PSFLAGS |
sed -e 1d |
EGREP_OPTIONS= egrep "$EGREPFLAGS" |
sort -b -k1,1 -k2,2 |
uniq -c |
sort -b -k2,2 -k1nr,1 -k3,3 |
awk '{
user = (LAST = = $2) ? " " : $2
LAST = $2
printf("%-15s\t%2d\t%s\n", user, $1, $3)
}'
After the familiar preamble, the puser script uses a loop to collect the optional command-line arguments into the EGREPFLAGS variable, with the vertical-bar separators that indicate alternation to egrep. The if statement in the loop body handles the initial case of an empty string, to avoid producing an egrep pattern with an empty alternative.
When the argument-collection loop completes, we check EGREPFLAGS: if it is empty, we reassign it a match-anything pattern. Otherwise, we augment the pattern to match only at the beginning of a line, and to require a trailing space, to prevent false matches of usernames with common prefixes, such as jon and jones.
The case statement handles implementation differences in the ps options. We want an output form that displays just two values: a username and a command name. The BSD systems and BSD-derived Mac OS X (Darwin) systems require slightly different options from