Skip to content

Instantly share code, notes, and snippets.

@prwhite
Last active October 2, 2024 03:13
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
@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!

@letrunghieu
Copy link

Hi @chrissv, you can repeat the target twice. Once for the help comment and the other one for the list of dependencies. With your example:

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

@chrissv
Copy link

chrissv commented Apr 4, 2024

Hi @chrissv, you can repeat the target twice. Once for the help comment and the other one for the list of dependencies. With your example:

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

That's a great suggestion, thanks!

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