- 开发工具
- CLI
- 代码生成
【免费下载链接】cookiecutter
A cross-platform command-line utility that creates projects from cookiecutters (project templates), e.g. Python package projects, C projects.
本文围绕 Cookiecutter 的 Hooks(钩子)机制展开,系统讲解pre_prompt、pre_gen_project、post_gen_project三类钩子的执行时机、工作目录、模板变量支持与失败处理语义,并结合本仓库源码(cookiecutter/hooks.py、cookiecutter/main.py、cookiecutter/generate.py)与测试用例(tests/test_hooks.py、tests/test_pre_prompt_hooks.py)剖析底层实现。读完本文,你将能够为模板编写可跨平台的 Python 钩子,实现输入校验、前置检查、条件化文件清理等实战能力。
一、什么是 Cookiecutter Hooks
Cookiecutter 的 Hooks 是在项目生成流程的特定阶段自动执行的脚本,支持 Python 脚本与 Shell 脚本两种形态。它们的存在是为了让模板作者在"用户输入之前""模板渲染之前""项目生成之后"三个关键节点插入自定义逻辑,从而完成数据校验、预处理、后处理等自动化任务,例如:
- 在提示用户输入之前,检查 Docker、Git 等前置依赖是否就绪;
- 在生成文件之前,校验用户提供的变量是否合法(如模块名是否符合 Python 命名规范);
- 在项目生成之后,按用户选择的条件化删除多余文件,或执行初始化设置(如
git init、安装依赖)。
Hooks 不参与模板渲染本身,它们是模板的"伴生脚本",存放在模板根目录的hooks/文件夹中,随模板一起分发。
二、三类 Hook 及其特性总览
| Hook | 执行时机 | 工作目录 | 模板变量(Jinja 渲染) | 引入版本 |
|---|---|---|---|---|
pre_prompt | 渲染任何提问之前 | 仓库目录的一份副本的根目录 | 不支持 | 2.4.0 |
pre_gen_project | 提问之后、模板处理之前 | 生成项目的根目录 | 支持 | 0.7.0 |
post_gen_project | 项目生成完成之后 | 生成项目的根目录 | 支持 | 0.7.0 |
上表中的三个钩子名称、执行时机与版本信息与官方文档一致,并在源码中得到了印证:cookiecutter/hooks.py的_HOOKS列表定义了唯一合法的三个钩子名:
_HOOKS = [ 'pre_prompt', 'pre_gen_project', 'post_gen_project', ]pre_prompt于 2.4.0 版本引入,见 CHANGELOG/2.4.0.md("Implement a pre_prompt hook that will run before prompts");pre_gen_project与post_gen_project自 0.7.0 起即存在。
版本适用前提:pre_prompt钩子仅在 Cookiecutter 2.4.0 及以上版本可用;若需兼容旧版本,应仅使用pre_gen_project与post_gen_project。
三、创建 Hook:目录结构与命名规范
Hooks 必须放在模板的hooks/文件夹中,文件名必须是三类钩子名之一(如pre_prompt.py、pre_gen_project.sh),不带扩展名的部分与钩子名完全一致才会被识别。
Python 钩子结构:
cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ ├── hooks │ ├── pre_prompt.py │ ├── pre_gen_project.py │ └── post_gen_project.py └── cookiecutter.jsonShell 脚本结构:
cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ ├── hooks │ ├── pre_prompt.sh │ ├── pre_gen_project.sh │ └── post_gen_project.sh └── cookiecutter.json推荐使用 Python 脚本,因为它天然跨平台(Windows / macOS / Linux 通用)。Shell 脚本或 Windows 的.bat文件可用于平台特定的模板场景——测试目录中就有现成的例子:tests/test-shellhooks/ 使用.sh脚本,tests/test-shellhooks-win/ 使用.bat脚本,tests/test-pyhooks/ 与 tests/test-pyshellhooks/ 则展示了纯 Python 与 Python+Shell 混合的钩子布局。
源码层的识别规则
钩子文件如何被"认出来"?看 cookiecutter/hooks.py 中的valid_hook函数:
def valid_hook(hook_file: str, hook_name: str) -> bool: filename = os.path.basename(hook_file) basename = os.path.splitext(filename)[0] matching_hook = basename == hook_name supported_hook = basename in _HOOKS backup_file = filename.endswith('~') return matching_hook and supported_hook and not backup_file由此可以总结出三条硬性规则:
- 文件名必须匹配:去掉扩展名后的名称必须等于
pre_prompt/pre_gen_project/post_gen_project三者之一; - 扩展名任意但后缀有讲究:
.py会使用当前 Python 解释器执行,其他扩展名(.sh、.bat、无扩展名)则直接作为可执行文件调用; - 自动忽略备份文件:以
~结尾的文件(如pre_gen_project.py~)会被视为备份文件而跳过——这一行为有测试用例test_ignore_hook_backup_files专门验证(见 tests/test_hooks.py)。
find_hook函数(cookiecutter/hooks.py)会在模板根目录(以hooks为默认目录)中扫描并返回所有匹配脚本的绝对路径;若hooks/目录不存在或没有匹配文件,则返回None,对应的钩子阶段被静默跳过。
四、Hook 执行语义:时机、目录与失败处理
完整执行时序
以 cookiecutter/main.py 为主干,结合 cookiecutter/generate.py,三类钩子在一次完整生成流程中的调用位置如下:
1. determine_repo_dir() 确定模板目录(本地路径或克隆仓库) 2. run_pre_prompt_hook() 执行 pre_prompt(在提问之前) 3. prompt_for_config() 向用户提问,收集 cookiecutter 上下文 4. generate_files() 内部: ├── render_and_create_dir() 渲染并创建项目根目录 ├── run_hook_from_repo_dir() 执行 pre_gen_project ├── 遍历渲染全部文件/目录 └── run_hook_from_repo_dir() 执行 post_gen_project 5. 返回生成的项目目录源码中的关键调用点:
- cookiecutter/main.py:
repo_dir = str(run_pre_prompt_hook(base_repo_dir)) if accept_hooks else repo_dir - cookiecutter/generate.py:
run_hook_from_repo_dir(repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure) - cookiecutter/generate.py:
run_hook_from_repo_dir(repo_dir, 'post_gen_project', project_dir, context, delete_project_on_failure)
工作目录(Working Directory)
pre_prompt:脚本在仓库目录一份副本的根目录中运行。这是pre_prompt独有的能力——因为你可以在提问开始前重写cookiecutter.json,从而动态改变接下来要提问的内容与默认值。由于它可能修改模板配置,Cookiecutter 会先通过create_tmp_repo_dir(见 cookiecutter/utils.py)把模板目录复制到一个临时目录,再在其中执行钩子,避免污染原始模板。pre_gen_project/post_gen_project:脚本在生成项目(project_dir)的根目录中运行,因此脚本内可以使用相对路径直接定位生成的文件,例如os.path.exists('requirements.txt')即检查生成项目根目录下的文件。测试用例test_run_hook(tests/test_hooks.py)验证了钩子确实在指定的tests_dir(即项目输出目录)中产生文件。
模板变量(Template Variables)
pre_gen_project与post_gen_project钩子支持 Jinja 模板渲染,与项目模板文件一样。执行前,Cookiecutter 会读取钩子脚本全文,用当前上下文渲染后再写入临时文件执行(run_script_with_context,见 cookiecutter/hooks.py)。因此你可以在钩子代码中直接内插上下文变量:
module_name = '{{ cookiecutter.module_name }}'而pre_prompt钩子不支持模板变量(因为它在提问收集上下文之前执行,此时尚无完整上下文),它只能读取cookiecutter.json文件或环境变量来感知环境。
失败处理与目录清理
钩子必须健壮并优雅地处理错误。如果钩子以非零状态退出,项目生成将中止,并且已生成的目录会被清理删除。
源码层面,run_script(cookiecutter/hooks.py)通过subprocess.Popen启动脚本并检查退出码:
exit_status = proc.wait() if exit_status != EXIT_SUCCESS: msg = f'Hook script failed (exit status: {exit_status})' raise FailedHookException(msg)而run_hook_from_repo_dir(cookiecutter/hooks.py)在捕获FailedHookException或 Jinja 的UndefinedError后,会依据delete_project_on_failure标志删除项目目录:
if delete_project_on_failure: rmtree(project_dir)delete_project_on_failure在 cookiecutter/generate.py 中计算:
delete_project_on_failure = output_directory_created and not keep_project_on_failure即:只有 Cookiecutter 自己创建了输出目录(而不是写入已存在的目录)且未设置keep_project_on_failure时,失败才会触发目录清理。两个例外情况值得注意:
- 写入已存在的目录时,失败不会删除该目录(因为里面有可能是用户的既有内容);
- 显式传入
keep_project_on_failure=True(Python API)或使用 CLI 的--keep-project-on-failure标志时,失败也会保留生成目录以便排查。
测试用例test_run_failing_script、test_run_failing_script_enoexec(tests/test_hooks.py)分别验证了OSError与空文件/缺少 shebang(ENOEXEC)时的报错信息;test_run_failing_hook(tests/test_hooks.py)则验证了sys.exit(1)会触发FailedHookException。
执行器细节:Python 与 Shell 的差异
在run_script中可以看到执行差异(cookiecutter/hooks.py):
run_thru_shell = sys.platform.startswith('win') if script_path.endswith('.py'): script_command = [sys.executable, script_path] else: script_command = [script_path] utils.make_executable(script_path) proc = subprocess.Popen(script_command, shell=run_thru_shell, cwd=cwd).py文件使用当前 Python 解释器(sys.executable)执行,无需 shebang;- 其他脚本先通过
make_executable赋予可执行权限,再直接执行,因此需要正确的 shebang(如#!/usr/bin/env bash),否则会报 "might be an empty file or missing a shebang" 错误; - 在 Windows 上会通过 shell 执行,从而支持
.bat脚本。
五、实战示例:三类钩子的典型用法
示例一:pre_prompt 前置环境检查
下面这个hooks/pre_prompt.py在向用户提问之前检查 Docker 是否安装,未安装则直接终止生成,避免用户填写完所有选项后才在后续步骤中失败:
import sys import subprocess def is_docker_installed() -> bool: try: subprocess.run(["docker", "--version"], capture_output=True, check=True) return True except Exception: return False if __name__ == "__main__": if not is_docker_installed(): print("ERROR: Docker is not installed.") sys.exit(1)仓库自带了一个更贴近真实场景的参考实现 tests/test-pyhooks/hooks/pre_prompt.py:它会备份cookiecutter.json到_cookiecutter.json,并且当环境变量COOKIECUTTER_FAIL_PRE_PROMPT=1时以退出码 1 失败——对应的测试test_run_pre_prompt_python_hook与test_run_pre_prompt_python_hook_fail(tests/test_pre_prompt_hooks.py)验证了"成功时创建临时副本目录 + 失败时抛FailedHookException"的完整行为。
示例二:pre_gen_project 校验模板变量
pre_gen_project在模板渲染之前运行,是校验用户输入的理想位置。以下脚本检查用户提供的模块名是否合法:
import re import sys MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$' module_name = '{{ cookiecutter.module_name }}' if not re.match(MODULE_REGEX, module_name): print(f'ERROR: {module_name} is not a valid Python module name!') sys.exit(1)注意module_name由 Jinja 渲染注入:用户输入会直接替换{{ cookiecutter.module_name }}占位符后再执行。校验失败时sys.exit(1)使生成中止并清理项目目录。仓库测试目录中的 tests/test-pyhooks/hooks/pre_gen_project.py 与 tests/test-pyshellhooks/hooks/pre_gen_project.py 提供了同类写法的参考。
示例三:post_gen_project 条件化清理文件
post_gen_project在全部文件生成之后执行,常用于按用户的选择删除不需要的文件。以下示例根据打包方式选项,删除requirements.txt或poetry.lock:
import os REMOVE_PATHS = [ '{% if cookiecutter.packaging != "pip" %}requirements.txt{% endif %}', '{% if cookiecutter.packaging != "poetry" %}poetry.lock{% endif %}', ] for path in REMOVE_PATHS: path = path.strip() if path and os.path.exists(path): os.unlink(path) if os.path.isfile(path) else os.rmdir(path)这里的技巧是:列表项本身就是 Jinja 模板,条件不满足时渲染为空字符串,path.strip()后再判断即可安全跳过。该钩子在项目根目录运行,因此可以直接用相对路径访问生成的文件。
六、控制 Hook 的执行:CLI 与 Python API
Hook 是否执行是可控的,这在信任度较低的第三方模板场景下尤其重要。
CLI 的 --accept-hooks 选项
cookiecutter/cli.py 定义了三态选项:
--accept-hooks [yes|ask|no] Accept pre/post hooks(默认 yes)yes(默认):直接执行全部钩子;ask:执行前询问 "Do you want to execute hooks?",由用户交互确认;no:完全跳过钩子。
对应的 CLI 测试见 tests/test_cli.py(test_cli_accept_hooks),它参数化验证了三种取值最终传入cookiecutter()的accept_hooks布尔值。
Python API 参数
在 cookiecutter/main.py 中,cookiecutter()函数暴露了与钩子直接相关的两个参数:
accept_hooks: bool = True:设为False可跳过pre_prompt、pre_gen_project、post_gen_project全部钩子;keep_project_on_failure: bool = False:设为True时,即使钩子失败也保留生成的项目目录(用于调试)。
from cookiecutter.main import cookiecutter # 生成项目但跳过所有钩子 cookiecutter('path/to/template', accept_hooks=False) # 钩子失败时保留项目目录便于排查 cookiecutter('path/to/template', keep_project_on_failure=True)调用链上,accept_hooks同时控制run_pre_prompt_hook(cookiecutter/main.py)与generate_files内两个钩子(cookiecutter/generate.py 与 cookiecutter/generate.py)的执行,三者行为保持一致。
七、编写高质量 Hook 的实践要点
综合文档约定与源码行为,编写 Hook 时应遵循以下要点:
- 优先使用 Python:天然跨平台;Shell 钩子在 Windows 上需要
shell=True经由命令解释器执行,.bat钩子仅适用于 Windows 平台。 - Hook 内可使用相对路径:
pre_gen_project/post_gen_project的工作目录就是生成项目根目录;pre_prompt的工作目录是模板副本根目录(可安全读写cookiecutter.json)。 - 不要假设目录可随意删除:只有当输出目录由 Cookiecutter 创建且未设置
keep_project_on_failure时,失败才会自动清理;覆盖既有目录时失败不会删除项目目录。 - 保持脚本幂等与健壮:
pre_prompt每次生成都会执行且可修改cookiecutter.json,应避免产生不可重复的副作用。 - 善用环境变量:
pre_prompt拿不到上下文变量,但可以读取环境变量感知运行环境(仓库示例 tests/test-pyhooks/hooks/pre_prompt.py 即通过环境变量控制失败行为)。 - 注意 Jinja 渲染的副作用:
pre_gen_project/post_gen_project脚本本身会先被 Jinja 渲染,脚本中若出现{{ ... }}之外的特殊语法,需确保在模板上下文中可解析,否则会触发UndefinedError并中止生成。
八、小结
Cookiecutter 的 Hooks 机制在"模板渲染"这条主线之外,为用户提供了三个精确的干预点:提问前的pre_prompt(环境预检、动态改写配置)、渲染前的pre_gen_project(变量校验)与生成后的post_gen_project(后处理与初始化)。理解它们的执行时机、工作目录、模板变量支持与失败清理语义,是编写健壮模板的进阶基本功。若需进一步了解钩子与模板文件渲染的配合细节,可继续阅读 docs/advanced/hooks.rst 之外的本仓库相关文档,如 docs/advanced/human_readable_prompts.rst(提问环节)与 docs/advanced/replay.rst(上下文回放对钩子执行的影响)。
- 开发工具
- CLI
- 代码生成
【免费下载链接】cookiecutter
A cross-platform command-line utility that creates projects from cookiecutters (project templates), e.g. Python package projects, C projects.
相关推荐
Lefthook `setup` 指令完全指南:在 Git Hook 任务执行前自动准备环境
Lefthook setup 指令完全指南:在 Git Hook 任务执行前自动准备环境 导读 setup 是 Lefthook 提供的一组"预执行指令",用于
开发工具Velero Backup Hooks 完全指南:在备份前后于 Pod 容器内执行命令
Velero Backup Hooks 完全指南:在备份前后于 Pod 容器内执行命令 本文围绕 Velero 1.1 文档中的 Hooks 主题(对应仓库文档
云原生灾备存储后端MaaAssistantArknights maa-cli 完全指南:MaaCore 管理、任务执行与自定义自动化任务
MaaAssistantArknights maa cli 完全指南:MaaCore 管理、任务执行与自定义自动化任务 本篇基于 MaaAssistantArk
计算机视觉GUI自动化RPA
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考