chezmoi 使用 `.chezmoidata.$FORMAT` 注入静态模板数据:格式、合并规则与源码原理
2026/9/20 19:57:52 网站建设 项目流程

chezmoi 使用.chezmoidata.$FORMAT注入静态模板数据:格式、合并规则与源码原理

【免费下载链接】chezmoiManage your dotfiles across multiple diverse machines, securely.项目地址: https://gitcode.com/gh_mirrors/ch/chezmoi

本文围绕 chezmoi 源码状态中的特殊数据文件.chezmoidata.$FORMAT展开,讲解如何用 JSON、JSONC、TOML、YAML 四种格式为模板引擎预置结构化静态数据,说明多个数据文件的词法排序合并规则、字典递归合并与其余类型整体替换的行为,并给出用chezmoi data验证结果的完整实操。读完本文,你将掌握在 chezmoi 的模板中使用.fontSize这类全局数据变量的标准姿势,并理解它与.chezmoidata/目录、data配置段之间的分工边界。

一、.chezmoidata.$FORMAT是什么

在 chezmoi 的源码状态(source state)中,只要存在名为.chezmoidata.$FORMAT的文件,它就会被解释为给定格式的结构化静态数据。这些数据随后可以在所有模板中作为点号根变量(如.fontSize)直接使用。$FORMAT支持四种格式:JSON、JSONC、TOML、YAML,这一点在 config-format.md 中明确列出,且格式解析由 internal/chezmoi/format.go 中的FormatJSONFormatJSONCFormatTOMLFormatYAML四种实现承载。

与它配套的还有一种组织形式:.chezmoidata/目录,目录内所有文件同样会被当作结构化数据读取。两者的语义一致,相关说明见.chezmoidata/

最小示例:让模板读到fontSize

假如~/.local/share/chezmoi/.chezmoidata.toml内容如下:

fontSize = 12

那么在任意模板(如dot_config/foo.tmpl)中,.fontSize变量即变为可用:

FONT_SIZE={{ .fontSize }}

渲染结果是:

FONT_SIZE=12

这里的关键在于数据文件必须位于源码状态目录内(默认即~/.local/share/chezmoi/),并且文件名精确匹配.chezmoidata.toml这类带格式后缀的形式。源码中通过isPrefixDotFormat(fileInfo.Name(), dataName)(internal/chezmoi/format.go)判断,凡是.chezmoidata.后跟任一已知格式扩展名的文件都会被识别,并交给addTemplateData处理。

二、支持的四种格式与解析入口

.chezmoidata.$FORMAT$FORMAT的取值由扩展名决定,与 chezmoi 配置文件一致,支持:

扩展名格式解析实现
.jsonJSONformat.goFormatJSON
.jsoncJSON with Comments(带注释与尾逗号的 JSON)format.goFormatJSONC
.tomlTOMLformat.goFormatTOML
.yaml/.ymlYAMLformat.goFormatYAML

从源码看,addTemplateData(internal/chezmoi/sourcestate.go)首先用FormatFromAbsPath(sourceAbsPath)依据扩展名解析出对应Format,然后调用format.Unmarshal(data, &templateData)把文件内容反序列化为map[string]any,最后合并进s.userTemplateData。这一过程发生在模板引擎启动之前:源码状态读取时,switch分支(internal/chezmoi/sourcestate.go)会在遍历源码目录时专门捕获dataName(即.chezmoidata)与isPrefixDotFormat(fileInfo.Name(), dataName)两类文件,并执行addTemplateDataDiraddTemplateData,这正是下文“数据文件不能是模板”这一约束的根源。

三、多文件合并:词法排序 + 字典递归合并

源码状态中可以存在多个.chezmoidata.$FORMAT文件,它们全部合并到数据字典(data dictionary)的根上,读取顺序为文件系统下的词法(字母序)顺序

合并示例:四种格式的 z 字典

dot_config源码目录下的四个文件为例:

{ "z": { "z": 3 } }
{ "z": { "z": 4 } }
z.x = 1
z: y: 2

按文件名词法排序,读取顺序为.chezmoidata.json.chezmoidata.jsonc.chezmoidata.toml.chezmoidata.yaml。因此.chezmoidata.jsoncz.z = 4会覆盖.chezmoidata.json中的z.z = 3(后读覆盖先读),最终chezmoi data输出的合并结果为:

{ "z": { "x": 1, "y": 2, "z": 4 } }

注意.chezmoidata.tomlz.x.chezmoidata.yamlz.y分别作为新键并入,而非整体替换z字典。

源码层面的合并实现

上述行为由 internal/chezmoi/recursivemerge.go 的RecursiveMerge保证:

  • 目标字典中不存在的键:直接复制源值;
  • 目标字典中已存在的键,且两侧值都是map[string]any:递归向下合并字典;
  • 其余所有情况(包括键已存在但值不是字典、或源值不是字典):用源值整体替换目标值。

这直接解释了文档中的一条硬性规则——只有字典会被递归合并,其他值(尤其是列表)一律整体替换。例如若两个数据文件都定义了colors列表,后读文件的colors会完全覆盖先读文件的值,而不是拼接。

addTemplateData每次读取一个文件后都会调用RecursiveMerge(s.userTemplateData, templateData),并把缓存的s.templateData置空(internal/chezmoi/sourcestate.go),保证合并结果在下一次TemplateData()调用时重新生效。

四、数据文件的读取顺序与.chezmoidata/目录

.chezmoidata/目录内的文件遵循同样的规则:文件间按词法顺序合并,目录与目录之间也按词法顺序合并,最终同样合并到数据字典的根。典型示例如下(参见.chezmoidata/):

