nmake recognizes some standard programming constructs that are described in chapter 5 of the manual. Programmers usually pick the important points up quickly, so we won't spend time showing you how to use if statements or write for loops. Instead, we'll say a few words about print, introduce two other important commands, and then show you how to write action blocks designed for nmake rather than the shell.
We've already used print in an example, and it wasn't difficult. When nmake finds a print statement it takes the rest of the line, expands variable references, and copies the result to standard output. The syntax is:
print [ options ] message ...
Options were introduced in 3.1's implementation of print. We're not going to describe them here, except to say that -- (i.e., two minus signs) marks the end of the options. In other words, if you're using a new version of nmake
print -- message
prints your message, even if it starts with a minus sign.
When nmake finds an error statement it takes the rest of the line, expands variable references, and usually copies the result to standard error. The syntax is:
error [ level ] message ...
level is an optional integer  that controls the format of the message and determines what happens after nmake prints the message. Setting level to 0 is the same as leaving it out, as long as your message doesn't begin with a number. A level greater than 2 means abort after printing the message. We can experiment with different error levels by putting
error $(LEVEL) this message comes from error hello : silent echo "hello, world"
in a file named error.mk. For example, if we type
nmake -f error.mk LEVEL=3
we get the message from error
make: this message comes from error
and nothing else, because a level of 3 (or higher) means abort. Increase level and nothing obvious changes, but check nmake's exit status after it aborts and you'll see it depends on level.
Cooperating makefiles usually need to share information, and that's why a mechanism for including files is so important. When nmake finds an include statement it takes the rest of the line, expands variable references, and reads the files named in the result. The syntax is:
include [ - ] file ...
The optional minus sign disables the warning about a file that can't be found. Individual files are space separated and optionally quoted, so you'll often see
even though the quotes are unnecessary. nmake complains when it can't find an include file, but it's just a warning that usually disappears when nmake reads the objectfile. For example, put
include global.mk hello : silent echo "$(<), $(AUDIENCE)"
in include.mk and type
nmake -f include.mk hello
and we get:
make: "include.mk", line 1: global.mk: cannot read include file hello,
nmake couldn't find global.mk and told us so, but run it again and the warning goes away. The message from hello is still incomplete, because AUDIENCE isn't defined, but put
AUDIENCE = world
in a file named global.mk and both problems are fixed.
Action blocks are usually handed to the shell, but adding .MAKE to a prerequisite list means reading the action block (just like it's another makefile) whenever it builds the target. It's not hard to figure out who should get an action block. You let the shell do the work when you want to run Unix commands (e.g., a compiler) or do things in the file system, and you let nmake handle the action block when you want to define variables or assertions that can be used later on.
Most of our examples have been so simple that we could easily rewrite the action blocks and hand them to nmake. For example,
AUDIENCE = world goodbye hello : .MAKE print $(<), $(AUDIENCE)
and message.mk seem to be equivalent, even though the shell handles one action block and nmake handles the other. But even these two simple makefiles can behave differently. Add things to AUDIENCE that mean something special to the shell, print, or echo and you may notice a difference. For example, save the last makefile in make.mk and we get different results from
nmake -f message.mk AUDIENCE='$LOGNAME'
nmake -f make.mk AUDIENCE='$LOGNAME'
because the shell understands $LOGNAME, but nmake doesn't.
We haven't talked much about environment variables, but put parentheses around LOGNAME and we get the same thing from both makefiles. nmake reads your environment when it starts, so it recognizes LOGNAME, and that's why we get the right answer when nmake expands $(LOGNAME). Collisions aren't a problem, because variables defined on the command line or in a makefile override environment variables.
Even though the shell is usually the right choice, there may be times when we need to exercise some control over nmake using an action block. Here's an example that we'll save in setup.mk - it's contrived, but it's also easy and will help you understand what happens when nmake handles an action block:
setup : .MAKE AUDIENCE = world goodbye hello : silent echo "$(<), $(AUDIENCE)"
We started with a target named setup, added .MAKE as a prerequisite, and copied message.mk into setup's action block. You deserve an explanation if you're wondering why we didn't use
setup : .MAKE include message.mk
as our example: we wanted to make you look at a variable definition, a blank line, and an assertion in an action block. Even though we can see the assertion that defines hello, it's hidden in an action block, so when we type
nmake -f setup.mk hello
make: don't know how to make hello
because nmake won't find it, but add setup to the command line
nmake -f setup.mk setup hello
and we get:
Building setup defined AUDIENCE, hello, and goodbye, and that allowed nmake to build hello. By the way, you should have no trouble understanding what happens when we type:
nmake -f setup.mk AUDIENCE='New Jersey' setup hello
The command line assignment defined AUDIENCE, but building setup replaced the definition, and that's why the message doesn't change.