Skip to content

Instantly share code, notes, and snippets.

@prwhite
Last active April 16, 2023 19:38
Embed
What would you like to do?
Add a help target to a Makefile that will allow all targets to be self documenting
# Add the following 'help' target to your Makefile
# And add help text after each target name starting with '\#\#'
help: ## Show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
# Everything below is an example
target00: ## This message will show up when typing 'make help'
@echo does nothing
target01: ## This message will also show up when typing 'make help'
@echo does something
# Remember that targets can have multiple entries (if your target specifications are very long, etc.)
target02: ## This message will show up too!!!
target02: target00 target01
@echo does even more
@o5
Copy link

o5 commented Feb 10, 2019

I'm using version from here, it works great but I would like to support also templates. I tried to modify [a-zA-Z_-] but I stuck :/

Makefile

.DEFAULT_GOAL:=help

##@ Build
.PHONY: build

build: ## build something
	@echo "Bulding..."

##@ Clean
.PHONY: clean

cache: ## clean something
	@echo "Cleaning..."

define template =
##@ Section $(2)

.PHONY: $(1)-foo
$(1)-foo: ## Foo $(2)
	@echo "Doing $(2)-foo..."

.PHONY: $(1)-bar
$(1)-bar: ## Bar $(2)
	@echo "Doing $(2)-bar..."
endef

$(eval $(call template,a,Apple))
$(eval $(call template,l,Linux))

.PHONY: help
help:
	@awk 'BEGIN {FS = ":.*##"; printf "Usage: make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

Current output

Usage: make <target>

Build
  build       build something

Clean
  cache       clean something

Section $(2)

Needed output

Usage: make <target>

Build
  build       build something

Clean
  cache       clean something

Section Apple
  a-foo       Foo Apple
  a-bar       Bar Apple

Section Linux
  l-foo       Foo Linux
  l-bar       Bar Linux

Is there someone who can help?
Thank you! :)

@amerlyq
Copy link

amerlyq commented Feb 11, 2019

@o5, simple regex for makefile eval is not enough, you must use recurrent makefile with --print-data-base to extract evaluated recipes.
See here: https://stackoverflow.com/questions/4219255/how-do-you-get-the-list-of-targets-in-a-makefile/26339924#26339924

@adepretis
Copy link

adepretis commented Feb 23, 2019

This gist totally "triggered" me and kickstarted all my gizmos quite some time back - thank you so much for it! Also thanks to @lordnynex and @HarasimowiczKamil for your comments. Sorry it took me some years to comment here but better later than never ;-)

I took the idea a bit further and puzzled together some other bits and pieces (e.g. including variables) - have a look at https://github.com/25th-floor/docker-make-stub ... Feedback is highly appreciated!

@mirageglobe
Copy link

mirageglobe commented Feb 27, 2019

First go for a single line version.. fully awk.. place this at the end of your make file. it will print all recipes.

## print help
help:
	@printf "\nusage : make <commands> \n\nthe following commands are available : \n\n"
	@cat Makefile | awk '1;/help:/{exit}' | awk '/##/ { print; getline; print; }' | awk '{ getline x; print x; }1' | awk '{ key=$$0; getline; print key "\t\t\t\t " $$0;}'
	@printf "\n"

to yield

usage : make <commands>

the following commands are available :

help:				 ## print help

some issues in removing the ":" and "##" as well as trying to get colours for the menu item. otherwise its quite usable. this way is much faster as it does not loop and try to modify each line.

@m000
Copy link

m000 commented Nov 29, 2019

You don't need this whole pipeline. One sed is enough:

help:           ##- Show this help.
	@sed -e '/#\{2\}-/!d; s/\\$$//; s/:[^#\t]*/:/; s/#\{2\}- *//' $(MAKEFILE_LIST)