{ "z": { "z": 3 } }
{ "z": { "z": 4 } }
z.x = 1
z: y: 2

按文件名排序后读取顺序为alpha.jsoncbeta.tomlgamma.yamlzed.json,因此zed.jsonz.z = 3覆盖了alpha.jsoncz.z = 4,合并结果中z{"x": 1, "y": 2, "z": 3}

从源码看,addTemplateDataDir(internal/chezmoi/sourcestate.go)会遍历.chezmoidata/目录:以.开头的特殊文件(Prefix)被禁止放入该目录,常规文件逐个交给addTemplateData处理;同时目录内不允许出现模板(以.tmpl结尾)——这与.chezmoidata.$FORMAT文件“不能是模板”的限制一脉相承,源码里isPrefixDotFormat只匹配纯格式扩展名,而isPrefixDotFormatDotTmpl(internal/chezmoi/format.go)才匹配带.tmpl后缀的变体,后者用于.chezmoi.$FORMAT.tmpl这类配置模板而非数据文件。

命名建议

由于合并结果对同名键后者覆盖前者,若希望某个文件的优先级更高,可以在文件命名上利用词法顺序,例如把需要“垫底”的默认值放在以字母序更靠前的名字(如00-defaults.json),把需要覆盖默认值的机器特定值放在靠后的名字(如99-overrides.json)。这一技巧对.chezmoidata.$FORMAT文件与.chezmoidata/目录内文件同样适用。

五、验证合并结果:chezmoi data

chezmoi data命令会输出完整的模板数据字典。它通过WithTemplateDataOnly(true)构建源码状态并调用sourceState.TemplateData()(internal/cmd/datacmd.go),因此可以直观看到所有.chezmoidata.*文件合并后的最终形态,是排查数据覆盖问题的最直接手段。例如上文的四文件合并示例,执行后z字典即如第三节所示。

TemplateData()(internal/chezmoi/sourcestate.go)内部按固定优先级递归合并三类数据源:

  1. defaultTemplateData:chezmoi 内置的默认数据(含.chezmoi元信息);
  2. userTemplateData:来自.chezmoidata.$FORMAT文件与.chezmoidata/目录的数据;
  3. priorityTemplateData:来自配置data段的高优先级数据。

后合并者覆盖先合并者,因此配置段data中的动态机器数据优先级高于静态数据文件。

六、重要限制:数据文件不能是模板

!!! warning明确指出:.chezmoidata.$FORMAT文件不能是模板。原因在于模板引擎尚未启动之前,这些数据文件就必须已就位,模板渲染(如.chezmoi.toml.tmpl对配置的求值)反过来还要依赖这些数据。源码也印证了这一点:addTemplateData直接ReadFileformat.Unmarshal解析为静态字典,全程不经过模板执行器(internal/chezmoi/sourcestate.go)。

对不同类型的动态数据,文档给出两条明确出路:

  • 动态机器数据(如主机名、操作系统、架构等随机器变化的值):应放在.chezmoi.$FORMAT.tmpl配置文件的data段中,让模板配置先计算再注入数据;
  • 动态环境数据(运行时才能获取的外部信息):应在模板中通过函数实时读取,推荐outputfromJsonfromYaml等模板函数。

对应的函数参考文档为 output、fromJson 与 fromYaml。例如:

{{ $data := output "some-command" | fromJson }} VALUE={{ $data.field }}

这样的写法允许在模板渲染阶段拉取命令输出或外部文件,再解析为结构化数据使用,与静态数据文件形成互补。

七、数据文件在测试中的实际用法

仓库的 txtar 集成测试 templatedata.txtar 提供了数据文件与模板联动的端到端证据:

  • 测试创建了一个包含.chezmoidata.toml(内容filename = ".file2")、.chezmoiignore.chezmoitemplates/ignore的源码状态;
  • .chezmoiignore中通过{{ template "ignore" . }}引用模板,而该模板内部使用{{ .filename }}——这里的.filename正是来自.chezmoidata.toml的数据;
  • 最终chezmoi apply的结果是$HOME/.file1被创建、.file2被忽略,证明数据文件的值确实进入了模板上下文并影响实际行为。

这组测试同时验证了另一条事实:.chezmoidata.*的数据不仅可用于目标文件模板,也可以被.chezmoiignore等控制文件中的模板引用,只要这些模板的求值发生在数据加载之后。

八、总结与最佳实践

  • .chezmoidata.$FORMAT.json/.jsonc/.toml/.yaml)与.chezmoidata/目录用于向模板提供静态、结构化的数据,以.变量名的形式在任意模板中访问;
  • 多个数据文件按文件名词法顺序依次合并到数据字典根上;同名键后者覆盖前者;
  • 字典递归合并,其他类型(含列表)整体替换——这是 recursivemerge.go 定义的核心语义;
  • chezmoi data随时查看合并后的完整数据字典,用它来验证覆盖关系;
  • 数据文件不可模板化;机器动态数据放配置data段,环境动态数据用output/fromJson/fromYaml在模板内实时获取;
  • 需要覆盖默认值时,利用词法顺序把高优先级数据放到字母序更靠后的文件名中。

掌握这套规则后,你可以把常见的跨机器差异(如字体大小、编辑器偏好、用户名邮箱等)沉淀为静态数据文件,让所有模板统一消费,同时保持数据与模板逻辑清晰分离。

【免费下载链接】chezmoiManage your dotfiles across multiple diverse machines, securely.项目地址: https://gitcode.com/gh_mirrors/ch/chezmoi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询