3.  Short-cuts and Other Nice Things  

Based on what I've told you so far, you may have gotten the impression that PMake is just a way of storing away commands and making sure you don't forget to compile something. Good. That's just what it is. However, the ways I've described have been inelegant, at best, and painful, at worst. This chapter contains things that make the writing of makefiles easier and the makefiles themselves shorter and easier to modify (and, occasionally, simpler). In this chapter, I assume you are somewhat more familiar with Sprite (or UNIX, if that's what you're using) than I did in chapter 2, just so you're on your toes. So without further ado...

3.1.  Transformation Rules  

As you know, a file's name consists of two parts: a base name, which gives some hint as to the contents of the file, and a suffix, which usually indicates the format of the file. Over the years, as UNIXhas developed, naming conventions, with regard to suffixes, have also developed that have become almost as incontrovertible as Law. E.g. a file ending in .c is assumed to contain C source code; one with a .o suffix is assumed to be a compiled, relocatable object file that may be linked into any program; a file with a .ms suffix is usually a text file to be processed by Troff with the -ms macro package, and so on. One of the best aspects of both Make and PMake comes from their understanding of how the suffix of a file pertains to its contents and their ability to do things with a file based soley on its suffix. This ability comes from something known as a transformation rule. A transformation rule specifies how to change a file with one suffix into a file with another suffix.

A transformation rule looks much like a dependency line, except the target is made of two known suffixes stuck together. Suffixes are made known to PMake by placing them as sources on a dependency line whose target is the special target .SUFFIXES. E.g. .SUFFIXES : .o .c .c.o : $(CC) $(CFLAGS) -c $(.IMPSRC) The creation script attached to the target is used to transform a file with the first suffix (in this case, .c) into a file with the second suffix (here, .o). In addition, the target inherits whatever attributes have been applied to the transformation rule. The simple rule given above says that to transform a C source file into an object file, you compile it using cc with the -c flag. This rule is taken straight from the system makefile. Many transformation rules (and suffixes) are defined there, and I refer you to it for more examples (type ``pmake -h'' to find out where it is).

There are several things to note about the transformation rule given above:

1)
The .IMPSRC variable. This variable is set to the ``implied source'' (the file from which the target is being created; the one with the first suffix), which, in this case, is the .c file.
2)
The CFLAGS variable. Almost all of the transformation rules in the system makefile are set up using variables that you can alter in your makefile to tailor the rule to your needs. In this case, if you want all your C files to be compiled with the -g flag, to provide information for dbx, you would set the CFLAGS variable to contain -g (``CFLAGS = -g'') and PMake would take care of the rest.

To give you a quick example, the makefile in 2.3.4 could be changed to this: OBJS = a.o b.o c.o program : $(OBJS) $(CC) -o $(.TARGET) $(.ALLSRC) $(OBJS) : defs.h The transformation rule I gave above takes the place of the 6 lines[note 1] a.o : a.c cc -c a.c b.o : b.c cc -c b.c c.o : c.c cc -c c.c

Now you may be wondering about the dependency between the .o and .c files -- it's not mentioned anywhere in the new makefile. This is because it isn't needed: one of the effects of applying a transformation rule is the target comes to depend on the implied source. That's why it's called the implied source.

For a more detailed example. Say you have a makefile like this: a.out : a.o b.o $(CC) $(.ALLSRC) and a directory set up like this: total 4 -rw-rw-r-- 1 deboor 34 Sep 7 00:43 Makefile -rw-rw-r-- 1 deboor 119 Oct 3 19:39 a.c -rw-rw-r-- 1 deboor 201 Sep 7 00:43 a.o -rw-rw-r-- 1 deboor 69 Sep 7 00:43 b.c While just typing ``pmake'' will do the right thing, it's much more informative to type ``pmake -d s''. This will show you what PMake is up to as it processes the files. In this case, PMake prints the following: Suff_FindDeps (a.out) using existing source a.o applying .o -> .out to "a.o" Suff_FindDeps (a.o) trying a.c...got it applying .c -> .o to "a.c" Suff_FindDeps (b.o) trying b.c...got it applying .c -> .o to "b.c" Suff_FindDeps (a.c) trying a.y...not there trying a.l...not there trying a.c,v...not there trying a.y,v...not there trying a.l,v...not there Suff_FindDeps (b.c) trying b.y...not there trying b.l...not there trying b.c,v...not there trying b.y,v...not there trying b.l,v...not there --- a.o --- cc -c a.c --- b.o --- cc -c b.c --- a.out --- cc a.o b.o

