Useful bmake modifier patterns

Posted on 2022-02-09

Back to index

💬 Introduction

Variable modifiers in bmake (make(1) on FreeBSD) are very powerful. Here are some examples of what you can do with them.

I wrote this for myself a while back and am now publishing this here. Some parts of this were very helpful when I switched USES=cargo to use the .crate file extension for crates on new ports and when I overhauled the Git source support in USES=cargo last year.

🌍Commit: Mk/Uses/cargo.mk: Use canonical crate file extension going forward

🌍Commit: Uses/cargo: Rework git source support based on patch-in-config sections

All of these examples can be tried out on the command line with

$ printf 'V=${:Ufoo}' | make -f - VV
foo

⏎ Return default value

:U allows you to return a default value in case a variable is undefined. The following will return 'foo' if FLAVOR was left undefined.

FLAVOR:Ufoo

The inverse is :D which returns a set value if a variable is defined. The following will return 'foo' if FLAVOR was defined:

FLAVOR:Dfoo

$ make -f /dev/null FLAVOR=bla -VFLAVOR:Dfoo
foo

Note that 'x:Uy' is *not* a shortcut for

${:?defined(x):${x}:y}

and neither is 'x:Dy' a shortcut for

${:?defined(x):y:${x}}

:D and :U will only ever be evaluated once but :? will be evaluated whenever it is encountered and thanks to the ${x} indirection inside, the value of 'x' will be looked up again later.

🤐 Zip up words from two variables

Zipping up 2 lists A and B can be done with

A:range:@i@${A:[$i]} ${B:[$i]}@

If A and B are not the same length the result might be of an odd-numbered length, so cannot be used with .for with multiple loop variables.

With 3 lists A, B, and C this becomes:

A:range:@i@${A:[$i]} ${B:[$i]} ${C:[$i]}@

etc.

$ printf 'A=1 2 3\nB=a b c\nC=x y z\nV=${A:range:@i@${A:[$i]} ${B:[$i]} ${C:[$i]}@}' | make -f - -VV
1 a x 2 b y 3 c z

№ Enumerate values

Now that we know how to zip up word lists we can easily figure out how to enumerate a variable:

$ printf 'A=a b c\nV=${A:range:@i@$i ${A:[$i]}@}' | make -f - -VV
1 a 2 b 3 c

or

$ cat <<EOF | make -f -
A=	a b c
.for index word in ${A:range:@i@$i ${A:[$i]}@}
.info "${index}. ${word}"
.endfor
all:
EOF
make: "(stdin)" line 3: "1. a"
make: "(stdin)" line 3: "2. b"
make: "(stdin)" line 3: "3. c"

We still need to handle the case of empty lists because a suprising quirk of ':range' is that 'A:range' will return 1 if A is defined but empty:

$ printf 'A=\nV=${A:range:@i@$i ${A:[$i]}@}' | make -f - -VV
1

Luckily we can easily handle that:

$ printf 'A=\nV=${empty(A):?:${A:range:@i@$i ${A:[$i]}@}}' | make -f - -VV
<empty>

🐨 Lazily run shell commands once

We have the != assignment operator and the :sh and :! modifiers that capture stdout of a shell command. They behave differently. This is best shown with a couple of examples.

With != (expand V 3 times and command runs only once):

$ printf 'V!=echo ran>&2; echo foo\n.info hi' | make -f - -VV -VV -VV
ran
make: "(stdin)" line 2: hi
foo
foo
foo

!= forces variable expansion at the time of definition, but :! and :sh are lazy. This is exemplified by how the 'hi' appears only after the command ran.

Laziness with ':!' but uncached (expand V 3 times and command runs 3 times):

$ printf 'V=${:!echo ran>&2; echo foo!}\n.info hi' | make -f - -VV -VV -VV
make: "(stdin)" line 2: hi
ran
foo
ran
foo
ran
foo

This is not quite working as !=. The first 'ran' comes after the 'hi' due to the laziness of :!. The command is also rerun every time we access the value.

But there is a way around the rerun problem by combining :! with :_ and :?.

Lazy :! (expand V 3 times and command runs only once):

$ printf 'V=${defined(_CACHE):?${_CACHE}:${:!echo ran>&2; echo foo!:_=_CACHE}}\n.info hi' | make -f - -VV -VV -VV
make: "(stdin)" line 2: hi
ran
foo
foo
foo

This document is also available on Gemini: gemini://tobik.me/2022/02/09/make-patterns.gmi