Makefile help target

Wednesday 4 April 2018

In a pull request today, I was struck again by the difficulty of providing a “help” target for Makefiles. The make command doesn’t natively have a way to see what targets are available, because the set is dynamic and large, so we are left to cobble things together ourselves.

We’d been cargo-culting this target across Makefiles for a while:

help: ## display this help message
        @echo "Please use \`make <target>' where <target> is one of"
        @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m  %-25s\033[0m %s\n", $$1, $$2}'

Here’s the meaty line, split across lines for readability, as all the rest of the code in this post will be:

perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) |\
    sort |\
    awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m  %-25s\033[0m %s\n", $$1, $$2}'

It finds labelled lines with double-hash comments, and prints them, sorted, in a nice two-column layout, with the target names in cyan.

We’re a Python shop, so that Perl command really seemed out of place. What would it look like in Python? Longer, that’s what:

python -c 'import fileinput,re; \
    ms=filter(None, ("([a-zA-Z_-]+):.*?## (.*)$$",l) for l in fileinput.input())); \
    print("\n".join(sorted("\033[36m  {:25}\033[0m {}".format(*m.groups()) for m in ms)))' $(MAKEFILE_LIST)

But looking at that original line more, what is the Perl even doing? It’s just selecting lines. That’s what grep is for:

grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
    sort | \
    awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m  %-25s\033[0m %s\n", $$1, $$2}'

That’s shorter than the original, but we can do even better by using awk more effectively:

grep '^[a-zA-Z]' $(MAKEFILE_LIST) | \
    sort | \
    awk -F ':.*?## ' 'NF==2 {printf "\033[36m  %-25s\033[0m %s\n", $$1, $$2}'

The terminal coloring is cute, but unnecessary and can actually be counterproductive depending on your terminal’s natural colors, so:

grep '^[a-zA-Z]' $(MAKEFILE_LIST) | \
    sort | \
    awk -F ':.*?## ' 'NF==2 {printf "  %-26s%s\n", $$1, $$2}'

Looking around for other people’s techniques, marmelab had a very similar line, while Rodrigo Machado and O. Libre went down the all-awk path with fancier behavior.

In many ways, this doesn’t matter at all. But it’s a fun rabbit-hole...

» 6 reactions


awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf " %-26s%s\n", $$1, $$2}' | sort


(also, isn't the '?' redundant for .'*?'?)
(also also, what if your pretty target depends on something...)
@Karl, nice, get awk to do all the work. About the question mark: .* is greedy (matches as much as possible), .*? is not, matching as little as possible. It means that the first double-hash on the line will be found. And it's OK if the target depends on something, since the .*? will skip over all of it.
Ugh, I need to reup my regexp training :-)

grep | awk is something that tends to evolve organically on command lines and you can almost always make awk do all the work. I think awk should be able to do anything sed can, but sed expresses what it does much more elegantly.
I don't think awk groks *? as minimal matches. If it does, then you might want to check GNU awk and OS X awk separately, since they sometimes differ. One reason I've seen (and used) perl in one-liners like this is that it's consistent across platforms.

Separately, the original formula has bare $& which is interpolated by make as the empty string. It still works because perl's print, in the absence of an argument, prints $_ which is the current line of input.

Can't help myself from golfing a bit :-)

perl -ne's/^([-\w]+):.*?##\s*(.*)/printf" %-26s %s\n",$$1,$$2/e' $(MAKEFILE_LIST) | sort
FYI, in debian systems bash competion lists all targets using:
"make " TAB TAB

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.