My Favorite One-Liners: Some Useful Shell Programs
David Fiedler
Everyone thinks they know what KISS stands for, but
in this case,
it's probably more appropriate to say, "Keep It
Simple, SysAdmin,"
or perhaps just "Keep It Shell Script." Too
many system administrators,
especially former or frustrated programmers, get involved
and distracted
creating ad hoc programs that grow until they have a
life and a following
of their own. These programs generally are written in
C, awk, or C
shell, have no documentation, and are incomprehensible,
even to the
author, less than two weeks after being finished.
While it's no cure-all for bad programming practices,
I prefer to
write simple shell scripts for system administration
work. How simple
are they? Simple enough that their inner workings should
be almost
obvious; tricky programs are fine for impressing your
friends but
hard to understand when the system has to be restored
at 3:00 A.M.
And of course, the C shell is so much more elegant,
but not every
system has one, which means that C shell programs aren't
portable.
What price elegance?
My own pride is in writing programs that can be easily
understood,
even if they aren't perfect. In fact, I'm sure I will
get at least
five pieces of email for each of these programs, showing
how they
can be improved. If you like being tricky, find ingenious
things to
do with your programs after you write them. If these
programs
are beyond you, don't despair: but don't fail to read
the manual pages
for the shell and every command you don't understand.
Meanwhile, they
don't get much simpler than Tail:
for i in $*
do
echo "=========" $i "=========="
tail $i
done
Tail emulates the behavior of the head
command when presented with multiple files, instead
of the binary
tail's default behavior of just showing you the tail
of one
file. The simple for-do-done loop executes the echo
and tail commands repeatedly on whatever files you specify
on the command line.
Take Four, They're Small
While we're on incredibly simple programs, how many
times have you
had to double-space a file and forgotten how? It happened
to me every
time, until I wrote double:
pr -dt $*
See? Most of the work is just picking a good name for
the program. In the same vein, here's another self-documenting
program
called crtolf:
tr "\015" "\012"
Here, I had to surround the arguments to the tr
program with double quotes to prevent the shell from
hiccupping on
the backslash, which serves to indicate to tr that the
argument
is an octal constant (in this case, the octal representations
of the
carriage return and linefeed characters).
I'm sure you'd sometimes like to know which files are
taking up the
most space in a particular overstuffed directory, so
here's largest:
ls -l $* | sort -r -n +4 -5
Since the ls -l command shows the file size in
the fifth whitespace-delimited column, the arguments
+4 -5
tell sort that's the column you want to look at. The
-n
flag tells sort to consider the full numerical value
of the
numbers in that column. Without this precaution, the
single digit
9 would be sorted after 1111111, since 1 is lower in
the ASCII
collating sequence. Finally, the -r flag reminds sort
to present the lines in reverse sequence, since it's
the largest numbers
we're most interested in here.
And how about a program to help you create these little
one-line programs?
if test -f $1
then echo $0: cannot overwrite existing file $1
exit 1
fi
cat $1 ; chmod 755 $1
exit 0
To use mkcmd, above, simply type mkcmd filename,
then the shell program you want to enter, followed by
a single Control-D.
The program does the rest.
Testing, Testing
Mkcmd introduces rudimentary error checking to these
short
programs. In cases like these, some error checking is
necessary, but
more might be overkill for what's essentially a one-line
program.
Why didn't I check to make sure that at least the filename
argument is typed on the command line? Leave out the
filename, and
the test program itself will complain:
mkcmd: test: argument expected
Clearly, this is minimalist thinking. But it alerts
you
to the problem, so why bother getting much fancier?
Testing can be done for other reasons than error checking.
Sometimes
you want to perform a function depending on what tty
line a
user is logging in on, or what system they're on. The
program fragment
in Listing 1 from the end of a Korn shell .profile is
a good
illustration. The first section grabs the name of the
system the .profile
is running on (by running the uname command), and stuffs
it
into the SYS variable. That string is then used, together
with
the value of PWD (kept up-to-date by the Korn shell),
to make
the shell prompt reflect the system, current directory,
and command
number that the user is working with. When you set a
shell variable,
you simply name it; when you use its actual value, you
have to prefix
it with a $ character.
But wait, there's more! A similar technique extracts
the actual name
of the tty line, using sed to remove its /dev/ prefix
(note the use of semicolons as delimiters, since the
normal slashes
would prove confusing here). The case statement is then
used
to select a specific series of commands as the final
step in the login
procedure.
Note the special handling of tty1A, to which a modem
is connected
for dialins from a laptop computer. This section cuts
the normal 25-line
display down to 24 (for compatibility with a larger
number of terminal
programs), and starts a program that will automatically
log out the
user (thus regaining the port) after five minutes of
inactivity. A
subsequent section handles a tty name beginning with
the string
ttyp (a pseudo tty which signifies rlogin or
telnet from another machine on the network) by displaying
an appropriate
message as a reminder. This is also an example of recycling
the use
of the SYS variable already created. And not a single
line
of this has to be changed to use it on another computer
(except perhaps
the hardware-specific tty numbers)!
I'm a Back-Quote Man
If you're not conversant with the back quotes (accent
grave) used
in Listing 1, read the manual page for your favorite
shell carefully.
You can usually find the details in a section titled
"Command
Substitution." Surrounding a command by back quotes
effectively
puts the results of running that command on the command
line, as if
you had typed it yourself.
This little one-liner almost breaks all my own rules,
because it's
not immediately apparent what it's good for, but it's
neat and a good
illustration of how useful back quotes can be. It's
called kind:
file * | grep $1 | sed 's/:.*$//'
Kind looks for a specific type of file (that you
supply on the command line) using the file command,
then selects
the type you want with grep and removes file's extraneous
comments with sed. You generally use kind, along with
back quotes, when you want a command to operate only
on, say, text
files:
$ cd /usr/local/bin
$ more `kind text`
These two lines let you look at all your locally produced
shell programs (like this one), while not cluttering
up your screen
with garbage from binary executable files.
While we're on back quotes, let's try another one-liner.
I call this
one lsbin:
/bin/ls -1 `echo $PATH | tr ":" " "`
Lsbin dynamically reads your current PATH environment
variable, removes the colons, then feeds the result
to ls as
simply a list of directories. The final result is a
list of all the
programs in your PATH, on which you can then run sort,
grep,
or whatever you wish.
The More the Merrier
Now let's combine back quotes, test, and if-then-else.
The
program /usr/local/colors, referred to in the .profile
in Listing 1, provides an easy-to-follow example:
SYSTEM=`uname -n`
if [ $SYSTEM = "tegu" ]
then
setcolor yellow brown
setcolor -r yellow red
setcolor -g yellow lt_magenta
else
setcolor yellow blue
setcolor -r red lt_magenta
setcolor -g cyan lt_blue
fi
Here I'm simply picking one color scheme or another,
so I can instantly tell which system I'm working with
on a crowded
X Window display (note: neither the author or publisher
is responsible
for eye or brain damage resulting from your use of these
colors).
I thought the "bracket" version of the test
command
looked better here; it works exactly the same as using
the word test.
The following program, which I call arcmail, is a little
bit
more complicated, but illustrates more of the ways you
can combine
these simple programming techniques, yet still come
up with a readable
program. The program moves old email correspondence
into a "holding
directory," then, if the mail is not examined within
10 days,
puts it into a compressed archive file. Arcmail is run
every
night by cron:
OLDMAIL="/ips/david/Mail/old-mail"
cd $OLDMAIL
FILES=`find . -mtime +190 -print`
if [ "$FILES" ]
then /usr/lbin/arc m Oldmail.arc $FILES
fi
cd ..
FILES=`find . -mtime +180 -print`
if [ "$FILES" ]
then mv $FILES $OLDMAIL
fi
The first thing I do is define the location of the holding
directory. This makes it much easier to edit the program
later (without
causing inadvertent errors) if the particular directory
you're using
happens to change.
Lines 2 and 3 establish the holding directory as a starting
point,
by cding to it, then running a find in back quotes to
determine which, if any, files there meet the age criteria
(6 months
old, plus 10 days). The (bracketed) test on line 4 is
neat:
if no filenames were found, the test fails and we skip
to the next
routine. Otherwise, those same names are used as a command
line parameter
(on line 5) to the arc command, where the files are
added into
the archive.
In either case, line 7 brings us up one directory, to
the place where
current mail is stored. Using back quotes, we again
store filenames
matching our criteria into a variable called FILES.
Notice,
though, that the number of days is now exactly 180.
If any files 180
days old (or more) are found, they're moved to the holding
directory
OLDMAIL.
The program had to be written "backwards"
this way because
the find command checks the files in the current directory
-- and also in all subdirectories. Since the holding
directory
was a subdirectory of the main mail directory, those
older files had
to be removed first (which was effectively accomplished
by archiving
them). Finishing the program by moving the 180-day files
to the holding
directory ensures that that's where they'll be in 10
more days, when
it will be their turn to be archived.
Don't Stay Lost
Every system administrator runs fsck, and every once
in awhile,
you discover, to your horror, that you are wasting a
couple of precious
megabytes of disk space on files that you didn't even
know you had.
To clean out your lost+found directories on a regular
basis,
simply run checklost as often as you deem advisable,
via cron,
and mail the results to yourself:
for i in / /usr/spool /ips
do
ls -lta $i/lost+found
echo That was the $i file system
echo "---------------------------"
done
Naturally, replace my file system names with yours in
the first line. Don't wipe all the files out by reflex,
either; take
a look at them and see if they look useful. I once found
a header
file -- needed to rebuild the kernel -- in a lost+found
directory after a crash!
About the Author
David Fiedler is a writer and consultant specializing
in UNIX
and networking topics. He was the co-author of the first
book on UNIX
system administration, and can be reached at david@infopro.com.
|