Cover V01, I01
Article
Listing 1

may92.tar


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.