Posted on 2022-02-09
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
: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.
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
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>
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