chezmoi 模板函数 fromJsonc 详解:在 dotfiles 模板中解析带注释的 JSONC 数据
【免费下载链接】chezmoiManage your dotfiles across multiple diverse machines, securely.项目地址: https://gitcode.com/gh_mirrors/ch/chezmoi
fromJsonc是 chezmoi 模板引擎中用于将 JSONC(Human JSON,即带注释与尾随逗号的 JSON 超集)文本解析为结构化数据的模板函数。它让模板作者可以直接读取带有注释、尾随逗号等人类友好写法的配置文件与数据源,并像使用普通 map/slice 一样访问其中的字段。读完本文,你将掌握fromJsonc的完整签名、底层解析原理、数字类型转换规则、实际模板用法及其在 chezmoi 代码库内部的真实应用场景。
函数签名与核心语义
根据官方函数参考文档 fromJsonc.md,fromJsonc的签名定义如下:
fromJsonc jsonctext- 参数:
jsonctext,一个字符串,内容是待解析的 JSONC 文本。 - 返回值:解析得到的值(对象、数组、字符串、数字或布尔值)。
- 解析引擎:底层使用
github.com/tailscale/hujson库完成 JSONC 解析,该依赖在 go.mod 中声明。
简单来说,fromJsonc与模板函数fromJson行为一致,但额外兼容 JSONC 语法扩展:允许//行注释、/* ... */块注释以及对象与数组末尾的尾随逗号(trailing comma),这正是诸如 VS Code、微软 WinGet 等工具配置文件所采用的"Human JSON"风格。
JSONC 与标准 JSON 的区别
标准 JSON(RFC 7159)是严格的纯数据格式,不允许任何注释和尾随逗号;而 JSONC(JSON with Comments)是它的超集,允许:
//单行注释;/* ... */多行块注释;- 数组和对象最后一个元素之后的逗号(尾随逗号)。
例如下面这份文本是合法的 JSONC,但无法被标准 JSON 解析器接受:
{ "key": 1, // Comment }该示例正是 chezmoi 测试套件中用于验证fromJsonc的输入数据,见 templatefuncs.txtar。
底层实现原理
模板函数入口
fromJsonc的模板函数实现位于 templatefuncs.go:
// fromJsoncTemplateFunc parses s as JSONC and returns the result. In contrast // to encoding/json, numbers are represented as int64s or float64s if possible. func (c *Config) fromJsoncTemplateFunc(s string) any { var value any must(chezmoi.FormatJSONC.Unmarshal([]byte(s), &value)) return value }实现非常简洁:将输入字符串转为字节切片,交给 chezmoi 内部定义的FormatJSONC反序列化器,结果存入any类型变量返回。解析失败时通过must抛出 panic,与 chezmoi 其他模板函数(如fromJson、fromYaml)的错误处理风格保持一致。
函数注册
fromJsonc在模板函数注册表中与众多内建函数一起注册,见 config.go:
"fromJsonc": c.fromJsoncTemplateFunc,因此它可以在任何 chezmoi 模板上下文中直接使用,包括chezmoi execute-template、源文件模板、chezmoi data输出等。文档站点配置 mkdocs.yml 也将其注册为独立的参考页面。
JSONC 反序列化的两步流水线
FormatJSONC由 format.go 中的formatJSONC类型实现,其Unmarshal采用"先标准化、再按 JSON 解析"的两步策略:
// Unmarshal implements Format.Unmarshal. func (formatJSONC) Unmarshal(data []byte, value any) error { data, err := hujson.Standardize(data) if err != nil { return err } return FormatJSON.Unmarshal(data, value) }- 标准化(Standardize):
hujson.Standardize将 JSONC 文本中的注释剥离、移除尾随逗号,并规范空白,产出一份等价的标准 JSON; - 标准解析:标准化结果交给
FormatJSON.Unmarshal(即 format.go 中的严格 JSON 解码器)完成真正的解析。
formatJSONC同时实现了Marshal(见 format.go):先用 Go 标准库json.Encoder编码(关闭 HTML 转义),再用hujson.Format输出人类友好的 JSONC 排版。这意味着FormatJSONC在 chezmoi 内部被当作一种一等序列化格式对待,与json、toml、yaml并列,注册于FormatsByName与FormatsByExtension映射中(见 format.go)。
严格的 JSON 解码细节
FormatJSON.Unmarshal使用了json.Decoder的DisallowUnknownFields(),并在解码到通用类型时启用UseNumber()以保留数字精度,随后做一次"只允许单一顶层值"的 EOF 校验(见 format.go)。这些约束同样作用于fromJsonc的解析结果。
数字类型转换规则
fromJsonc与fromJson一样,在数字处理上比 Go 标准库encoding/json更智能。根据 format.go 的replaceJSONNumbersWithNumericValues逻辑,解析出的 JSON 数字按如下优先级转换:
- 能被精确表示为 64 位有符号整数(int64)的数字,返回
int64; - 否则,若在 64 位 IEEE 浮点数范围内,返回
float64; - 否则(超出两者表示范围),以字符串形式返回,以保留原始数值,如 format.go 注释所述(这类值合法但实际罕见,参见 RFC 7159 Section 6)。
这一规则与 fromJson.md 中fromJson的行为完全一致,保证模板中做数值运算(如add、mul)时能拿到真正的数字而非字符串。
实战用法
在 execute-template 中解析标准输入
最直接的用法是借助--with-stdin将文件内容注入模板上下文.chezmoi.stdin,再用fromJsonc解析。chezmoi 的集成测试 templatefuncs.txtar 给出了完整可复现的示例:
// 输入文件 example.jsonc { "key": 1, // Comment }chezmoi execute-template --with-stdin '{{ fromJsonc .chezmoi.stdin | toJson }}' # 输出 {"key":1}可以看到,输入中的// Comment注释被正确剥离,toJson输出为标准 JSON 格式。
在模板文件中读取带注释的配置
fromJsonc也常用于源文件模板内部,读取机器上已有的 JSONC 风格配置文件作为模板数据。例如:
{{- $config := fromJsonc (include "~/.config/editor/settings.jsonc") -}} {{ $config.theme | default "dark" }}这里的include负责读取文件内容(支持~展开),fromJsonc将其解析为字典,随后即可通过点路径访问嵌套字段,配合default提供回退值。
结合管道与其他函数使用
由于fromJsonc返回值是any类型,天然适合接入管道,配合toJson、toYaml、index、hasKey、get等函数做后续处理。比如将 JSONC 配置整体转成 YAML 输出:
chezmoi execute-template --with-stdin '{{ fromJsonc .chezmoi.stdin | toYaml }}'错误处理与边界情况
fromJsonc对非法输入直接报错(panic 后转为 chezmoi 错误消息)。format_test.go 用一组表格测试完整覆盖了FormatJSONC的边界行为,可作为排查解析问题的参考:
| 输入 | 结果 |
|---|---|
{"key":"value"} // comment | 解析成功,值为{"key":"value"} |
{"key":"value"} | 解析成功 |
| 空输入 | 报错parsing value: unexpected EOF |
{"key":"value"}1(顶层多余值) | 报错invalid character '1' after top-level value |
{"unknown":"value"}(未知字段) | 报错json: unknown field "unknown" |
{(意外 EOF) | 报错parsing value: unexpected EOF |
"\n"(仅空白) | 报错parsing value: unexpected EOF |
这些用例揭示了几条实用结论:
- 顶层必须恰好是一个值:JSONC 内容之后若还有多余 token(如
}1)会失败,避免静默丢弃数据; - 不允许未知字段:在需要严格校验结构的场景中(结合具名结构体解析),
DisallowUnknownFields会拒绝未声明的键; - 空白与空输入视为错误:与
fromYaml对空输入宽容不同(见 format.go),fromJsonc要求内容非空。
在 chezmoi 代码库中的真实应用
fromJsonc的底层格式FormatJSONC并非仅服务模板函数,它还被 chezmoi 自身用于解析真实世界中的 JSONC 文件。最典型的例子在 upgradecmd_windows.go:chezmoi 在 Windows 上升级时,会读取微软 WinGet 的settings.json(该文件允许注释,属 JSONC 格式)来判断可移植包安装位置:
settingsPaths := []string{ os.ExpandEnv(`${LOCALAPPDATA}\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json`), os.ExpandEnv(`${LOCALAPPDATA}\Microsoft\WinGet\Settings\settings.json`), } for _, settingsPath := range settingsPaths { if _, err := os.Stat(settingsPath); err == nil { winGetSettingsContents, err := os.ReadFile(settingsPath) if err == nil { if err := chezmoi.FormatJSONC.Unmarshal(winGetSettingsContents, &winGetSettings); err != nil { return false, err } } } }这印证了FormatJSONC(即fromJsonc的底层)对"人类可读配置"的解析能力是经过真实场景验证的——同样是"带注释的 JSON 配置文件",在模板中你可以用fromJsonc,在 Go 代码中可以直接用chezmoi.FormatJSONC.Unmarshal。
与其他解析函数的对比与选择
chezmoi 提供了完整的反序列化函数家族,注册表见 config.go:
| 函数 | 解析格式 | 典型场景 |
|---|---|---|
fromJson | 严格 JSON | 标准的 JSON API 响应、纯 JSON 配置 |
fromJsonc | JSONC(注释 + 尾随逗号) | 允许注释的配置文件(如 WinGet、VS Code 风格) |
fromToml | TOML | Rust/Cargo 风格的配置文件 |
fromYaml | YAML | Kubernetes、Ansible 等 YAML 生态 |
选型建议:当数据来源可能带注释、或来源是人工维护的 JSONC 配置时用fromJsonc;当数据来自程序生成的严格 JSON 时用fromJson即可。若数据可能包含注释但不确定,直接选用fromJsonc是更稳妥的选择,因为它兼容标准 JSON 的全部语法,仅是放宽了注释与尾随逗号的限制。
小结
fromJsonc以极小的 API 面(一个字符串参数、一个任意类型返回值)提供了 JSONC 解析能力,其背后是"hujson 标准化 + 严格 JSON 解码"的两步流水线,以及 int64/float64/string 三级数字保真策略。无论是通过chezmoi execute-template --with-stdin快速解析带注释的配置,还是在源文件模板中读取机器上的 JSONC 文件,fromJsonc都让"人类友好的 JSON"与"模板可用的结构化数据"无缝衔接,是处理配置类数据源时值得优先考虑的工具。
【免费下载链接】chezmoiManage your dotfiles across multiple diverse machines, securely.项目地址: https://gitcode.com/gh_mirrors/ch/chezmoi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考