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 中的FormatJSON、FormatJSONC、FormatTOML、FormatYAML四种实现承载。
与它配套的还有一种组织形式:.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 配置文件一致,支持:
| 扩展名 | 格式 | 解析实现 |
|---|---|---|
.json | JSON | format.goFormatJSON |
.jsonc | JSON with Comments(带注释与尾逗号的 JSON) | format.goFormatJSONC |
.toml | TOML | format.goFormatTOML |
.yaml/.yml | YAML | format.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)两类文件,并执行addTemplateDataDir或addTemplateData,这正是下文“数据文件不能是模板”这一约束的根源。
三、多文件合并:词法排序 + 字典递归合并
源码状态中可以存在多个.chezmoidata.$FORMAT文件,它们全部合并到数据字典(data dictionary)的根上,读取顺序为文件系统下的词法(字母序)顺序。
合并示例:四种格式的 z 字典
以dot_config源码目录下的四个文件为例:
{ "z": { "z": 3 } }{ "z": { "z": 4 } }z.x = 1z: y: 2按文件名词法排序,读取顺序为.chezmoidata.json→.chezmoidata.jsonc→.chezmoidata.toml→.chezmoidata.yaml。因此.chezmoidata.jsonc中z.z = 4会覆盖.chezmoidata.json中的z.z = 3(后读覆盖先读),最终chezmoi data输出的合并结果为:
{ "z": { "x": 1, "y": 2, "z": 4 } }注意.chezmoidata.toml的z.x、.chezmoidata.yaml的z.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 = 1z: y: 2按文件名排序后读取顺序为alpha.jsonc→beta.toml→gamma.yaml→zed.json,因此zed.json的z.z = 3覆盖了alpha.jsonc的z.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)内部按固定优先级递归合并三类数据源:
defaultTemplateData:chezmoi 内置的默认数据(含.chezmoi元信息);userTemplateData:来自.chezmoidata.$FORMAT文件与.chezmoidata/目录的数据;priorityTemplateData:来自配置data段的高优先级数据。
后合并者覆盖先合并者,因此配置段data中的动态机器数据优先级高于静态数据文件。
六、重要限制:数据文件不能是模板
!!! warning明确指出:.chezmoidata.$FORMAT文件不能是模板。原因在于模板引擎尚未启动之前,这些数据文件就必须已就位,模板渲染(如.chezmoi.toml.tmpl对配置的求值)反过来还要依赖这些数据。源码也印证了这一点:addTemplateData直接ReadFile后format.Unmarshal解析为静态字典,全程不经过模板执行器(internal/chezmoi/sourcestate.go)。
对不同类型的动态数据,文档给出两条明确出路:
- 动态机器数据(如主机名、操作系统、架构等随机器变化的值):应放在
.chezmoi.$FORMAT.tmpl配置文件的data段中,让模板配置先计算再注入数据; - 动态环境数据(运行时才能获取的外部信息):应在模板中通过函数实时读取,推荐
output、fromJson、fromYaml等模板函数。
对应的函数参考文档为 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),仅供参考