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, (re.search("([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...
Comments
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
Add a comment: