让 Makefile 会自述:用 ## 注释、grep+awk 自动生成彩色帮助菜单

通过在 Makefile 目标后用“## 说明”做约定,并结合 grep + awk(利用 MAKEFILE_LIST、$$ 转义与 ANSI 颜色码)自动提取、对齐并着色输出,一个无需维护的自文档化帮助菜单就能让项目命令一键可读,极大降低维护成本并提升开发体验。


这段脚本是现代 Makefile(尤其在 Go、Rust 和云原生开源项目)中极其流行的 “自文档化帮助菜单”(Self-Documenting Help) 黑科技。

它的核心思想是:不需要额外维护一份帮助文档,而是让 Makefile 变成一个文本输入源,自己用 Shell 工具(grep + awk)读取自己的源代码,自动排版并打印出带颜色的命令清单。


一、最终运行效果

当你在终端输入 make help 时,控制台会输出排版整齐、带颜色的菜单:

help            查看所有支持的命令  
build           编译主程序  
deploy          部署到生产环境  

(注:左侧的目标名默认会显示为高亮的青色/蓝绿色,右侧为普通白色文本,且左列自动对齐为 15 个字符宽。)


二、逐句核心拆解

整个流程分为两部分:约定语法 与 管道处理流水线(grep | awk)。

1. 前置约定:暗号 ##

help: ## 查看所有支持的命令  
build: ## 编译主程序  
deploy: ## 部署到生产环境  
  • 普通的 Makefile 注释是 #。
  • 这里约定用 ## 作为“帮助说明”的专用暗号。这样后续的文本处理工具可以很容易把“普通注释代码”和“需要展示给用户的帮助说明”区分开。

2. 第一关:grep 负责精准筛选行

grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST)  
  • $(MAKEFILE_LIST):这是 Make 内置变量,此时里面存放的就是当前这个 Makefile 文件的路径。等于把当前文件自己传给 grep 当作输入。
  • -E:启用扩展正则表达式(Extended Regular Expression)。
  • 正则表达式分解:
    • ^:行首。
    • [a-zA-Z_-]+:匹配由字母、下划线、减号组成的目标名(如 help、build、deploy)。
    • ::匹配目标后面的冒号。
    • .*?:非贪婪匹配冒号和 ## 之间可能存在的任何依赖项。
    • ## :匹配前面约定的帮助暗号及一个空格。
    • .*:匹配 ## 后面的所有说明文字。
    • $$:避坑细节! 在 Makefile 中,单个 $ 会被 Make 当作自身变量的起始符。要想给终端传递一个原生的正则表达式行尾符 $,必须写成 $$ 进行转义。

经过 grep 过滤后,输入的内容变成了纯净的几行:

help: ## 查看所有支持的命令  
build: ## 编译主程序  
deploy: ## 部署到生产环境  

3. 第二关:awk 负责切割、上色与排版

awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'  

过滤后的文本通过管道 | 喂给了文本处理神器 awk:

  • BEGIN {FS = ":.*?## "}:
    • FS 即 Field Separator(列分隔符)。
    • 告诉 awk:“以 :.*?## 为界线,把每一行切成两半!”
    • 切割后:
      • 第一列($1):目标名称(如 build)
      • 第二列($2):说明文字(如 编译主程序)
  • printf "...":格式化输出,这里运用了 ANSI 控制码 为终端着色:
    • \033[36m:终端控制码,把后面的文字颜色变成青色(Cyan)。
    • %-15s:左对齐字符串,固定占 15 个字符宽(不够 15 字符的用空格补齐,这就是列对齐的秘密)。
    • \033[0m:重置控制码,把终端颜色还原回默认白色(防止后面的说明文字也变色)。
    • %s\n:打印第二列说明文字,并换行。
  • 为什么写的是 $$1, $$2 而不是 $1, $2?
    • 同样是因为 Make 的转义规则!在 awk 中表示第一列是 $1,但如果写在 Makefile 里,必须写成 $$1,否则 Make 会以为你想获取 Make 自己的名为 1 的变量。

三、其他小细节

  1. 开头的 @ 符:
    @grep... 必须加 @,否则在执行命令前,Make 会先把这串又臭又长的 grep ... | awk ... 命令本身打印到屏幕上,破坏排版美感。
  2. 末尾的反斜杠 \:
    $(MAKEFILE_LIST) | \ 这里的 \ 是为了把两行长命令在代码里换行显示,同时告诉 Make:“这其实是同一条 Shell 命令行,请在同一个子进程里执行”。

总结

这是一个非常优雅的自省(Introspection)设计:
以后你给项目增加任何新命令(比如 test: 或 lint:),只需要随手在目标行后面敲个 ## 你的说明,make help 就会自动收录并排版输出,再也不需要手动更新 README 了。