Suff_FindDeps is the name of a function in PMake that is called to check for implied sources for a target using transformation rules. The transformations it tries are, naturally enough, limited to the ones that have been defined (a transformation may be defined multiple times, by the way, but only the most recent one will be used). You will notice, however, that there is a definite order to the suffixes that are tried. This order is set by the relative positions of the suffixes on the .SUFFIXES line -- the earlier a suffix appears, the earlier it is checked as the source of a transformation. Once a suffix has been defined, the only way to change its position in the pecking order is to remove all the suffixes (by having a .SUFFIXES dependency line with no sources) and redefine them in the order you want. (Previously-defined transformation rules will be automatically redefined as the suffixes they involve are re-entered.)

Another way to affect the search order is to make the dependency explicit. In the above example, a.out depends on a.o and b.o. Since a transformation exists from .o to .out, PMake uses that, as indicated by the ``using existing source a.o'' message.

The search for a transformation starts from the suffix of the target and continues through all the defined transformations, in the order dictated by the suffix ranking, until an existing file with the same base (the target name minus the suffix and any leading directories) is found. At that point, one or more transformation rules will have been found to change the one existing file into the target.

For example, ignoring what's in the system makefile for now, say you have a makefile like this: .SUFFIXES : .out .o .c .y .l .l.c : lex $(.IMPSRC) mv lex.yy.c $(.TARGET) .y.c : yacc $(.IMPSRC) mv y.tab.c $(.TARGET) .c.o : cc -c $(.IMPSRC) .o.out : cc -o $(.TARGET) $(.IMPSRC) and the single file jive.l. If you were to type ``pmake -rd ms jive.out,'' you would get the following output for jive.out: Suff_FindDeps (jive.out) trying jive.o...not there trying jive.c...not there trying jive.y...not there trying jive.l...got it applying .l -> .c to "jive.l" applying .c -> .o to "jive.c" applying .o -> .out to "jive.o" and this is why: PMake starts with the target jive.out, figures out its suffix (.out) and looks for things it can transform to a .out file. In this case, it only finds .o, so it looks for the file jive.o. It fails to find it, so it looks for transformations into a .o file. Again it has only one choice: .c. So it looks for jive.c and, as you know, fails to find it. At this point it has two choices: it can create the .c file from either a .y file or a .l file. Since .y came first on the .SUFFIXES line, it checks for jive.y first, but can't find it, so it looks for jive.l and, lo and behold, there it is. At this point, it has defined a transformation path as follows: .l -> .c -> .o -> .out and applies the transformation rules accordingly. For completeness, and to give you a better idea of what PMake actually did with this three-step transformation, this is what PMake printed for the rest of the process: Suff_FindDeps (jive.o) using existing source jive.c applying .c -> .o to "jive.c" Suff_FindDeps (jive.c) using existing source jive.l applying .l -> .c to "jive.l" Suff_FindDeps (jive.l) Examining jive.l...modified 17:16:01 Oct 4, 1987...up-to-date Examining jive.c...non-existent...out-of-date --- jive.c --- lex jive.l ... meaningless lex output deleted ... mv lex.yy.c jive.c Examining jive.o...non-existent...out-of-date --- jive.o --- cc -c jive.c Examining jive.out...non-existent...out-of-date --- jive.out --- cc -o jive.out jive.o

One final question remains: what does PMake do with targets that have no known suffix? PMake simply pretends it actually has a known suffix and searches for transformations accordingly. The suffix it chooses is the source for the .NULL target mentioned later. In the system makefile, .out is chosen as the ``null suffix'' because most people use PMake to create programs. You are, however, free and welcome to change it to a suffix of your own choosing. The null suffix is ignored, however, when PMake is in compatibility mode (see chapter 4).

3.2.  Including Other Makefiles  

Just as for programs, it is often useful to extract certain parts of a makefile into another file and just include it in other makefiles somehow. Many compilers allow you say something like #include "defs.h" to include the contents of defs.h in the source file. PMake allows you to do the same thing for makefiles, with the added ability to use variables in the filenames. An include directive in a makefile looks either like this: #include <file> or this #include "file" The difference between the two is where PMake searches for the file: the first way, PMake will look for the file only in the system makefile directory (or directories) (to find out what that directory is, give PMake the -h flag). The system makefile directory search path can be overridden via the -m option. For files in double-quotes, the search is more complex:

