Skip to content

Instantly share code, notes, and snippets.

@ruittenb
Last active April 12, 2023 13:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ruittenb/5d2d281237385276f49652b9b9f6d5a1 to your computer and use it in GitHub Desktop.
Save ruittenb/5d2d281237385276f49652b9b9f6d5a1 to your computer and use it in GitHub Desktop.
Makefile target for automatically generating help
############################################################################
#
# makefile-autohelp
#
# This file is at : https://tinyurl.com/makefile-autohelp
# also known as : https://gist.github.com/ruittenb/5d2d281237385276f49652b9b9f6d5a1
############################################################################
# DESCRIPTION
#
# This code defines a 'help' target to add to your Makefile, that can auto-
# matically create a list of your Makefile's targets, with short descriptions.
#
# This has been tested on (and compatible with):
# - GNU make 3.81
# - GNU awk 5.1.1
# - MacOS awk version 20200816
#
############################################################################
# INSTALLATION
#
# Copy this block over to your own Makefile:
.DEFAULT_GOAL:=help
.PHONY: help # See https://tinyurl.com/makefile-autohelp
help: ## Print help for each target
@awk -v tab=12 'BEGIN{FS="(:.*## |##@ |@## )";c="\033[36m";m="\033[0m";y=" ";a=2;h()}function t(s){gsub(/[ \t]+$$/,"",s);gsub(/^[ \t]+/,"",s);return s}function u(g,d){split(t(g),f," ");for(j in f)printf"%s%s%-"tab"s%s%s\n",y,c,t(f[j]),m,d}function h(){printf"\nUsage:\n%smake %s<target>%s\n\nRecognized targets:\n",y,c,m}/\\$$/{gsub(/\\$$/,"");b=b$$0;next}b{$$0=b$$0;b=""}/^[-a-zA-Z0-9*\/%_. ]+:.*## /{p=sprintf("\n%"(tab+a)"s"y,"");gsub(/\\n/,p);if($$1~/%/&&$$2~/^%:/){n=split($$2,q,/%:|:% */);for(i=2;i<n;i+=2){g=$$1;sub(/%/,q[i],g);u(g,q[i+1])}}else if($$1~/%/&&$$2~/%:[^%]+:[^%]+:%/){d=$$2;sub(/^.*%:/,"",d);sub(/:%.*/,"",d);n=split(d,q,/:/);for(i=1;i<=n;i++){g=$$1;d=$$2;sub(/%/,q[i],g);sub(/%:[^%]+:%/,q[i],d);u(g,d)}}else u($$1,$$2)}/^##@ /{gsub(/\\n/,"\n");if(NF==3)tab=$$2;printf"\n%s\n",$$NF}END{print""}' $(MAKEFILE_LIST) # v1.62
############################################################################
# USAGE
#
# The awk option '-v tab=12' can be changed to change the initial indentation of the output.
#
# Descriptions for each target should be added in the Makefile itself:
#
# '##' after a target is used for target descriptions;
# '##@' at the start of a line is used for header lines.
############################################################################
# EXAMPLES
#
# --------------------------------------------------------------------------
##@ Examples of heading text:
##@ This is heading text, which is shown on a separate line.
##@ Heading text may \
span multiple lines.
##@ Heading text may contain\nnewlines by specifying\nthese with backslash-n
# This is just any comment and its continuation. \
It will not be parsed by 'make help'.
# --------------------------------------------------------------------------
##@ Examples of targets:
show1: ## Description for show1, no prerequisites
show2: prereq1 prereq2 ## Description for show2, with compact prerequisites
show3: prereq1 \
prereq2 ## Description for show3, with prerequisites spanning lines
show4: ## Description for show4 \
and its continuation, no prerequisites
show5: prereq1 prereq2 ## Description for show5 \
and its continuation, with compact prerequisites
show6: prereq1 \
prereq2 ## Description for show6 \
and its continuation, with prerequisites spanning lines
double: prereq ## This will not be shown ## Sequential comments only show the latter
newline: ## Comments may\ncontain newlines,\nspecified with backslash-n
# --------------------------------------------------------------------------
# Multiple targets:
target1 target2: ## Multiple targets will be split across lines
goal1 \
goal2: ## Multiple targets may span lines
# --------------------------------------------------------------------------
##@ 16 @## Heading text may change the indentation of the next block.
# --------------------------------------------------------------------------
##@ Examples of targets with wildcards:
target-%: ## %:one:% Wildcard targets may be... %:two:% ...specified with individual descriptions
copy-%: ## %:here:% Copy things hither %:there:% Copy things thither
test-%: ## Test the %:production:staging:% environment
# --------------------------------------------------------------------------
##@ Nothing should be printed from the lines below:
noshow1: # This is just any comment, which will not be parsed by 'make help'
noshow2: prereq1 prereq2 # This is just any comment, which will not be parsed by 'make help'
noshow3: prereq1 \
prereq2 # This is just any comment, which will not be parsed by 'make help'
noshow4: # This is just any comment and its continuation. \
It will not be parsed by 'make help'.
noshow5: prereq1 prereq2 # This is just any comment and its continuation. \
It will not be parsed by 'make help'.
noshow6: prereq1 \
prereq2 # This is just any comment and its continuation. \
It will not be parsed by 'make help'.
############################################################################
#
# Readable version of code:
#
# @awk -v tab=12 '
# BEGIN {
# FS = "(:.*## |##@ |@## )";
# buffer = "";
# color = "\033[36m";
# nocolor = "\033[0m";
# indent = " ";
# hang = 2;
# header();
# }
# function trim(str) {
# gsub(/[ \t]+$/, "", str);
# gsub(/^[ \t]+/, "", str);
# return str;
# }
# function spout(target, desc) {
# split(trim(target), fields, " ");
# for (j in fields) printf "%s%s%-" tab "s%s%s\n", indent, color, trim(fields[j]), nocolor, desc;
# }
# function header() {
# printf "\nUsage:\n%smake %s<target>%s\n\nRecognized targets:\n", indent, color, nocolor;
# }
# /\\$/ {
# gsub(/\\$/, "");
# buffer = buffer $0;
# next;
# }
# buffer {
# $0 = buffer $0;
# buffer = "";
# }
# /^[-a-zA-Z0-9*\/%_. ]+:.*## / {
# pad = sprintf("\n%" (tab + hang) "s" indent, "");
# gsub(/\\n/, pad);
# if ($1 ~ /%/ && $2 ~ /^%:/) {
# n = split($2, parts, /%:|:% */);
# for (i = 2; i < n; i += 2) {
# target = $1;
# sub(/%/, parts[i], target);
# spout(target, parts[i + 1]);
# }
# } else if ($1 ~ /%/ && $2 ~ /%:[^%]+:[^%]+:%/) {
# desc = $2;
# sub(/^.*%:/, "", desc);
# sub(/:%.*/, "", desc);
# n = split(desc, parts, /:/);
# for (i = 1; i <= n; i++) {
# target = $1;
# desc = $2;
# sub(/%/, parts[i], target);
# sub(/%:[^%]+:%/, parts[i], desc);
# spout(target, desc);
# }
# } else spout($1, $2);
# }
# /^##@ / {
# gsub(/\\n/, "\n");
# if (NF == 3) tab = $2;
# printf "\n%s\n", $NF;
# }
# END {
# print "";
# }
# ' $(MAKEFILE_LIST) # v1.62
#!/usr/bin/env perl
#
# This script may be used to generate a minimized 'help' recipe
# from the readable version of the code in the Makefile.
#
# Just pipe the entire Makefile through this script.
$_ = join "", map { chomp; s/^# *//; $_ } grep { /^# \@awk/ .. 0 } <>;
# remove whitespace around operators, statements and blocks
s/ ([+=<>~]|[-+<>=]=|!~|&&|\|\|) /$1/g;
s/([;{}(),]) (?!#)/$1/g;
s/ ([{}()])(?!")/$1/g;
s/\$(?!\()/\$\$/g;
s/;}/}/g;
# shorten variable names
s/\bhang\b/a/g;
s/\bbuffer\b/b/g;
s/\bcolor\b/c/g;
s/\bdesc\b/d/g;
s/\bfields\b/f/g;
s/\btarget\b(?!>)/g/g;
s/\bheader\b/h/g;
s/\bnocolor\b/m/g;
s/\bpad\b/p/g;
s/\bparts\b/q/g;
s/\bstr\b/s/g;
s/\btrim\b/t/g;
s/\bspout\b/u/g;
s/\bindent\b/y/g;
# leftover corrections
s/(printf?) /$1/g;
s/ (tab) /$1/g;
s/" y/"y/g;
s/b \$/b\$/g;
s/(BEGIN{[^{}]+;)b="";/$1/g;
print;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment