Skip to content

Instantly share code, notes, and snippets.

@prwhite
Last active March 20, 2024 06:25
Show Gist options
  • Save prwhite/8168133 to your computer and use it in GitHub Desktop.
Save prwhite/8168133 to your computer and use it in GitHub Desktop.
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
@mbarkhau
Copy link

mbarkhau commented Nov 2, 2018

I'm now using the following based on the post of @hans-d. It doesn't have the grouping feature, but does have multi-line doc strings.

Features

  • only awk (works cross platform)
  • multi-line doc strings
  • doc strings aligned to right
  • section delimiters
.DEFAULT_GOAL := help

## -- Section Delimiter --

## This help message
## Which can also be multiline
.PHONY: help
help:
	@printf "Usage\n";

	@awk '{ \
			if ($$0 ~ /^.PHONY: [a-zA-Z\-\_0-9]+$$/) { \
				helpCommand = substr($$0, index($$0, ":") + 2); \
				if (helpMessage) { \
					printf "\033[36m%-20s\033[0m %s\n", \
						helpCommand, helpMessage; \
					helpMessage = ""; \
				} \
			} else if ($$0 ~ /^[a-zA-Z\-\_0-9.]+:/) { \
				helpCommand = substr($$0, 0, index($$0, ":")); \
				if (helpMessage) { \
					printf "\033[36m%-20s\033[0m %s\n", \
						helpCommand, helpMessage; \
					helpMessage = ""; \
				} \
			} else if ($$0 ~ /^##/) { \
				if (helpMessage) { \
					helpMessage = helpMessage"\n                     "substr($$0, 3); \
				} else { \
					helpMessage = substr($$0, 3); \
				} \
			} else { \
				if (helpMessage) { \
					print "\n                     "helpMessage"\n" \
				} \
				helpMessage = ""; \
			} \
		}' \
		$(MAKEFILE_LIST)

## -- QA Task Runners --

## Run linter
.PHONY: lint
lint:
	exit 0


## Run unit and integration tests
.PHONY: test
test:
	exit 0

Output

$ make help

                   -- Section Delimiter --

help               This help message
                   Which can also be multiline

                   -- QA Task Runners --

lint               Run linter
test               Run unit and integration tests

@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

@PenelopeFudd
Copy link

I thought I'd throw my hat into the ring:

## Usage: make <target>
##   This makefile generates a WhatzIt report, suitable for pasting into Slack.
##   It prints the report to the screen, and also puts it in the copy-paste buffer
##
## Possible Targets:

usage: ## Displays this message
        @gawk -vG=$$(tput setaf 2) -vR=$$(tput sgr0) ' \
          match($$0,"^(([^:]*[^ :]) *:)?([^#]*)##(.*)",a) { \
            if (a[2]!="") {printf "%s%-36s%s %s\n",G,a[2],R,a[4];next}\
            if (a[3]=="") {print a[4];next}\
            printf "\n%-36s %s\n","",a[4]\
          }\
        ' $(MAKEFILE_LIST)

example.txt : example.json ## Make the Example

Lines starting with ## are printed as-is (the text after the ##).
Lines starting with a target and ending with ## get the target printed in green, and the text after the ## printed starting in column 37.
A further refinement would be to send the output through column -t -s@ or similar, to align the columns efficiently.

Since it's the first target in the Makefile, it runs if make is run with no arguments. I hate playing "guess the usage message flag". 🎏 😃

I'll look into Guile, didn't know about it, and I thought I'd read the make docs enough times to have stumbled over it. 😃

@alhirzel
Copy link

alhirzel commented Sep 22, 2023

Here's an iteration on @PenelopeFudd's version with a few small improvements:

  • It can be added to the end of the Makefile because .DEFAULT_GOAL is used
  • It ignores commented targets
  • It ignores certain kinds of headers that can be useful to organize targets not mentioned in the help text
##
## These lines show up in the final output,
##including leading whitespace.
##
## They are useful for end-user documentation.
##

# Note that sections like the following are ignored in the help output; they are
# meant for developer documentation.

##
## generate target files
##

################################################################################
# File copies - easy stuff
################################################################################

target3.txt: target2.txt ## Generate target3.txt
	cp $< $@

target2.txt: target1.txt ## Generate target2.txt
	cp $< $@

################################################################################
# This one could be complicated or require further explanation to a developer.
################################################################################

target1.txt: # (this one is undocumented)
	touch $@

##
## other stuff
##

file4.txt: ## Generate file4.txt
	touch $@

##
## tools and help
##

lint: ## run linter

################################################################################
# Help target
################################################################################
help:: ## show this help text
	@gawk -vG=$$(tput setaf 2) -vR=$$(tput sgr0) ' \
	  match($$0, "^(([^#:]*[^ :]) *:)?([^#]*)##([^#].+|)$$",a) { \
	    if (a[2] != "") { printf "    make %s%-18s%s %s\n", G, a[2], R, a[4]; next }\
	    if (a[3] == "") { print a[4]; next }\
	    printf "\n%-36s %s\n","",a[4]\
	  }' $(MAKEFILE_LIST)
	@echo -e "" # blank line at the end
.DEFAULT_GOAL := help

image

@BlackHole1
Copy link

##@
##@ Clean build files commands
##@

kernel-%-clean: ##@ Clean kernel build files with specified architecture
                ##@ e.g. kernel-amd64-clean / kernel-arm64-clean
	$(MAKE) -C ./arch/kernel/$* clean

rootfs-%-clean: ##@ Clean rootfs build files with specified architecture
                ##@ e.g. rootfs-amd64-clean / rootfs-arm64-clean
	$(MAKE) -C ./arch/rootfs/$* clean

