Shell Programming Shell programmers and the dinosaur cloners of Jurassic Park have much in common. They don’t have all the pieces they need, so they fill in the missing pieces with random genomic material. Despite tremendous self-confidence and ability, they can’t always control their creations. Shell programs, goes the theory, have a big advantage over programs written in languages like C: shell programs are portable. That is, a program written in the shell “programming language” can run on many different flavors of Unix running on top of many different computer architectures, because the shell interprets its programs, rather than compiling them into machine code. What’s more, sh, the standard Unix shell, has been a central part of Unix since 1977 and, thus, we are likely to find it on any machine. Let’s put the theory to the test by writing a shell script to print the name and type of every file in the current directory using the file program: Date: Fri, 24 Apr 92 14:45:48 EDT From: Stephen Gildea Subject: Simple Shell Programming To: UNIX-HATERS Shell Programming 155 Hello, class. Today we are going to learn to program in “sh.” The “sh” shell is a simple, versatile program, but we'll start with a basic example: Print the types of all the files in a directory. (I heard that remark in the back! Those of you who are a little familiar with the shell and bored with this can write “start an X11 client on a remote machine” for extra credit. In the mean time, shh!) While we're learning to sh, of course we also want the program we are writing to be robust, portable, and elegant. I assume you've all read the appropriate manual pages, so the following should be trivially obvious: file * Very nice, isn’t it? A simple solution for a simple problem; the * matches all the files in the directory. Well, not quite. Files beginning with a dot are assumed to be uninteresting, and * won’t match them. There probably aren’t any, but since we do want to be robust, we’ll use “ls” and pass a special flag: for file in `ls -A` do file $file done There: elegant, robust... Oh dear, the “ls” on some systems doesn’t take a “-A” flag. No problem, we'll pass -a instead and then weed out the . and .. files: for file in `ls -a` do if [ $file != . -a $file != .. ] then file $file fi done Not quite as elegant, but at least it’s robust and portable. What’s that? “ls -a” doesn’t work everywhere either? No problem, we'll use “ls -f” instead. It’s faster, anyway. I hope all this is obvious from reading the manual pages. 156 csh, pipes, and find Hmm, perhaps not so robust after all. Unix file names can have any character in them (except slash). A space in a filename will break this script, since the shell will parse it as two file names. Well, that’s not too hard to deal with. We'll just change the IFS to not include Space (or Tab while we're at it), and carefully quote (not too little, not too much!) our variables, like this: IFS=' ' for file in `ls -f` do if [ "$file" != . -a "$file" != .. ] then file "$file" fi done Some of you alert people will have already noticed that we have made the problem smaller, but we haven't eliminated it, because Linefeed is also a legal character in a filename, and it is still in IFS. Our script has lost some of its simplicity, so it is time to reevaluate our approach. If we removed the “ls” then we wouldn’t have to worry about parsing its output. What about for file in .* * do if [ "$file" != . -a "$file" != .. ] then file "$file" fi done Looks good. Handles dot files and files with nonprinting characters. We keep adding more strangely named files to our test directory, and this script continues to work. But then someone tries it on an empty directory, and the * pattern produces “No such file.” But we can add a check for that… …at this point my message is probably getting too long for some of your uucp mailers, so I'm afraid I'll have to close here and leave fixing the remaining bugs as an exercise for the reader. Stephen Shell Programming 157 There is another big problem as well, one that we’ve been glossing over from the beginning. The Unix file program doesn’t work. Date: Sat, 25 Apr 92 17:33:12 EDT From: Alan Bawden Subject: Simple Shell Programming To: UNIX-HATERS WHOA! Hold on a second. Back up. You're actually proposing to use the ‘file’ program? Everybody who wants a good laugh should pause right now, find a Unix machine, and try typing “file *” in a directory full of miscellaneous files. For example, I just ran ‘file’ over a directory full of C source code— here is a selection of the results: arith.c: c program text binshow.c: c program text bintxt.c: c program text So far, so good. But then: crc.c: ascii text See, ‘file’ isn’t looking at the “.c” in the filename, it’s applying some heuristics based on an examination of the contents of the file. Apparently crc.c didn’t look enough like C code—although to me it couldn’t possibly be anything else. gencrc.c.~4~: ascii text gencrc.c: c program text I guess I changed something after version 4 that made gencrc.c look more like C… tcfs.h.~1~: c program text tcfs.h: ascii text while tcfs.h looked less like C after version 1. time.h: English text That’s right, time.h apparently looks like English, rather than just ascii. I wonder if ‘file’ has recognition rules for Spanish or French? 158 csh, pipes, and find (BTW, your typical TeX source file gets classified as “ascii text” rather than “English text,” but I digress…) words.h.~1~: ascii text words.h: English text Perhaps I added some comments to words.h after version 1? But I saved the best for last: arc.h: shell commands Makefile: [nt]roff, tbl, or eqn input text Both wildly wrong. I wonder what would happen if I tried to use them as if they were the kinds of program that the ‘file’ program assigns them? —Alan