Posted on 8 September 2022.
Developer productivity is a top priority for most engineering organizations. Here at Padok, we apply the DevOps philosophy: developers and SREs alike own all the code. The key to productivity from local development all the way to production is automation. Makefiles are great tools for automation! And isn't it nice when our tools produce beautiful output?
No time to waste. This is what a beautiful Makefile looks like:
Just give me stuff to copy and paste please
Add this to your Makefile and enjoy your beautiful help
target:
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: fmt
fmt: ## Format source code.
go fmt ./...
.PHONY: vet
vet: ## Vet source code.
go vet ./...
How this beautiful Makefile works
I did not come up with the magic awk command that makes Makefiles beautiful. I got it from the Go operator-sdk for writing Kubernetes operators, which itself got it from kubebuilder, a code generation engine for Kubernetes operators.
However, reverse engineering it enhanced my understanding of awk and terminal formatting. Let’s dive into how the magic one-line works.
This is the command make help
runs:
awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $1, $2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($0, 5) } ' Makefile
Formatted for easier reading:
awk 'BEGIN {
FS = ":.*##";
printf "\nUsage:\n make \033[36m<target>\033[0m\n"
}
/^[a-zA-Z_0-9-]+:.*?##/ {
printf " \033[36m%-15s\033[0m %s\n", $1, $2
}
/^##@/ {
printf "\n\033[1m%s\033[0m\n", substr($0, 5)
} ' Makefile
If you don't already know, awk is a general-purpose tool for filtering data streams, like text files. Here, a single awk program takes our Makefile as input and outputs a beautiful help message.
The program is made of three pattern and action pairs that look like this:
pattern { action }
The first pattern is BEGIN
, a special pattern that runs its action before awk reads any data. The action has two parts:
FS = ":.*##";
printf "\nUsage:\n make \033[36m<target>\033[0m\n"
The first part sets the program's field separator (FS = Field Separator), which awk uses to split lines into multiple fields. This separator is used in the later pattern/action pairs.
The second part prints the first lines of the help message. The printf
command is like bash's echo
, with the added ability to inject values into strings, like C's printf
function. Here, we are simply printing this string:
Usage:
make \033[36m<target>\033[0m
This string includes special sequences called escape codes that your shell interprets to apply formatting to the text that follows. Without those escape codes, the string looks like this:
Usage:
make <target>
\\033
is an ASCII escape character that marks the beginning of a terminal code. Your terminal interprets the \\033[XXXm
sequence to mean "apply color modification XXX" and \\033[0m
to mean "end all color modifications".
The 36 in \\033[36m
is the color cyan. This webpage has a handy table of available colors.
So now we understand how the first printf
command in the awk program prints the top of our Makefile's beautiful help message:
Now let's look at the second pattern/action pair in the awk program:
/^[a-zA-Z_0-9-]+:.*?##/ {
printf " \033[36m%-15s\033[0m %s\n", $1, $2
}
The pattern, /^[a-zA-Z_0-9-]+:.*?##/
is a simple regular expression. It searches for lines that look like target_name:
or target_name: ## description
. When it finds a line that matches this pattern, it executes the printf
command inside the curly brackets.
The $1
and $2
variables are set by awk itself, once it has split the line's contents with the field separator (FS) set at the beginning. The $1
is set to target_name
and $2
is either set to description
or set to the empty string if the FS pattern did not appear anywhere in the line.
The string given to printf
has some escape codes that set text color just like in the earlier printf
command. It also does some aligning by using %-15s
instead of a simple %s
for the target name. The -
left-aligns the target name and the 15
adds whitespace so that the string is 15 characters long. This displays the Makefile's targets in beautiful, perfectly aligned columns:
Finally, let's look at the third and last pattern/action pair:
/^##@/ {
printf "\n\033[1m%s\033[0m\n", substr($0, 5)
}
The pattern matches any line that starts with ##@
, like ##@ Section name
. The action uses printf
again to print the section's name. The 1
in \\033[1m
means "write the following text in bold".
The built-in substr(s, p)
function returns the substring with s
starting at position
p. So substr($0, 5)
returns the entire line without the first 4 characters. So this action prints section names like this:
The awk program processes the Makefile's lines one by one, in order. For each line, it applies each pattern/action pair. If the pattern matches the line, it runs the action.
All together, this produces a beautiful self-documenting help message for your Makefile.