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
|
---
title: Escaping characters in Makefile
excerpt: Making less error-prone.
---
I'm a big sucker for irrelevant neatpicks 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 the prologue suggested in the guide in all Makefiles in
this post:
[Makefile style guide]: https://clarkgrubb.com/makefile-style-guide
```
MAKEFLAGS += --warn-undefined-variables
.DEFAULT_GOAL := all
.DELETE_ON_ERROR:
.SUFFIXES:
SHELL := bash
.SHELLFLAGS := -e -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.
TL;DR
-----
* Quote command arguments in Makefiles using single quotes `'`.
* Don't use `'` and `$` in stuff like file paths/environment variable values,
and you're good to go.
* To escape `$(shell)` function output, define a helper function:
escape = $(subst ','\'',$(1))
You can then replace `'$(shell your-command arg1 arg2)'` with
`'$(call escape,$(shell your-command arg1 arg2))'`.
* To escape environment variable values, define another helper function:
escape_var = $(call escape,$(value $(1)))
You can then replace `'$(VAR_NAME)'` with `'$(call escape_var,VAR_NAME)'`.
* Don't override variables using `make var1=value1 var2=value2`, use your shell
instead: `var1=value1 var2=value2 make`.
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.
```
$ cat > Makefile
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"
$ make test
printf '%s\n' Same line?
Same
line?
printf '%s\n' 'Same line?'
Same line?
printf '%s\n' $test_var
Same
line?
printf '%s\n' "$test_var"
Same line?
```
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.
```
$ cat > Makefile
test_var := Includes ' quote
test:
printf '%s\n' '$(test_var)'
$ make test
printf '%s\n' 'Includes ' quote'
bash: -c: line 0: unexpected EOF while looking for matching `''
make: *** [Makefile:11: test] Error 2
```
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`.
```
$ cat > Makefile
escape = $(subst ','\'',$(1))
test_var := Includes ' quote
test:
printf '%s\n' '$(call escape,$(test_var))'
$ make test
printf '%s\n' 'Includes '\'' quote'
Includes ' quote
```
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.
```
$ cat > Makefile
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))'
$ make test
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
```
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.
```
$ cat > Makefile
escape = $(subst ','\'',$(1))
cwd := $(shell basename -- "$$( pwd )")
export cwd
echo_cwd := printf '%s\n' '$(call escape,$(cwd))'
bash_cwd := bash -c '$(call escape,$(echo_cwd))'
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(cwd))'
@printf '%s\n' "$$cwd"
@bash -c '$(call escape,$(echo_cwd))'
@bash -c '$(call escape,$(bash_cwd))'
$ ( mkdir -p -- "Includes ' quote" && cd -- "Includes ' quote" && make -f ../Makefile test ; )
Includes ' quote
Includes ' quote
Includes ' quote
Includes ' quote
$ ( mkdir -p -- 'Maybe a comment #' && cd -- 'Maybe a comment #' && make -f ../Makefile test ; )
Maybe a comment #
Maybe a comment #
Maybe a comment #
Maybe a comment #
$ ( mkdir -p -- 'Variable ${reference}' && cd -- 'Variable ${reference}' && make -f ../Makefile test ; )
Variable ${reference}
Variable ${reference}
Variable ${reference}
Variable ${reference}
```
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 quoted when passed to external commands, of course.
To prevent mismatched quotes, the `escape` function might seem useful, but in
case of environment variables, a complication arises 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 `?=`).
```
$ cat > Makefile
escape = $(subst ','\'',$(1))
test_var ?= This is safe.
export test_var
echo_test_var := printf '%s\n' '$(call escape,$(test_var))'
bash_test_var := bash -c '$(call escape,$(echo_test_var))'
.PHONY: test
test:
@printf '%s\n' '$(call escape,$(test_var))'
@printf '%s\n' "$$test_var"
@bash -c '$(call escape,$(echo_test_var))'
@bash -c '$(call escape,$(bash_test_var))'
$ test_var='Variable ${reference}' make test
Makefile:18: warning: undefined variable 'reference'
Variable
Variable ${reference}
Variable
Variable
```
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:
```
$ test_var='Variable $${reference}' make test
Variable ${reference}
Variable $${reference}
Variable ${reference}
Variable ${reference}
```
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`.
```
$ cat > Makefile
escape = $(subst ','\'',$(1))
escape_var = $(call escape,$(value $(1)))
test_var ?= This is safe.
export test_var
echo_test_var := printf '%s\n' '$(call escape_var,test_var)'
bash_test_var := bash -c '$(call escape_var,echo_test_var)'
.PHONY: test
test:
@printf '%s\n' '$(call escape_var,test_var)'
@printf '%s\n' "$$test_var"
@bash -c '$(call escape_var,echo_test_var)'
@bash -c '$(call escape_var,bash_test_var)'
$ test_var='Variable ${reference}' make test
Variable ${reference}
Variable ${reference}
Variable ${reference}
Variable ${reference}
```
You can still use the original `escape` function to escape `shell` output.
One thing to note is that I couldn't find a way to prevent variable values from
being expanded 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
```
$ make test test_var='Variable ${reference}'
make: warning: undefined variable 'reference'
Variable ${reference}
Variable
Variable ${reference}
Variable ${reference}
$ make test test_var='Variable $${reference}'
Variable $${reference}
Variable ${reference}
Variable $${reference}
Variable $${reference}
```
As a workaround, set parameter values using your shell: `var_name=value make
...`.
|