1)
The directory of the makefile that's including the file.
2)
The current directory (the one in which you invoked PMake).
3)
The directories given by you using -I flags, in the order in which you gave them.
4)
Directories given by .PATH dependency lines (see chapter 4).
5)
The system makefile directory.

in that order.

You are free to use PMake variables in the filename--PMake will expand them before searching for the file. You must specify the searching method with either angle brackets or double-quotes outside of a variable expansion. I.e. the following SYSTEM = <command.mk> #include $(SYSTEM) won't work.

3.3.  Saving Commands  

There may come a time when you will want to save certain commands to be executed when everything else is done. For instance: you're making several different libraries at one time and you want to create the members in parallel. Problem is, ranlib is another one of those programs that can't be run more than once in the same directory at the same time (each one creates a file called __.SYMDEF into which it stuffs information for the linker to use. Two of them running at once will overwrite each other's file and the result will be garbage for both parties). You might want a way to save the ranlib commands til the end so they can be run one after the other, thus keeping them from trashing each other's file. PMake allows you to do this by inserting an ellipsis (``...'') as a command between commands to be run at once and those to be run later.

So for the ranlib case above, you might do this: lib1.a : $(LIB1OBJS) rm -f $(.TARGET) ar cr $(.TARGET) $(.ALLSRC) ... ranlib $(.TARGET) lib2.a : $(LIB2OBJS) rm -f $(.TARGET) ar cr $(.TARGET) $(.ALLSRC) ... ranlib $(.TARGET) This would save both ranlib $(.TARGET) commands until the end, when they would run one after the other (using the correct value for the .TARGET variable, of course).

Commands saved in this manner are only executed if PMake manages to re-create everything without an error.

3.4.  Target Attributes  

PMake allows you to give attributes to targets by means of special sources. Like everything else PMake uses, these sources begin with a period and are made up of all upper-case letters. There are various reasons for using them, and I will try to give examples for most of them. Others you'll have to find uses for yourself. Think of it as ``an exercise for the reader.'' By placing one (or more) of these as a source on a dependency line, you are ``marking the target(s) with that attribute.'' That's just the way I phrase it, so you know.

Any attributes given as sources for a transformation rule are applied to the target of the transformation rule when the rule is applied.

.DONTCARE
If a target is marked with this attribute and PMake can't figure out how to create it, it will ignore this fact and assume the file isn't really needed or actually exists and PMake just can't find it. This may prove wrong, but the error will be noted later on, not when PMake tries to create the target so marked. This attribute also prevents PMake from attempting to touch the target if it is given the -t flag.
.EXEC
This attribute causes its shell script to be executed while having no effect on targets that depend on it. This makes the target into a sort of subroutine. An example. Say you have some LISP files that need to be compiled and loaded into a LISP process. To do this, you echo LISP commands into a file and execute a LISP with this file as its input when everything's done. Say also that you have to load other files from another system before you can compile your files and further, that you don't want to go through the loading and dumping unless one of your files has changed. Your makefile might look a little bit like this (remember, this is an educational example, and don't worry about the COMPILE rule, all will soon become clear, grasshopper): system : init a.fasl b.fasl c.fasl for i in $(.ALLSRC); do echo -n '(load "' >> input echo -n ${i} >> input echo '")' >> input done echo '(dump "$(.TARGET)")' >> input lisp < input a.fasl : a.l init COMPILE b.fasl : b.l init COMPILE c.fasl : c.l init COMPILE COMPILE : .USE echo '(compile "$(.ALLSRC)")' >> input init : .EXEC echo '(load-system)' > input
.EXEC sources, don't appear in the local variables of targets that depend on them (nor are they touched if PMake is given the -t flag). Note that all the rules, not just that for system, include init as a source. This is because none of the other targets can be made until init has been made, thus they depend on it.
.EXPORT
This is used to mark those targets whose creation should be sent to another machine if at all possible. This may be used by some exportation schemes if the exportation is expensive. You should ask your system administrator if it is necessary.
.EXPORTSAME
Tells the export system that the job should be exported to a machine of the same architecture as the current one. Certain operations (e.g. running text through nroff) can be performed the same on any architecture (CPU and operating system type), while others (e.g. compiling a program with cc) must be performed on a machine with the same architecture. Not all export systems will support this attribute.
.IGNORE
Giving a target the .IGNORE attribute causes PMake to ignore errors from any of the target's commands, as if they all had `-' before them.
.INVISIBLE
This allows you to specify one target as a source for another without the one affecting the other's local variables. Useful if, say, you have a makefile that creates two programs, one of which is used to create the other, so it must exist before the other is created. You could say prog1 : $(PROG1OBJS) prog2 MAKEINSTALL prog2 : $(PROG2OBJS) .INVISIBLE MAKEINSTALL where MAKEINSTALL is some complex .USE rule (see below) that depends on the .ALLSRC variable containing the right things. Without the .INVISIBLE attribute for prog2, the MAKEINSTALL rule couldn't be applied. This is not as useful as it should be, and the semantics may change (or the whole thing go away) in the not-too-distant future.
.JOIN
This is another way to avoid performing some operations in parallel while permitting everything else to be done so. Specifically it forces the target's shell script to be executed only if one or more of the sources was out-of-date. In addition, the target's name, in both its .TARGET variable and all the local variables of any target that depends on it, is replaced by the value of its .ALLSRC variable. As an example, suppose you have a program that has four libraries that compile in the same directory along with, and at the same time as, the program. You again have the problem with ranlib that I mentioned earlier, only this time it's more severe: you can't just put the ranlib off to the end since the program will need those libraries before it can be re-created. You can do something like this: program : $(OBJS) libraries cc -o $(.TARGET) $(.ALLSRC) libraries : lib1.a lib2.a lib3.a lib4.a .JOIN ranlib $(.OODATE) In this case, PMake will re-create the $(OBJS) as necessary, along with lib1.a, lib2.a, lib3.a and lib4.a. It will then execute ranlib on any library that was changed and set program's .ALLSRC variable to contain what's in $(OBJS) followed by ``lib1.a lib2.a lib3.a lib4.a.'' In case you're wondering, it's called .JOIN because it joins together different threads of the ``input graph'' at the target marked with the attribute. Another aspect of the .JOIN attribute is it keeps the target from being created if the -t flag was given.
.MAKE
The .MAKE attribute marks its target as being a recursive invocation of PMake. This forces PMake to execute the script associated with the target (if it's out-of-date) even if you gave the -n or -t flag. By doing this, you can start at the top of a system and type pmake -n and have it descend the directory tree (if your makefiles are set up correctly), printing what it would have executed if you hadn't included the -n flag.
.NOEXPORT
If possible, PMake will attempt to export the creation of all targets to another machine (this depends on how PMake was configured). Sometimes, the creation is so simple, it is pointless to send it to another machine. If you give the target the .NOEXPORT attribute, it will be run locally, even if you've given PMake the -L 0 flag.
.NOTMAIN
Normally, if you do not specify a target to make in any other way, PMake will take the first target on the first dependency line of a makefile as the target to create. That target is known as the ``Main Target'' and is labeled as such if you print the dependencies out using the -p flag. Giving a target this attribute tells PMake that the target is definitely not the Main Target. This allows you to place targets in an included makefile and have PMake create something else by default.
.PRECIOUS
When PMake is interrupted (you type control-C at the keyboard), it will attempt to clean up after itself by removing any half-made targets. If a target has the .PRECIOUS attribute, however, PMake will leave it alone. An additional side effect of the `::' operator is to mark the targets as .PRECIOUS.
.SILENT
Marking a target with this attribute keeps its commands from being printed when they're executed, just as if they had an `@' in front of them.
.USE
By giving a target this attribute, you turn it into PMake's equivalent of a macro. When the target is used as a source for another target, the other target acquires the commands, sources and attributes (except .USE) of the source. If the target already has commands, the .USE target's commands are added to the end. If more than one .USE-marked source is given to a target, the rules are applied sequentially.
The typical .USE rule (as I call them) will use the sources of the target to which it is applied (as stored in the .ALLSRC variable for the target) as its ``arguments,'' if you will. For example, you probably noticed that the commands for creating lib1.a and lib2.a in the example in section 3.3 were exactly the same. You can use the .USE attribute to eliminate the repetition, like so: lib1.a : $(LIB1OBJS) MAKELIB lib2.a : $(LIB2OBJS) MAKELIB MAKELIB : .USE rm -f $(.TARGET) ar cr $(.TARGET) $(.ALLSRC) ... ranlib $(.TARGET)
Several system makefiles (not to be confused with The System Makefile) make use of these .USE rules to make your life easier (they're in the default, system makefile directory...take a look). Note that the .USE rule source itself (MAKELIB) does not appear in any of the targets's local variables. There is no limit to the number of times I could use the MAKELIB rule. If there were more libraries, I could continue with ``lib3.a : $(LIB3OBJS) MAKELIB'' and so on and so forth.

3.5.  Special Targets  

As there were in Make, so there are certain targets that have special meaning to PMake. When you use one on a dependency line, it is the only target that may appear on the left-hand-side of the operator. As for the attributes and variables, all the special targets begin with a period and consist of upper-case letters only. I won't describe them all in detail because some of them are rather complex and I'll describe them in more detail than you'll want in chapter 4. The targets are as follows:

.BEGIN
Any commands attached to this target are executed before anything else is done. You can use it for any initialization that needs doing.
.DEFAULT
This is sort of a .USE rule for any target (that was used only as a source) that PMake can't figure out any other way to create. It's only ``sort of'' a .USE rule because only the shell script attached to the .DEFAULT target is used. The .IMPSRC variable of a target that inherits .DEFAULT's commands is set to the target's own name.
.END
This serves a function similar to .BEGIN, in that commands attached to it are executed once everything has been re-created (so long as no errors occurred). It also serves the extra function of being a place on which PMake can hang commands you put off to the end. Thus the script for this target will be executed before any of the commands you save with the ``...''.
.EXPORT
The sources for this target are passed to the exportation system compiled into PMake. Some systems will use these sources to configure themselves. You should ask your system administrator about this.
.IGNORE
This target marks each of its sources with the .IGNORE attribute. If you don't give it any sources, then it is like giving the -i flag when you invoke PMake -- errors are ignored for all commands.
.INCLUDES
The sources for this target are taken to be suffixes that indicate a file that can be included in a program source file. The suffix must have already been declared with .SUFFIXES (see below). Any suffix so marked will have the directories on its search path (see .PATH, below) placed in the .INCLUDES variable, each preceded by a -I flag. This variable can then be used as an argument for the compiler in the normal fashion. The .h suffix is already marked in this way in the system makefile. E.g. if you have .SUFFIXES : .bitmap .PATH.bitmap : /usr/local/X/lib/bitmaps .INCLUDES : .bitmap PMake will place ``-I/usr/local/X/lib/bitmaps'' in the .INCLUDES variable and you can then say cc $(.INCLUDES) -c xprogram.c (Note: the .INCLUDES variable is not actually filled in until the entire makefile has been read.)
.INTERRUPT
When PMake is interrupted, it will execute the commands in the script for this target, if it exists.
.LIBS
This does for libraries what .INCLUDES does for include files, except the flag used is -L, as required by those linkers that allow you to tell them where to find libraries. The variable used is .LIBS. Be forewarned that PMake may not have been compiled to do this if the linker on your system doesn't accept the -L flag, though the .LIBS variable will always be defined once the makefile has been read.
.MAIN
If you didn't give a target (or targets) to create when you invoked PMake, it will take the sources of this target as the targets to create.
.MAKEFLAGS
This target provides a way for you to always specify flags for PMake when the makefile is used. The flags are just as they would be typed to the shell (except you can't use shell variables unless they're in the environment), though the -f and -r flags have no effect.
.NULL
This allows you to specify what suffix PMake should pretend a file has if, in fact, it has no known suffix. Only one suffix may be so designated. The last source on the dependency line is the suffix that is used (you should, however, only give one suffix...).
.PATH
If you give sources for this target, PMake will take them as directories in which to search for files it cannot find in the current directory. If you give no sources, it will clear out any directories added to the search path before. Since the effects of this all get very complex, I'll leave it til chapter four to give you a complete explanation.
.PATHsuffix
This does a similar thing to .PATH, but it does it only for files with the given suffix. The suffix must have been defined already. Look at Search Paths (section 4.1) for more information.
.PRECIOUS
Similar to .IGNORE, this gives the .PRECIOUS attribute to each source on the dependency line, unless there are no sources, in which case the .PRECIOUS attribute is given to every target in the file.
.RECURSIVE
This target applies the .MAKE attribute to all its sources. It does nothing if you don't give it any sources.
.SHELL
PMake is not constrained to only using the Bourne shell to execute the commands you put in the makefile. You can tell it some other shell to use with this target. Check out A Shell is a Shell is a Shell (section 4.4) for more information.
.SILENT
When you use .SILENT as a target, it applies the .SILENT attribute to each of its sources. If there are no sources on the dependency line, then it is as if you gave PMake the -s flag and no commands will be echoed.
.SUFFIXES
This is used to give new file suffixes for PMake to handle. Each source is a suffix PMake should recognize. If you give a .SUFFIXES dependency line with no sources, PMake will forget about all the suffixes it knew (this also nukes the null suffix). For those targets that need to have suffixes defined, this is how you do it.

In addition to these targets, a line of the form attribute : sources applies the attribute to all the targets listed as sources.

3.6.  Modifying Variable Expansion  

Variables need not always be expanded verbatim. PMake defines several modifiers that may be applied to a variable's value before it is expanded. You apply a modifier by placing it after the variable name with a colon between the two, like so: ${VARIABLE:modifier} Each modifier is a single character followed by something specific to the modifier itself. You may apply as many modifiers as you want -- each one is applied to the result of the previous and is separated from the previous by another colon.

There are seven ways to modify a variable's expansion, most of which come from the C shell variable modification characters:

Mpattern
This is used to select only those words (a word is a series of characters that are neither spaces nor tabs) that match the given pattern. The pattern is a wildcard pattern like that used by the shell, where * means 0 or more characters of any sort; ? is any single character; [abcd] matches any single character that is either `a', `b', `c' or `d' (there may be any number of characters between the brackets); [0-9] matches any single character that is between `0' and `9' (i.e. any digit. This form may be freely mixed with the other bracket form), and `\' is used to escape any of the characters `*', `?', `[' or `:', leaving them as regular characters to match themselves in a word. For example, the system makefile <makedepend.mk> uses ``$(CFLAGS:M-[ID]*)'' to extract all the -I and -D flags that would be passed to the C compiler. This allows it to properly locate include files and generate the correct dependencies.
Npattern
This is identical to :M except it substitutes all words that don't match the given pattern.
S/search-string/replacement-string/[g]
Causes the first occurrence of search-string in the variable to be replaced by replacement-string, unless the g flag is given at the end, in which case all occurences of the string are replaced. The substitution is performed on each word in the variable in turn. If search-string begins with a ^, the string must match starting at the beginning of the word. If search-string ends with a $, the string must match to the end of the word (these two may be combined to force an exact match). If a backslash preceeds these two characters, however, they lose their special meaning. Variable expansion also occurs in the normal fashion inside both the search-string and the replacement-string, except that a backslash is used to prevent the expansion of a $, not another dollar sign, as is usual. Note that search-string is just a string, not a pattern, so none of the usual regular-expression/wildcard characters have any special meaning save ^ and $. In the replacement string, the & character is replaced by the search-string unless it is preceded by a backslash. You are allowed to use any character except colon or exclamation point to separate the two strings. This so-called delimiter character may be placed in either string by preceeding it with a backslash.
T
Replaces each word in the variable expansion by its last component (its ``tail''). For example, given OBJS = ../lib/a.o b /usr/lib/libm.a TAILS = $(OBJS:T) the variable TAILS would expand to ``a.o b libm.a.''
H
This is similar to :T, except that every word is replaced by everything but the tail (the ``head''). Using the same definition of OBJS, the string ``$(OBJS:H)'' would expand to ``../lib /usr/lib.'' Note that the final slash on the heads is removed and anything without a head is replaced by the empty string.
E
:E replaces each word by its suffix (``extension''). So ``$(OBJS:E)'' would give you ``.o .a.''
R
This replaces each word by everything but the suffix (the ``root'' of the word). ``$(OBJS:R)'' expands to `` ../lib/a b /usr/lib/libm.''

In addition, the System V style of substitution is also supported. This looks like: $(VARIABLE:search-string=replacement) It must be the last modifier in the chain. The search is anchored at the end of each word, so only suffixes or whole words may be replaced.

3.7.  More on Debugging  

3.8.  More Exercises  

(3.1)
You've got a set programs, each of which is created from its own assembly-language source file (suffix .asm). Each program can be assembled into two versions, one with error-checking code assembled in and one without. You could assemble them into files with different suffixes (.eobj and .obj, for instance), but your linker only understands files that end in .obj. To top it all off, the final executables must have the suffix .exe. How can you still use transformation rules to make your life easier (Hint: assume the error-checking versions have ec tacked onto their prefix)?
(3.2)
Assume, for a moment or two, you want to perform a sort of ``indirection'' by placing the name of a variable into another one, then you want to get the value of the first by expanding the second somehow. Unfortunately, PMake doesn't allow constructs like $($(FOO)) What do you do? Hint: no further variable expansion is performed after modifiers are applied, thus if you cause a $ to occur in the expansion, that's what will be in the result.