I’ve written in the past about using Makefiles for build automation and documentation. But sometimes I wish to provide help to users, that doesn’t require being a Makefiles expert to read!
Let’s explore how we can teach users our build system by turning
the project’s Makefile (and awk
) into a cheap documentation-as-code tool.
make help
As expected, when asking to disable colors:
make help NO_COLOR=1
This all works with a Makefile target named “help”, running awk
over the Makefile itself to filter/transform it into what we want to see.
# Run 'make help' to see guidance on usage of this Makefile
# Note that comments with single # aren't rendered, requires ##
ANSI_COLOR_RED=\033[31;1;4m
ANSI_COLOR_RESET=\033[0m
ifneq (${NO_COLOR}, )
ANSI_COLOR_RED=
ANSI_COLOR_RESET=
endif
## Generate the help message by reading the Makefile
.PHONY: help
help:
@echo "This makefile contains the following targets," \
"from most commonly used to least:" \
"(docs first, then target name)"
@echo "Set NO_COLOR=1 to disable ANSI color coding\n"
@awk \
'/^##/ {sub(/## */,""); print} \
/^[a-z0-9-]+:/ && !/\.PHONY:/ \
{sub(/:.*/, ""); \
print "⮡ ${ANSI_COLOR_RED}" \
$$0 \
"${ANSI_COLOR_RESET}\n" \
}' \
Makefile
# Sample Makefile command being documented:
## Set up the virtualenv and install the package + dependencies
.PHONY: install
install:
poetry install
I’m happy to say that, while I’ve seen this idea a few times out there, this specific implementation has a few things not seen elsewehere, and I’m pretty happy about it. Let’s dig into it.
Looking at the file’s content, top to bottom.
First, a simple message for curious people peeking at the file, to tell them that help is on the way:
# Run 'make help' to see guidance on usage of this Makefile
This is to give hope to people who may have opened the Makefile out of desperation, not interested in details, just wanting to know how to operate the repo. The message for them is “we’ve got docs, no need to read the whole file!”
# Note that comments with single # aren't rendered, requires ##
We follow with a quick explanation about how we determine the comments that get
rendered (useful info for users), from the ones that aren’t (implementation
details). Of course, this specific line being only one #
means it will not
show in the rendered version: an implementation detail indeed.
ANSI_COLOR_RED=\033[31;1;4m
ANSI_COLOR_RESET=\033[0m
ifneq (${NO_COLOR}, )
ANSI_COLOR_RED=
ANSI_COLOR_RESET=
endif
We want to print help in color for convenience, but we’re very aware that ANSI color codes aren’t for everyone, all the time, because we’ve read and abide by https://no-color.org/, which states:
Command-line software which adds ANSI color to its output by default should check for a
NO_COLOR
environment variable that, when present and not an empty string (regardless of its value), prevents the addition of ANSI color.
So we set internal variables to the values of ANSI color codes for red + color-reset,
and if the NO_COLOR
variable is set (not equal to empty string, per the quote
“regardless of its value”), override those values to be the
empty string, nullifying them, printing nothing instead, no color markers.
## Generate the help message by reading the Makefile
.PHONY: help
help:
Finally we get to a target and its documentation!
This is a simple Makefile target, set as phony (this is NOT about creating a
file named “help”, but a command to run always when running make help
, see
Phony Targets documentation)
with some user-facing documentation above it.
As for the content of this target, what to run when asked for help…
@echo "This makefile contains the following targets," \
"from most commonly used to least:" \
"(docs first, then target name)\n"
First, we runs the command echo (the shell script built-in), printing a
top-level general help message. Note the @
prefix, which is a Makefile thing,
used to disable printing the command being run before its output: We don’t need
to double-print the message (command to run as well as the resulting message
from running it), so we put an @
in front.
@echo "Set NO_COLOR=1 to disable ANSI color coding\n"
Similarly, we notify the users about the NO_COLOR
flag.
And finally the real meat of this trick:
@awk \
'/^##/ {sub(/## */,""); print} \
/^[a-z0-9-]+:/ && !/\.PHONY:/ \
{sub(/:.*/, ""); \
print "⮡ ${ANSI_COLOR_RED}" \
$$0 \
"${ANSI_COLOR_RESET}\n" \
}' \
Makefile
This awk
command processes the Makefile itself for printing docs.
In general, awk
is called like awk 'a whole awk program in here'
filename-to-process
, with in our case the
filename being Makefile
.
As for the awk program being run over the file: awk’s language works by defining
patterns then actions to do on each pattern. Patterns usually are regular
expressions delimited by slashes like /pattern/
, filtering the line being
processed, and actions are delimited by {do_stuff}
. Specifically, we run two
patterns-and-actions.
# Filter to only match lines starting with ##
/^##/
# ... and, for these matching lines...
{
sub(/## */,""); # remove the ## prefix in line + whitespace
print # and print the modified line
}
The result of running this single pattern + action on our previous file is:
And the second command, broken down first between pattern then action:
# Filter to makefile targets only.
# done via filtering lines that have a : character
/^[a-z0-9-]+:/
&&
# but Phony targets would be captured here, so exclude them
# using the ! in front of pattern
# Escaping . which has a meaning in regexes
!/\.PHONY:/
That pattern ensures we have the Makefile target selected, but we want to make it printed with color etc. We do this via the action run over the pattern, below.
{
# Erase anything after the target name, like dependencies
sub(/:.*/, "");
# Print the modified line, with some text before and after
print \
"⮡ ${ANSI_COLOR_RED}" # pretty arrow + 4 spaces + red color
$$0 # The original line = $0. Makefiles escape $ as $$
"${ANSI_COLOR_RESET}\n" # Print the ANSI color reset
}
I was trying to tailor this script to be convenient for prospective users, while remaining fairly simple to implement, and easy to explain to curious minds. Let’s review a few of the technical decisions that went into it.
The first choice was having the docs show up first in the result, then the command being documented: Usually, documentation systems print the command first (as a kind of section header), and then describe it with documentation.
But in the Makefile itself it would make no sense to have the command first, then have the documentation below it. Usually the docs are like javadocs, written just before the code, and the docs renderer swaps the order.
To find a way to do command then docs would have meant the script would need to store commands and docs, then swap their order in the output, which increases complexity of our script.
Much easier to highlight that the command being printed is after the docs by taking advantage of indentation, with fancy characters like that right-angled arrow, and of course we color the output.
So we just “filter and print” the whole file, top to bottom, in the order the file is written, taking advantage of awk doing all the heavy lifting for us.
Of course, when we use ANSI colors, it’s pretty important that we support
the NO_COLOR
directive, it’s just being a good command-line citizen.
I could have avoided the echo command, and instead have all the text inside the
Makefile, with two ##
for the “these are the commands” and “Set NO_COLOR=1”
messages. The choice to use an “echo” instead is minor, but it highlights an
important point around documentation:
First, this would mean we’re mixing up the two kinds of users of the Makefile: the people needing help about the build system, are likely not the people who will open up the makefile and read a block of text from it.
The other issue is that after the overall help message, I wanted a couple of
empty lines
of separation, which would require me to write into the Makefile a couple of empty lines with just ##
,
defacing the Makefile for the sake of the aesthetics of the documentation
message?
Nah, much nicer to have the Makefile have an extra echo command to prefix the actual documentation. This way we’ve split messages aimed at basic help vs Makefile-editing wizards.
As described, we have two kinds of users to write docs for: devs who just want
simple help (using make help
), who likely will be overwhelmed by the details
of the Makefile (for now), and devs who will read the whole Makefile and likely
edit it. Because of the different targets, we need to make some docs messages
(comments) visible to one group but not the other.
I thus steal the trick from javadocs, that simple comments are technical details, but comments with two characters are aimed at documentation, generated by extracting from the code.
In the purest sense, this is documentation-as-code: Documentation living inside the repository next to the code it described, the docs is managed as code, versioned, reviewed etc. I do seem to gravitate around documentation as code in multiple posts, so I may try to collect my thoughts around it in a future article.
I’ve presented before that I really like Makefiles, and I acknowledge that for Makefile non-experts, it can be difficult to understand what’s going on.
My interest is making sure that entry-level users can find guidance on running
project-related commands without having to become experts themselves.
Usually, the discovery process around Makefiles means opening the Makefile and
getting overwhelmed by the complexity. This system of make help
addresses
that, by providing some kind of documentation-as-code, summarizing what a user
may want to do, easing them in.
On the way I got to talk about a few things I like, from awk, to docs as code, and justify the multiple users of documentation.
So I hope this Makefile documentation-as-code trick is useful to pass on.