GNU Make Escaping: A Walk on the Wild Side

[article]
Summary:

Sometimes you find yourself needing to insert a special character in a Makefile: perhaps you need a newline inside a \$(error) message, or a space character in a \$(subst) or a comma as the argument to a GNU Make function.  Those three simple things can be frustratingly hard in GNU Make; this article takes you through simple GNU Make syntax that removes the frustration.

Sometimes you find yourself needing to insert a special character in a Makefile: perhaps you need a newline inside a \$(error) message, or a space character in a \$(subst) or a comma as the argument to a GNU Make function.  Those three simple things can be frustratingly hard in GNU Make; this article takes you through simple GNU Make syntax that removes the frustration.

GNU Make's escaping rules
GNU Make's handling of 'tab' as the start of a command is a legendary language feature, but some other special characters can trip you up.  GNU Make's handling of \$, %, ?, *, [, ~, \, and # are all special.

Every GNU Make user is familiar with \$ for starting a macro reference.  It's possible to write \$(macro) or \${macro} to get the value of macro and if the macro name is a single character (such as a) then the parens can be dropped and the short hand \$a used.

To get a literal \$ you write \$\$.  So to define a macro containing a single \$ symbol you'd write:

dollar := \$\$

The % character rears its ugly head in three different places: in the vpath directive, in a \$(patsubst) and in a pattern or static-pattern rule.

Escaping % is not as simple as \$, but it only needs to be done in the three situation above, and the same rules apply for each.

The three rules for % escaping are:

1. % can be escaped with a single \ character (i.e. \% becomes a literal %.

2. If you need to put a literal \ in front of a % (i.e. you want the \ to not escape the %) then escape it with \ (i.e. \\% becomes a literal \ followed by a % character that will be used for the pattern match).

3. Don't worry about escaping \ anywhere else in a pattern, it will be treated as literal (i.e. \hello is \hello).

?, *, [, and ] get treated specially when they appear in a filename.  A Makefile that does

*.c:

@command

Will actually search for all .c files in the current directory and define a rule for each.  The target (and the same applies for prerequisites and files mentioned in the include
directive) are globbed if they contain a wildcard character.  The globbing characters have the same meaning as in the Bourne shell.

The ~ character is also handled specially in filenames and is expanded to the home directory of the current user.

All of those special filename characters can be escaped with a \.  The following Makefile defines a rule for the file named literally *.c.

\*.c:

@command

As well as the escaping function mentioned in this section, the \ can also be used as an continuation character at the end of a line:

all: \

prerequisite \

something else

@command

The # character is used to start a comment and it can be made literal with a \ escape:

pound := \#

I just want a newline!

GNU Make does its best to insulate you from the newline character.  You can't escape a newline, there's no syntax for special characters (e.g. you can't write \n), and even the \$(shell) function strips newlines from the returned value.

But you can define a macro that contains a newline using the define syntax:

define newline

endef

(Note that there are two blanks lines in the definition above).  With that definition \$(newline) will expand to a newline and it can be used to format an error message nicely:

\$(error This is an error message\$(newline)with two lines)

Because of GNU Make's rather liberal macro naming rules it's possible to actually define a macro called \n so if you line to keep things looking familiar you can do

define \n

endef

\$(error This is an error message \$(\n)with two lines)

(More on special macro names in The Twilight Zone below)

Function arguments: space and comma
A problem that many GNU Make users run into is that handling of spaces and commas in GNU Make function arguments.  Consider the following use of \$(subst) (which takes three arguments separated by commas: the from text, the to text and the string to change):

spaces-to-commas = \$(subst  ,,,\$1)

It defines a function called spaces-to-commas to convert all spaces in its argument to commas (might be handy for making a CSV file for example).  Unfortunately, it doesn't work. It doesn't work for two reasons:

1. The first argument of the \$(subst) is a space.  Unfortunately, GNU Make strips all leading and trailing whitespace around function arguments.  In this case, the first argument is interpreted as an empty string.
2. The second argument is a comma.  Unfortunately, GNU Make cannot distinguish between the commas used for argument separators and the comma as an argument. In addition, there's no way to 'escape' the comma.

Fortunately, it's possible to work around this by knowning that GNU Make does the whitespace stripping, and separation of arguments before it doesn any expansion of the arguments themselves.   So, if we can define a macro containing a space and a macro containing a comma it will be possible to write:

spaces-to-commas = \$(subst \$(space),\$(comma),\$1)

to get the desired effect.

Defining a macro containing a comma is easy:

comma := ,

but space is a bit harder.   There are a couple of ways of defining a space.  The first uses the fact that whenever you append to a macro (using +=) a space is inserted before the appended text.
space :=
space +=

Another way is to first define a macro that contains nothing and then use it to surround the space so that it doesn't get stripped by GNU Make

blank :=
space := \$(blank) \$(blank)

This technique can also be used to get a literal tab character into a macro

blank :=
tab := \$(blank)   \$(blank)

(The whitespace in the definition of tab was created by hitting the tab key).

Much in the way I defined \$(\n) above it's possible to define specially named space and comma macros.  GNU Make's rules are liberal enough to allow us to do:
, := ,
blank :=
space := \$(blank) \$(blank)
\$(space) := \$(space)

The first line defines a macro called , (which can be used as \$(,) or even \$,) containing a comma. The last three lines define a macro called space containing a space character and then use it to define a macro name (that's right, it's name is a space character) containing a space.

With that definition it's possible to write \$( ) or even \$ to get a space character. Using those definitions the spaces-to-commas function can be written:

spaces-to-commas = \$(subst \$( ),\$(,),\$1)

which I think it very clear.

The Twilight Zone
If you read this column regularly, you'll know that I'm not going to let a chance to really mess with GNU Make go by without pushing it to its limits.  Here are some other interesting macro definitions:

# Defining the \$= or \$(=) macro which has the value =

equals := =
\$(equals) := =

# Define the \$# or \$(#) macro which has the value #

hash := \#
\$(hash) := \#

# Define the \$: or \$(:) macro which has the value :

colon := :
\$(colon) := :

# Define the \$(\$\$) macro which has the value \$

dollar := \$\$
\$(dollar) := \$\$

These probably aren't useful, but if you really want to push GNU Make syntax to its limits try this:

+:=+

Yes, that defines a variable called + containing a +.

Paul's warning
When I first blogged about some of this, I received the following warning from Paul Smith (the maintainer of GNU Make):

I don't recommend using a space as a variable name. I've toyed with the idea of allowing some special syntactic sugar for user-defined functions, that would allow you to omit the \$(call ...) prefix and just invoke the user-defined function directly, like:

\$(my-func arg1, arg2)

Obviously the way this works is that if make detects a space in the variable name, it would assume that it is a user-defined function. If this were to happen I'd probaby make whitespace in variable names illegal, just to cut down on confusion and bizarre misbehaviors.

OK, I'll be careful.