1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
|
---
title: Escaping characters in Makefile
---
TL;DR: visit [this page] for a short and concise version of this article.
{: .alert .alert-success }
[this page]: {% link _notes/makefile.md %}
I'm a big sucker for irrelevant nitpicks like properly quoting arguments in
shell scripts.
I've also recently started using GNU make as a substitute for one-line shell
scripts (so instead of a bunch of scripts like build.sh, deploy.sh, test.sh I
get to have a single Makefile and can just run `make build`, `make deploy`,
`make test`).
As a side note, there's an excellent [Makefile style guide] available on the
web.
I'm going to be using a slightly modified prologue suggested in the guide in
all Makefiles in this post:
[Makefile style guide]: https://clarkgrubb.com/makefile-style-guide
```
MAKEFLAGS += --no-builtin-rules --no-builtin-variables --warn-undefined-variables
unexport MAKEFLAGS
.DEFAULT_GOAL := all
.DELETE_ON_ERROR:
.SUFFIXES:
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
```
`make` invokes a shell program to execute recipes.
As issues of properly escaping "special" characters are going to be discussed,
the choice of shell is very relevant.
The Makefiles in this post specify `bash` explicitly using the `SHELL`
variable, but the same rules should apply for all similar `sh`-like shells.
Quoting arguments
-----------------
You should quote command arguments in `make` rule recipes, just like in shell
scripts.
This is to prevent a single argument from being expanded into multiple
arguments by the shell.
{% capture out1 %}
# Prologue goes here...
test_var := Same line?
export test_var
test:
@printf '%s\n' $(test_var)
@printf '%s\n' '$(test_var)'
@printf '%s\n' $$test_var
@printf '%s\n' "$$test_var"
{% endcapture %}
{% capture out2 %}
Same
line?
Same line?
Same
line?
Same line?
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd='make test' out=out2 %}
This is quite often sufficient to write valid recipes.
One thing to note is that you shouldn't use double quotes `"` for quoting
arguments, as they might contain literal dollar signs `$`, interpreted by the
shell as variable references, which is not something you always want.
Escaping quotes
---------------
What if `test_var` included a single quote `'`?
In that case, even the quoted `printf` invocation would break because of the
mismatch.
{% capture out1 %}
# Prologue goes here...
test_var := Includes ' quote
test:
printf '%s\n' '$(test_var)'
{% endcapture %}
{% capture out2 %}
printf '%s\n' 'Includes ' quote'
bash: -c: line 0: unexpected EOF while looking for matching `''
make: *** [Makefile:11: test] Error 2
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd='make test' out=out2 %}
One solution is to take advantage of how `bash` parses command arguments, and
replace every quote `'` by `'\''`.
This works because `bash` merges a string like `'Includes '\'' quote'` into
`Includes ' quote`.
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
test_var := Includes ' quote
test:
printf '%s\n' '$(call escape,$(test_var))'
{% endcapture %}
{% capture out2 %}
printf '%s\n' 'Includes '\'' quote'
Includes ' quote
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd='make test' out=out2 %}
Surprisingly, this works even in much more complicated cases.
You can have a recipe that executes a command that takes a whole other command
(with its own separate arguments) as an argument.
I guess the most common use case is doing something like `ssh 'rm -rf
$(junk_dir)'`, but I'll use nested `bash` calls instead for simplicity.
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
test_var := Includes ' quote
echo_test_var := printf '%s\n' '$(call escape,$(test_var))'
bash_test_var := bash -c '$(call escape,$(echo_test_var))'
test:
printf '%s\n' '$(call escape,$(test_var))'
bash -c '$(call escape,$(echo_test_var))'
bash -c '$(call escape,$(bash_test_var))'
{% endcapture %}
{% capture out2 %}
printf '%s\n' 'Includes '\'' quote'
Includes ' quote
bash -c 'printf '\''%s\n'\'' '\''Includes '\''\'\'''\'' quote'\'''
Includes ' quote
bash -c 'bash -c '\''printf '\''\'\'''\''%s\n'\''\'\'''\'' '\''\'\'''\''Includes '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' quote'\''\'\'''\'''\'''
Includes ' quote
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd='make test' out=out2 %}
That's somewhat insane, but it works.
Shell output
------------
The `shell` function is one of the two most common ways to communicate with the
outside world in a Makefile (the other being environment variables).
This little `escape` function we've defined is actually sufficient to deal with
the output of the `shell` function safely.
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
cwd := $(shell basename -- "$$( pwd )")
simple_var := Simple value
composite_var := Composite value - $(simple_var) - $(cwd)
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(cwd))'
@printf '%s\n' '$(call escape,$(composite_var))'
{% endcapture %}
{% capture cmd2 %}
mkdir "Includes ' quote" && \
cd "Includes ' quote" && \
make -f ../Makefile test
{% endcapture %}
{% capture out2 %}
Includes ' quote
Composite value - Simple value - Includes ' quote
{% endcapture %}
{% capture cmd3 %}
mkdir 'Maybe a comment #' && \
cd 'Maybe a comment #' && \
make -f ../Makefile test
{% endcapture %}
{% capture out3 %}
Maybe a comment #
Composite value - Simple value - Maybe a comment #
{% endcapture %}
{% capture cmd4 %}
mkdir 'Variable ${reference}' && \
cd 'Variable ${reference}' && \
make -f ../Makefile test
{% endcapture %}
{% capture out4 %}
Variable ${reference}
Composite value - Simple value - Variable ${reference}
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd=cmd2 out=out2 %}
{% include jekyll-theme/shell.html cmd=cmd3 out=out3 %}
{% include jekyll-theme/shell.html cmd=cmd4 out=out4 %}
Environment variables
---------------------
Makefiles often have parameters that modify their behaviour.
The most common example is doing something like `make install
PREFIX=/somewhere/else`, where the `PREFIX` argument overrides the default
value "/usr/local".
These parameters are often defined in a Makefile like this:
```
param_name ?= Default value
```
They should be `escape`d and quoted when passed to external commands, of
course.
However, things get complicated when they contain dollar signs `$`.
`make` variables may contain references to other variables, and they're
expanded recursively either when defined (for `:=` assignments) or when used
(in all other cases, including `?=`).
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
test_var ?= This is safe.
export test_var
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(test_var))'
@printf '%s\n' "$$test_var"
{% endcapture %}
{% capture cmd2 %}
test_var='Variable ${reference}' make test
{% endcapture %}
{% capture out2 %}
Makefile:15: warning: undefined variable 'reference'
Variable
Variable ${reference}
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd=cmd2 out=out2 %}
Here, `$(test_var)` is expanded recursively, substituting an empty string for
the `${reference}` part.
One attempt to solve this is to escape the dollar sign in the variable value,
but that breaks the `"$$test_var"` case:
{% capture cmd1 %}
test_var='Variable $${reference}' make test
{% endcapture %}
{% capture out1 %}
Variable ${reference}
Variable $${reference}
{% endcapture %}
{% include jekyll-theme/shell.html cmd=cmd1 out=out1 %}
A working solution would be to use the `escape` function on the unexpanded
variable value.
Turns out, you can do just that using the `value` function in `make`.
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
test_var ?= This is safe.
test_var := $(value test_var)
export test_var
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(test_var))'
@printf '%s\n' "$$test_var"
{% endcapture %}
{% capture cmd2 %}
test_var="Quote '"' and variable ${reference}' make test
{% endcapture %}
{% capture out2 %}
Quote ' and variable ${reference}
Quote ' and variable ${reference}
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd=cmd2 out=out2 %}
This doesn't quite work though when [overriding variables] on the command line.
For example, this doesn't work:
[overriding variables]: https://www.gnu.org/software/make/manual/html_node/Overriding.html#Overriding
{% capture cmd1 %}
make test test_var='Variable ${reference}'
{% endcapture %}
{% capture out1 %}
Makefile:16: warning: undefined variable 'reference'
make: warning: undefined variable 'reference'
Variable
Variable
{% endcapture %}
{% include jekyll-theme/shell.html cmd=cmd1 out=out1 %}
This is because `make` ignores all assignments to `test_var` if it's overridden
on the command line (including `test_var := $(value test_var)`).
This can be fixed using the `override` directive for these cases only.
A complete solution that works for seemingly all cases looks like something
along these lines:
```
ifeq ($(origin test_var),environment)
test_var := $(value test_var)
endif
ifeq ($(origin test_var),environment override)
test_var := $(value test_var)
endif
ifeq ($(origin test_var),command line)
override test_var := $(value test_var)
endif
```
Here, we check where the value of `test_var` comes from using the `origin`
function.
If it was defined in the environment (the `environment` and `environment
override` cases), its value is prevented from being expanded using the `value`
function.
If it was overridden on the command line (the `command line` case), the
`override` directive is used so that the unexpanded value actually gets
assigned.
The snippet above can be generalized by defining a custom function that
produces the required `make` code, and then calling `eval`.
```
define noexpand
ifeq ($$(origin $(1)),environment)
$(1) := $$(value $(1))
endif
ifeq ($$(origin $(1)),environment override)
$(1) := $$(value $(1))
endif
ifeq ($$(origin $(1)),command line)
override $(1) := $$(value $(1))
endif
endef
test_var ?= This is safe.
$(eval $(call noexpand,test_var))
```
I couldn't find a case where the combination of `escape` and `noexpand`
wouldn't work.
You can even safely use other variable as the default value of `test_var`, and
it works:
{% capture out1 %}
# Prologue goes here...
escape = $(subst ','\'',$(1))
define noexpand
ifeq ($$(origin $(1)),environment)
$(1) := $$(value $(1))
endif
ifeq ($$(origin $(1)),environment override)
$(1) := $$(value $(1))
endif
ifeq ($$(origin $(1)),command line)
override $(1) := $$(value $(1))
endif
endef
simple_var := Simple value
test_var ?= $(simple_var) in test_var
$(eval $(call noexpand,test_var))
simple_var := New simple value
composite_var := Composite value - $(simple_var) - $(test_var)
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(test_var))'
@printf '%s\n' '$(call escape,$(composite_var))'
{% endcapture %}
{% capture out2 %}
New simple value in test_var
Composite value - New simple value - New simple value in test_var
{% endcapture %}
{% capture cmd3 %}
make test test_var='Variable ${reference}'
{% endcapture %}
{% capture out3 %}
Variable ${reference}
Composite value - New simple value - Variable ${reference}
{% endcapture %}
{% capture cmd4 %}
test_var='Variable ${reference}' make test
{% endcapture %}
{% capture out4 %}
Variable ${reference}
Composite value - New simple value - Variable ${reference}
{% endcapture %}
{% include jekyll-theme/shell.html cmd='cat Makefile' out=out1 %}
{% include jekyll-theme/shell.html cmd='make test' out=out2 %}
{% include jekyll-theme/shell.html cmd=cmd3 out=out3 %}
{% include jekyll-theme/shell.html cmd=cmd4 out=out4 %}
|