Self-Documented Makefile

François Zaninotto
François ZaninottoFebruary 29, 2016
#popular#shell#tutorial

All our projects contain a lengthy Makefile, to automate installation, build, test, and deployment. Most of the target names are standardized (make install, make deploy), but some deserve explanation (make run-dev, make restart-api). The more we add fine-grained make targets, the more we need to describe what they do in text form. In our projects, we usually write this doc in the README file:

But when using the CLI, we prefer self-documenting tools. Wouldn't it better if we could just type make, and get a list of available commands, together with their desciption?

It turns out it's quite easy to do. First, document each target using a comment placed after the target name, and starting with ##, as follows:

install: ## Install npm dependencies for the api, admin, and frontend apps
	@echo "Installing Node dependencies"
	@npm install

install-dev: install ## Install dependencies and prepared development configuration
	@./node_modules/.bin/selenium-standalone install
	@cp -n ./config/development.js-dist ./config/development.js | true

run-frontend-dev: webpack.PID ## Run the frontend and admin apps in dev (using webpack-dev-server)

webpack.PID:
	@./node_modules/.bin/babel-node ./node_modules/.bin/webpack-dev-server \
		--content-base=build \
		--devtool=cheap-module-inline-source-map \
		--hot \
		--inline \
		--progress \
		& echo "$$!" > webpack.PID

stop-frontend-dev: webpack.PID ## Stop the frontend and admin apps in dev
	@kill `cat $<` && rm $<
	@echo "Webpack server stopped"

restart-frontend: ## Restart the frontend and admin apps in dev
		@make stop-frontend-dev && make run-frontend-dev
		@echo "Frontend app restarted"

Internal targets (like the webpack.PID in our example) don't need description, and therefore don't appear in the self-documentation. Next, add a help target in your makefile, doing some shell scripting voodoo:

.PHONY: help

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

If you copy this code snippet to a makefile, make sure your text editor converts indentation to tabs and not spaces.

Adjust the width of the first column by changing the 30 value in the printf pattern to something larger or smaller.

Remove the | sort to have targets ordered the way they appear in the makefile instead of alphabetically.

This glorious piece of code was written by our collaborator Brice, inspired by various snippets found on the Internet. It uses ANSI codes to color the output, and works both on Linux and OS X.

Final touch: make this help target the default target:

.DEFAULT_GOAL := help

And you're good to go. make is one of the oldest CLI task launchers, has plenty of documentation, is extremely powerful, and is installed everywhere. With that trick, it can even replace language-specific task launchers (like npm or php bin/console) that provide inline help, but that require installation. By the way, if you like make tricks, check out that other article of ours where we combine make and docker to run commands in containers: make-docker-commands.

Update: This article was featured on the Hacker News home page, so part of the discussion continues there: https://news.ycombinator.com/item?id=11195539.

Did you like this article? Share it!