178 Programming we wrote a recursive-descent parser for Lisp. It took about 250 lines of C. If the parser had been written in Lisp, it would not have even filled a page. The olden days mentioned above were just around the time that the editors of this book were born. Dinosaurs ruled the machine room and Real Men programmed with switches on the front panel. Today, sociologists and his- torians are unable to determine why the seemingly rational programmers of the time designed, implemented, and disseminated languages that were so hard to parse. Perhaps they needed open research problems and writing parsers for these hard-to-parse languages seemed like a good one. It kind of makes you wonder what kinds of drugs they were doing back in the olden days. A program to parse C programs and figure out which functions call which functions and where global variables are read and modified is the equiva- lent of a C compiler front end. C compiler front ends are complex artifacts the complexity of the C language and the difficulty of using tools like yacc make them that way. No wonder nobody is rushing to write this program. Die-hard Unix aficionados would say that you don’t need this program since grep is a perfectly good solution. Plus, you can use grep in shell pipelines. Well, the other day we were looking for all uses of the min func- tion in some BSD kernel code. Here’s an example of what we got: % grep min netinet/ip_icmp.c icmplen = oiplen + min(8, oip-ip_len) * that not corrupted and of at least minimum length. * If the incoming packet was addressed directly to us, * to the incoming interface. * Retrieve any source routing from the incoming packet % Yep, grep finds all of the occurrences of min, and then some. “Don’t know how to make love. Stop.” The ideal programming tool should be quick and easy to use for common tasks and, at the same time, powerful enough to handle tasks beyond that for which it was intended. Unfortunately, in their zeal to be general, many Unix tools forget about the quick and easy part. Make is one such tool. In abstract terms, make’s input is a description of a dependency graph. Each node of the dependency graph contains a set of commands to be run when that node is out of date with respect to the nodes that it depends on. Nodes corresponds to files, and the file dates determine
Programming in Plato’s Cave 179 whether the files are out of date with respect to each other. A small depen- dency graph, or Makefile, is shown below: program: source1.o source2.o cc -o program source1.o source2.o source1.o: source1.c cc -c source1.c source2.o: source2.c cc -c source2.c In this graph, the nodes are program, source1.o, source2.o, source1.c, and source2.c. The node program depends on the source1.o and source2.o nodes. Here is a graphical representation of the same makefile: When either source1.o or source2.o is newer than program, make will regenerate program by executing the command cc -o program source1.o source2.o. And, of course, if source1.c has been modified, then both source1.o and program will be out of date, necessitating a recompile and a relink. While make’s model is quite general, the designers forgot to make it easy to use for common cases. In fact, very few novice Unix programmers know exactly how utterly easy it is to screw yourself to a wall with make, until they do it. To continue with our example, let’s say that our programmer, call him Dennis, is trying to find a bug in source1.c and therefore wants to compile this file with debugging information included. He modifies the Makefile to look like this: program source1.o source2.o source1.c source1.c
Previous Page Next Page