Comments on this alternative:

  • sed can accept multiple commands delimited with ;.
  • sed can skip lines not matching a pattern with the /pattern/!d command. This saves us from having to use grep.
  • Lines consisting entirely of # are often used as horizontal rules in Makefiles. Using ##- as the delimiter prevents them from being printed.
  • s/:[^#\t]*/:/ is used to eliminate the need to add an additional entry for the target help message.
  • /#\{2\}-/!d is used instead of the equivalent /##-/!d so that the line containing the sed command doesn't match.

@lpsantil
Copy link

lpsantil commented Dec 18, 2019

Golfing here. One awk might be enough

.PHONY: help help_target-funky+names.0k and_with_2_targets_and_spaces_like_bison phony targets
help: ## Show this help.
help: phony targets ## Show this help here too.
        @awk '/^[a-zA-Z0-9\-_+. ]*:[a-zA-Z0-9\-_+. ]*#{2}/ { print; }' $(MAKEFILE_LIST)
help_target-funky+names.0k and_with_2_targets_and_spaces_like_bison: ## Show this help too.

@rcosnita
Copy link

rcosnita commented Mar 12, 2020

It works nicely. You might want to make a minor improvement:

@fgrep -h "##" $(MAKEFILE_LIST) | sed -e 's/\(\:.*\#\#\)/\:\ /' | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'

This will print only the targets without their dependencies.

all:  Runs the tests and builds the binary deliverable of the project using cython.
clean:  Cleans all the intermediate files and folders previously generated.
compile-proto:  Compiles the models protocol buffer files and gRPC stubs.
prereqs:  Installs all the prerequisites for this project.
test:  Runs all the available tests for this project.
cythonize:  Builds a binary deliverable using Cython.

@WarFox
Copy link

WarFox commented May 5, 2020

Single line with formating

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

Example from here https://stackoverflow.com/questions/35730218/how-to-automatically-generate-a-makefile-help-command

@muuvmuuv
Copy link

Can someone here help me get a awk working with this Makefile syntax?

##
## Section
##

## Start container
start:
  @echo "Start container"

@mbarkhau
Copy link

@muuvmuuv have a look if you can get something going here: https://awk.js.org/

@lpsantil
Copy link

lpsantil commented Aug 27, 2020

@muuvmuuv If you look at the link @WarFox posted (https://stackoverflow.com/questions/35730218/how-to-automatically-generate-a-makefile-help-command), there's an example there that might work for you.

.PHONY: help

#  Show this help
help:
     @awk '/^#/{c=substr($$0,3);next}c&&/^[[:alpha:]][[:alnum:]_-]+:/{print substr($$1,1,index($$1,":")),c}1{c=0}' $(MAKEFILE_LIST) | column -s: -t

Adjusted for the AWK web REPL - https://awk.js.org/?gist=238f6158b3673074ee92edc93eb84a91

... produces when using the Makefile as input

help: Show this help.

@eyanq
Copy link

eyanq commented Sep 16, 2020

I used the suggestion from @o5 with my makefile and it looks awesome to me

##@ Utility
help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

clean: ## Tidy up local environment
	find . -name \*.pyc -delete
	find . -name __pycache__ -delete

@jcwren
Copy link

jcwren commented Sep 16, 2020

I used the suggestion from @o5 with my makefile and it looks awesome to me

##@ Utility
help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

clean: ## Tidy up local environment
	find . -name \*.pyc -delete
	find . -name __pycache__ -delete

Small buglet. Need to add 0-9 to your [a-zA-Z_-] expression to capture targets with numbers in the name. And probably a space also, as I see it doesn't capture targets like foobar : ## This is a foobar test

@Xanders
Copy link

Xanders commented Dec 17, 2020

Is it crazy enough to show the Makefile help via Docker? This is exactly what I did. 😋 You're welcome:

# Show this help
help:
  @cat $(MAKEFILE_LIST) | docker run --rm -i xanders/make-help

Source: https://github.com/Xanders/make-help

Screenshot:

Screenshot with this project help output

@theherk
Copy link

theherk commented Feb 8, 2021

A slight improvement on a few preceding:

help: ## show help message
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

test: ## plain

$dollar: ## leading dollar

percent%: ## percent included

(paren): ## parenthesis

$(both): ## both

space : ## space before colon
➜ make

Usage:
  make 
  help             show help message
  test             plain
  $dollar          leading dollar
  percent%         percent included
  (paren)          parenthesis
  $(both)          both
  space            space before colon

note: Not shown here: the targets are colored.

@jcwren
Copy link

jcwren commented Feb 9, 2021

This updated expression still fails to find targets with numbers in the name. I think you need 0-9 in front of the a-zA-Z.

.PHONY: jflash
jflash: ## Program xxx using last J-Link specified (600112147 if none)
ifeq (,$(wildcard $(LAST_SEGGER_FILE)))
  @echo "600112147" > $(LAST_SEGGER_FILE)
endif
  $(JFLASH) -min -openprjsxxx_l`cat $(LAST_CPU_MODEL_FILE)`.jflash -open$(COMBINEDHEX) -usb`cat $(LAST_SEGGER_FILE)` -eliminate -auto -startapp -exit

.PHONY: jflash_147
jflash_147: ## Program xxx using J-Link 600112147
  echo "600112147" > $(LAST_SEGGER_FILE)
  $(JFLASH) -min -openprjsxxx_l`cat $(LAST_CPU_MODEL_FILE)`.jflash -open$(COMBINEDHEX) -usb`cat $(LAST_SEGGER_FILE)` -eliminate -auto -startapp -exit

.PHONY: jflash_977
jflash_977: ## Program xxx using J-Link 600108977
  echo "600108977" > $(LAST_SEGGER_FILE)
  $(JFLASH) -min -openprjxxx_l`cat $(LAST_CPU_MODEL_FILE)`.jflash -open$(COMBINEDHEX) -usb`cat $(LAST_SEGGER_FILE)` -eliminate -auto -startapp -exit

.PHONY: jflash_756
jflash_756: ## Program xxx using J-Link 600103756
  echo "600103756" > $(LAST_SEGGER_FILE)
  $(JFLASH) -min -openprjsxxx_l`cat $(LAST_CPU_MODEL_FILE)`.jflash -open$(COMBINEDHEX) -usb`cat $(LAST_SEGGER_FILE)` -eliminate -auto -startapp -exit

.PHONY: fix_crlf
fix_crlf: ## Convert all .c, .h, and .s files to Unix EOL, set 0x644 permissions
  chown -R $(USERNAME):$(GROUPNAME) *
  find . -type f -name \*.c -exec chmod 644 {} \;
  find . -type f -name \*.h -exec chmod 644 {} \;
  find . -type f -name \*.c -exec dos2unix -q {} \;
  find . -type f -name \*.h -exec dos2unix -q {} \;
  find . -type f -name \*.s -exec dos2unix -q {} \;
$ make help

Usage:
  make
  jflash           Program xxx using last J-Link specified (600112147 if none)
  fix_crlf         Convert all .c, .h, and .s files to Unix EOL, set 0x644 permissions

@nothub
Copy link

nothub commented Jun 18, 2021

This updated expression still fails to find targets with numbers in the name. I think you need 0-9 in front of the a-zA-Z.

Yeah, this works:

help: ## show help message
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
$ make help

Usage:
  make
  help             show help message
  jflash           Program xxx using last J-Link specified (600112147 if none)
  jflash_147       Program xxx using J-Link 600112147
  jflash_977       Program xxx using J-Link 600108977
  jflash_756       Program xxx using J-Link 600103756
  fix_crlf         Convert all .c, .h, and .s files to Unix EOL, set 0x644 permissions

@alexandregv
Copy link

alexandregv commented May 8, 2022

I had a case where my targets names contain semicolons (escaped with a backslash), to create "namespaces":

sw\:provision:  ## Provision machine "SW"
	@vagrant provision SW
$ make help

Usage:
  make 
  help              Show help message
  provision         Provision all machines
  s:provision       Provision machine "S"
  sw:provision      Provision machine "SW"

Here is the modified version to make it work:

help:  ## Show help message
	@awk 'BEGIN {FS = ": .*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+(\\:[$$()% 0-9a-zA-Z_-]+)*:.*?##/ { gsub(/\\:/,":", $$1); printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

And what I changed (based on the latest version just above):
image

@mathieu-aubin
Copy link

Here is how cargo-quickinstall does it in it's current Makefile

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

Simple and efficient, here is the result

Resulting_help_output

@maxixcom
Copy link

maxixcom commented Aug 6, 2022

Here is how cargo-quickinstall does it in it's current Makefile

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

Simple and efficient, here is the result

Resulting_help_output

If your makefile has something like:

-include .env

your proposal will display:

Makefile                       Display this help screen

@mathieu-aubin
Copy link

Here is how cargo-quickinstall does it in it's current Makefile

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

Simple and efficient, here is the result
Resulting_help_output

If your makefile has something like:

-include .env

your proposal will display:

Makefile                       Display this help screen

really?

But... assuming you write your own stuff and no try to retrofit someone else's code... why wouldnt you just use (per example....)

.INCLUDEDIRS : /usr/blah/blah
.INCLUDE : somefile
.INCLUDE .IGNORE : another_file /etc/yetanotherfile

Wouldn't NOT using the double ## hashtags make them be ignored by the grep rule?

Can you post an example so i can replicate? i guess its also good practice when making such affirmatiin... i mean, it helps everyone now and in the future be able to replicate and understand fast, too

Thanks!

@prwhite
Copy link
Author

prwhite commented Aug 9, 2022

Here is how cargo-quickinstall does it in it's current Makefile

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

Simple and efficient, here is the result
Resulting_help_output

If your makefile has something like:

-include .env

your proposal will display:

Makefile                       Display this help screen

Here's my fix for that:

.PHONY: help
help:	## Show this help.
	@grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

Added -h to hide the file name and tweaked the regex to include numbers and possible whitespace before the target's :, although it still doesn't cover all of the possible characters in a target identifier.

@nothub
Copy link

nothub commented Aug 14, 2022

To golf a little bit more, the grep expression of the previous post can be shrinked down to grep -hP '^[\w \-]*?:.*##.*$$' with the perl regex flag and \w that is an alias for [a-zA-Z0-9_].

@p-sherratt
Copy link

just to add another variant to the pile..

  • uses only sed for pre-processing the makefile
  • uses tput to detect if we should use colour
  • uses column for what it does best
help:
	@sed \
		-e '/^[a-zA-Z0-9_\-]*:.*##/!d' \
		-e 's/:.*##\s*/:/' \
		-e 's/^\(.\+\):\(.*\)/$(shell tput setaf 6)\1$(shell tput sgr0):\2/' \
		$(MAKEFILE_LIST) | column -c2 -t -s :

@arvenil
Copy link

arvenil commented Dec 1, 2022

What's the most cross-platform solution, without colors and which displays targets even if they don't have comment?

@mathieu-aubin
Copy link

mathieu-aubin commented Dec 7, 2022

@arvenil i might be wrong but i think that a makefile works using the same system, across all platforms? Please correct me if i am wrong, i am not very cross platform myself... As for color, i guess the terminal will be responsible for displaying it (or not) depending on the context. Many tools have the ability to force the suppression of color, too.

@lpsantil
Copy link

lpsantil commented Dec 7, 2022

@arvenil Depends on what you want to depend on? You have choices. awk, sed, grep, bash, sh, zsh, GNU Make, BSD Make, Linux, MacOS, BSD, WSL, RHEL/Fedora, Debian, Ubuntu, others. For widest cross compatibility, GNU Make and bash are fairly safe. But bash has been losing install base and mind share with MacOS and other smaller Linux distros changing their default shells.

@lpsantil
Copy link

lpsantil commented Dec 7, 2022

One other note, since GNU Make is the most popular, it has a fairly powerful and underutilized optional integration of GNU Guile. GNU Guile is language in the Scheme & Lisp family of languages.

If Guile is a bridge too far, then consider bash's BASH_REMATCH facility.

It'll be a bit more code (file I/O, regexes, file parsing, etc) and you'll have to maintain it rather than depending on other well tested tooling like awk, sed, grep.

@lpsantil
Copy link

lpsantil commented Dec 8, 2022

@arvenil A bit of golfing and I got this to sort of work with the only deps being GNU make and bash

SHELL:=/bin/bash
.PHONY: help help_target-funky+names.0k and_with_2_targets_and_spaces_like_bison
help_target-funky+names.0k and_with_2_targets_and_spaces_like_bison: ## Funky ones & bison dual target display ok
        echo "bad - why are you not displaying?"
help: ## bash help
help: ## moar bash help
        @RE='^[a-zA-Z0-9 ._+-]*:[a-zA-Z0-9 ._+-]*##' ; while read line ; do [[ "$$line" =~ $$RE ]] && echo "$$line" ; done <$(MAKEFILE_LIST) ; RE=''

Which for make help outputs

$ make help
help_target-funky+names.0k and_with_2_targets_and_spaces_like_bison: ## Funky ones & bison dual target display ok
help: ## bash help
help: ## moar bash help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment