Makefile ToC

Sadly, despite all the modern build systems available, we still end up using make and writing Makefiles by hand.

I needed to put together a help screen for the build targets in a project’s current Makefile. Out of the box, the ancient utility doesn’t give you a list of build targets and their descriptions — it was never meant to.

After looking at the existing solutions and confirming that most of them don’t work on macOS because of its BSD heritage, I decided to come up with my own approach for generating a table of contents.

So we have two problems to solve:

The first problem has an easy, cross-system solution. Just set .DEFAULT_GOAL at the very top of the Makefile:

.DEFAULT_GOAL := help

Now calling make without arguments will invoke the help target by default:

.DEFAULT_GOAL := help

.PHONY: help
help:
	@echo This is help target

The second problem turned out to be a bit trickier. The job is to gather target names along with their descriptions and lay them out in two clean columns.

Looking at examples, I settled on the following format for declaring build targets:

.PHONY: foo
foo: ## do some foo
	@echo foo

Here, the comment after the ## will be printed as the help text. Target name on the left, description on the right — everything’s in one place and easy to read.

Now we need to make make help actually do something — generate the help text dynamically.

Here’s my solution:

.PHONY: help
help: ## shows this help message
	@grep --fixed-strings --no-filename "##" $(MAKEFILE_LIST) \
		| grep -v 'grep --fixed-strings' \
		| sed -e 's/:/	/' \
		| awk 'BEGIN { FS="#"; } { print $$1 $$3 }' \
		| column -t -s $$'\t'

What’s going on here? Pretty straightforward:

Take a look at a demo Makefile:

.DEFAULT_GOAL := help

.PHONY: help
help: ## shows this help message
	@grep --fixed-strings --no-filename "##" $(MAKEFILE_LIST) \
		| grep -v 'grep --fixed-strings' \
		| sed -e 's/:/	/' \
		| awk 'BEGIN { FS="#"; } { print $$1 $$3 }' \
		| column -t -s $$'\t'

.PHONY: foo
foo: ## do some foo
	@echo foo

.PHONY: bar
bar: ## do some bar
	@echo bar

.PHONY: some-long-named-makefile-target
some-long-named-makefile-target: ## this is some-long-named-makefile-target
	@echo some-long-named-makefile-target

.PHONY: ignored
ignored:
	@echo ignored

And here’s the output of make with no arguments:

$ make
help                               shows this help message
foo                                do some foo
bar                                do some bar
some-long-named-makefile-target    this is some-long-named-makefile-target

Pretty clean, isn’t it?

That said, this solution is far from the most portable across different versions of make and operating systems. So if you have the option, try to avoid outdated build systems.