FizzBuzz in GNU make
- hacks
When I couldn’t fall asleep last night 1 my sleep-deprived brain suddenly went “I bet you can’t write a Makefile
for FizzBuzz.” While I obviously should have responded with Homer Simpson’s classic “Shut up, brain, or I’ll stab you with a Q-tip.”, it was too late, I had already nerd sniped myself. I fully expected to waste way too much time on this but it only (?) took a little over an hour to come up with a solution.
The Makefile
Without further ado, here’s the thing that should probably never have happened:
START := 1
END := 15
NUMS := $(shell seq $(START) $(END))
fizzbuzz: $(NUMS)
mod = $(shell echo $$(( $1 % $2 )))
define fb
$1:
ifeq "$(call mod, $1, 15)" '0'
@echo FizzBuzz
else ifeq "$(call mod, $1, 3)" '0'
@echo Fizz
else ifeq "$(call mod, $1, 5)" '0'
@echo Buzz
else
@echo $1
endif
endef
$(foreach _,${NUMS},$(eval $(call fb,$_)))
.PHONY: fizzbuzz $(NUMS)
Let’s see it in action:
$ make fizzbuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
Since fizzbuzz
is the first target we can leave it off. And we can also override START
and END
from the command line:
$ make START=19 END=30
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
Walkthrough
So how does this work? We start by defining a bunch of variables for the start and end values of the number range we want to check:
START := 1
END := 15
Next we use the shell
function to construct a sequence of numbers with seq
, so NUMS
will contain 1 2 3 [...] 14 15
.
Now we can set up a fizzbuzz
target and list all the numbers in NUM
as prerequisites:
fizzbuzz: $(NUMS)
Next up, a little helper function for modulo, since make
itself doesn’t support arithmetic operations. This uses the shell’s arithmetic expansion mechanism and does not rely on external programs like bc
.
mod = $(shell echo $$(( $1 % $2 )))
Now for the fun part :-) First, we use define
to, well, define a sequence of make
expressions that we will later eval
to turn them into targets:
define fb
$1:
ifeq "$(call mod, $1, 15)" '0'
@echo FizzBuzz
else ifeq "$(call mod, $1, 3)" '0'
@echo Fizz
else ifeq "$(call mod, $1, 5)" '0'
@echo Buzz
else
@echo $1
endif
endef
The code should be relatively straightforward: we refer to this block of statement as fb
when calling it and $1
will receive the value of the first argument. Then we do a couple of ifeq
checks 2
Now we can use foreach
in combination with eval
and call
to define targets for all the individual numbers:
$(foreach _, ${NUMS}, $(eval $(call fb, $_)))
Last but not least, let’s declare all targets as phony to indicate that they don’t related to real files.
Summary
While I probably need better hobbies, this was a fun way to kill some time and I definitely learned a bit more about make
today. When a friend (hi, Joe 👋) asked me why I did this, my answer was clear: “Because I could.” Hack on everyone and don’t forget to have fun with technology!
Updates
- This being the Internet (see xkcd #386), it was brought to my attention that “[this] relies on a specific and nonportable shell builtin.” You must be fun at parties. Anyway, let’s look into this a bit:
make
‘s default shell is/bin/sh
. It’s true thatseq
is not part of POSIX, but I tried thisMakefile
on macOS, NixOS, Arch Linux, and OpenBSD (where you also need topkg_add gmake
) and it worked on all of them. It also works when explicitly setting the shell to Bash, ZSH, and OpenBSD’s version of ksh. That’s portable enough for me. In all fairness though, it did NOT work with Fish, though not because ofseq
(which is available) but because the POSIX-style arithmetic is not supported. - User “GeoffWozniak” on lobste.rs joined in the fun and made a version that doesn’t rely on the
shell
function at all. That’s the spirit!