通过在 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的变量。
- 同样是因为 Make 的转义规则!在 awk 中表示第一列是
三、其他小细节
- 开头的
@符:
@grep...必须加@,否则在执行命令前,Make 会先把这串又臭又长的grep ... | awk ...命令本身打印到屏幕上,破坏排版美感。 - 末尾的反斜杠
\:
$(MAKEFILE_LIST) | \这里的\是为了把两行长命令在代码里换行显示,同时告诉 Make:“这其实是同一条 Shell 命令行,请在同一个子进程里执行”。
总结
这是一个非常优雅的自省(Introspection)设计:
以后你给项目增加任何新命令(比如 test: 或 lint:),只需要随手在目标行后面敲个 ## 你的说明,make help 就会自动收录并排版输出,再也不需要手动更新 README 了。