How to generate the help output of a Makefile from comments, so you never have to update the help text by hand.
Story
After a while, a project's Makefile has many targets and nobody remembers all of them (including me).
The naive solution is a help target that prints a hard-coded list with echo. It works for a week, then someone adds a target and forgets to update the list.
A better solution is to write the description next to the target as a comment, and let help generate the list from the Makefile itself. I use this in resume-cli.
Result
$ make
Usage: make <target>
Targets:
help List available commands and their usages
prepare Install golangci-lint, goimports, lefthook git hooks, git-chglog, and changie
dependency Download Go dependencies
test Run all tests
coverage Run all tests with coverage
lint Run golangci-lint
build Build resume-cli binary to bin/resume-cli
install Install resume-cli binary to GOPATH/binSolution
Add a ## description comment after each target you want to show in the help:
.DEFAULT_GOAL := help
help: ## List available commands and their usages
@echo "Usage: make <target>"
@echo ""
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "} {printf " %-12s %s\n", $$1, $$2}'
dependency: ## Download Go dependencies
@go get -v ./...
test: dependency ## Run all tests
@go test ./...
lint: ## Run golangci-lint
@golangci-lint run ./...
build: ## Build resume-cli binary to bin/resume-cli
@go build -o bin/resume-cli .
.PHONY: help dependency test lint buildNow make help (or just make) prints all targets that have a ## comment. A new target appears in the help as soon as you add the comment.
How it works
.DEFAULT_GOAL := help
When you run make without a target, Make runs the first target in the file. .DEFAULT_GOAL makes the default explicit, so help can be placed anywhere, and running make by accident shows the help instead of building something.
$(MAKEFILE_LIST)
A built-in variable which contains the list of Makefiles that Make has read, including the files loaded by include. Using it instead of a hard-coded Makefile means the help also lists targets of included files, and works if you run make -f build.mk.
grep -E '^[a-zA-Z_-]+:.*?## '
Select lines which:
- start with a target name (
^[a-zA-Z_-]+), - followed by a colon, optionally the prerequisites (
:.*?), sotest: dependency ## Run all testsis matched, - and contain
##(two hashes and a space).
Lines like # a normal comment or targets without ## are ignored, that is how you hide internal targets.
awk 'BEGIN {FS = ":.*?## "} ...'
awk splits each line using the regex :.*?## as the field separator, so for the line
test: dependency ## Run all tests$1 is test and $2 is Run all tests. Then printf " %-12s %s\n" prints the name padded to 12 characters, followed by the description.
$$1 and $$2
In a Makefile, $ starts a Make variable, so a literal $ must be escaped as $$. Make turns $$1 into $1 before passing it to the shell, and the shell passes it to awk because it is inside single quotes.
The @ prefix
By default Make prints every command before running it. @ hides the command, so only the output is displayed.
Improvements
The version above is what I use in resume-cli, and it has some limitations. I tested the following on macOS (GNU Make 3.81).
Targets with digits in the name
The character class [a-zA-Z_-] has no digits, so targets like build2 or test-e2e are silently ignored. Add 0-9 and a dot:
'^[a-zA-Z0-9_.-]+:.*?## 'Long target names
%-12s only pads names up to 12 characters. A longer name pushes its description to the right and breaks the alignment:
build Build the binary
generate_changelog Generate changelog for the current versionIncrease the width (for example %-20s), or pick a width from the longest target in your Makefile.
Colors and sorting
Add color for the target name with ANSI escape codes, and use sort if you want the targets in alphabetical order instead of the file order:
help: ## Show this help
@echo "Usage: make <target>"
@echo ""
@grep -hE '^[a-zA-Z0-9_.-]+:.*?## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "} {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'grep -h removes the file name prefix. It is only needed when $(MAKEFILE_LIST) has more than one file, but it is harmless otherwise.
Notes
- Do not forget the
.PHONYdeclaration for targets which are not files, includinghelp. - Targets with the same name defined in multiple included files will be listed more than once.
- The regex is intentionally simple: it does not support targets defined with variables (
$(BIN):), pattern rules (%.o:) or multiple targets in one line (a b: ## ...). Write a small wrapper target for them if you want them in the help. - It only needs
grepandawk, which are available by default on macOS and Linux. It is written for GNU Make.