clean: ##@ Clean all build files
	$(MAKE) kernel-amd64-clean
	$(MAKE) kernel-arm64-clean
	$(MAKE) rootfs-amd64-clean
	$(MAKE) rootfs-arm64-clean

##@
##@ Misc commands
##@

help: ##@ (Default) Print listing of key targets with their descriptions
	@printf "\nUsage: make <command>\n"
	@grep -F -h "##@" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' | awk 'BEGIN {FS = ":*[[:space:]]*##@[[:space:]]*"}; \
	{ \
		if($$2 == "") \
			pass; \
		else if($$0 ~ /^#/) \
			printf "\n%s\n", $$2; \
		else if($$1 == "") \
			printf "     %-20s%s\n", "", $$2; \
		else \
			printf "\n    \033[34m%-20s\033[0m %s\n", $$1, $$2; \
	}'

21-09 18 21@2x

@Windowsfreak
Copy link

I tried @BlackHole1 's solution and got this :(

Usage: make <command>
awk: illegal statement
 input record number 1, file 
 source line number 1```

@BlackHole1
Copy link

BlackHole1 commented Oct 23, 2023

I tried @BlackHole1 's solution and got this :(

Usage: make <command>
awk: illegal statement
 input record number 1, file 
 source line number 1```

@Windowsfreak Can you share the contents of your makefile? It works fine on my local machine.

@BlackHole1
Copy link

@Windowsfreak I just reproduced this issue on macOS. you need to install gawk first (brew install gawk).

AWK := awk
ifeq ($(shell uname -s), Darwin)
	AWK = gawk
    ifeq (, $(shell which gawk 2> /dev/null))
        $(error "gawk not found")
    endif
endif
-	@grep -F -h "##@" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' | awk 'BEGIN {FS = ":*[[:space:]]*##@[[:space:]]*"}; \
+ 	@grep -F -h "##@" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' | $(AWK) 'BEGIN {FS = ":*[[:space:]]*##@[[:space:]]*"}; \

Full Code:

AWK := awk
ifeq ($(shell uname -s), Darwin)
	AWK = gawk
    ifeq (, $(shell which gawk 2> /dev/null))
        $(error "gawk not found")
    endif
endif

##@
##@ Clean build files commands
##@

kernel-%-clean: ##@ Clean kernel build files with specified architecture
                ##@ e.g. kernel-amd64-clean / kernel-arm64-clean
	$(MAKE) -C ./arch/kernel/$* clean

rootfs-%-clean: ##@ Clean rootfs build files with specified architecture
                ##@ e.g. rootfs-amd64-clean / rootfs-arm64-clean
	$(MAKE) -C ./arch/rootfs/$* clean

clean: ##@ Clean all build files
	$(MAKE) kernel-amd64-clean
	$(MAKE) kernel-arm64-clean
	$(MAKE) rootfs-amd64-clean
	$(MAKE) rootfs-arm64-clean

##@
##@ Misc commands
##@

help: ##@ (Default) Print listing of key targets with their descriptions
	@printf "\nUsage: make <command>\n"
	@grep -F -h "##@" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' | $(AWK) 'BEGIN {FS = ":*[[:space:]]*##@[[:space:]]*"}; \
	{ \
		if($$2 == "") \
			pass; \
		else if($$0 ~ /^#/) \
			printf "\n%s\n", $$2; \
		else if($$1 == "") \
			printf "     %-20s%s\n", "", $$2; \
		else \
			printf "\n    \033[34m%-20s\033[0m %s\n", $$1, $$2; \
	}'

@jaymecd
Copy link

jaymecd commented Oct 30, 2023

@Windowsfreak @BlackHole1 no need for gawk, just replace pass with empty printf and problem solved. BSD awk does not know pass command.

		if($$2 == "") \
-			pass; \
+			printf ""; \
$ PATH="/usr/bin:/usr/sbin:/bin:/sbin" make help

Usage: make <command>

Clean build files commands

    kernel-%-clean       Clean kernel build files with specified architecture
                         e.g. kernel-amd64-clean / kernel-arm64-clean

    rootfs-%-clean       Clean rootfs build files with specified architecture
                         e.g. rootfs-amd64-clean / rootfs-arm64-clean

    clean                Clean all build files

Misc commands

    help                 (Default) Print listing of key targets with their descriptions

@kjellericson
Copy link

Many good suggestions.
I felt the help syntax a bit ugly when having several target dependancies.
99% of my targets are non-files, and therefor PHONY targets. So I put the check on the PHONY targets only.

This doesn't fit everyone, but maybe gives some one some thougths.

PHONY: help ## Show this help.
help:
	@grep -he '^PHONY:.*##' $(MAKEFILE_LIST) | sed -e 's/ *##/:\t/' | sed -e 's/^PHONY: *//'

I think writing a "makefile-helper <makefile_list>" in perl will be my next approach. I got many Makefiles, and copy/paste any advanced script into each Makefile is just bad.

@adepretis
Copy link

@kjellericson you could use include /absolute/or/relative/path/to/*.mk instead of embedding it into each Makefile directly.

@chrissv
Copy link

chrissv commented Feb 28, 2024

Hi, just discovered this gist/conversation from a google search. The solution by @BlackHole1 (Oct 26, 2023) meets my needs 90%.
But in some of our makefiles we have a dependency after the target, like this:

app: $(APP_FILE2).exe $(APP_FILE2).exe ##@ Build the applications

The items after the ":" are displayed in the help, and I really don't want this.

I am not conversant enough in awk/gawk to figure out how to suppress the display of the items after the ":"
Can anyone give me advice?

Thanks!

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