使用Golang修改YAML文件的实践指南
使用Golang修改YAML文件的实践指南 我已经尝试了好几天,想完成一件本以为很简单的事情。我仅仅想修改 Docker Compose YAML 文件中一个特定的键值(位于多层嵌套深处),同时保持文件其余部分(包括注释)完好无损。Docker Compose 文件没有明确定义的结构,因此每个文件的模式可能都不同。
据我所知,标准的 Go 语言 YAML 库并不支持我所需要的功能。我从未能够使用 map[string]interface{} 对 Docker Compose YAML 文件进行解组/编组,并使结果看起来与原始文件相似。当我尝试这样做时,文件的许多元素都丢失了。使用 yaml.Node 进行解组/编组会在编组调用时产生错误。简而言之,我甚至无法对 Docker Compose YAML 文件进行解组/编组,更不用说修改它了。
唯一接近我目标的 Go 语言包是 yamled(https://pkg.go.dev/github.com/vmware-labs/go-yaml-edit)。不幸的是,这个包不再受支持,甚至无法编译。
有人知道如何实现这件我本以为极其简单的事情吗?
更多关于使用Golang修改YAML文件的实践指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html
Bruce_Thompson:
请参考我最初的问题。如果你事先不知道模式,那么所有标准的 yaml 包(包括 yaml.v3 / yaml.Node)都无法工作。它们都相当无用。
你可以向你喜欢的 yaml 包提交错误报告或功能请求,如果你有的话。 你是否有无法正确解组/编组,或解码/编码的 yaml 文件示例?
更多关于使用Golang修改YAML文件的实践指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
请参考我最初的问题。如果你事先不知道模式,那么所有标准的 yaml 包(包括 yaml.v3 / yaml.Node)都无法工作。它们都相当无用。
但有个好消息。我让 yamled(yamled 包 - github.com/vmware-labs/go-yaml-edit - Go Packages)成功编译了。而且它确实有效!!!
有点遗憾的是,唯一一个似乎能处理任意 yaml 文件的包,其创建公司(vmware)已不再提供支持。
你试过这个吗?
GitHub - compose-spec/compose-go: 用于解析和加载 Compose 文件的参考库
GitHub - compose-spec/compose-go: 用于解析和加载 Compose 文件的参考库
用于解析和加载 Compose 文件的参考库 - GitHub - compose-spec/compose-go: 用于解析和加载 Compose 文件的参考库
你尝试过使用 gopkg.in/yaml.v3 吗?它似乎支持注释 yaml package - gopkg.in/yaml.v3 - Go Packages。
我目前还没有使用它。我仍然在使用 gopkg.in/yaml.v2,它使用 map[interface{}]interface{} 并且不支持注释。
除了注释之外,在你进行解组/编组时还有其他缺失的元素吗?我不明白你如何在使用通用的 map[string]any 时保留注释,尤其是在使用 Unmarshal/Marshal 的情况下。注释应该放在哪里呢?你可能需要使用某种 Decoder/Encoder。
你有没有考虑过不使用 yaml 来编辑文件,而是使用正则表达式来查找你想要修改的值?
保留注释以及可能的格式,对我来说似乎并不是一件非常简单的事情。
Bruce_Thompson:
如果你事先不知道模式,那么所有标准的 yaml 包(包括 yaml.v3 / yaml.Node)都无法工作。
我认为这个说法是不正确的。请看我示例代码,其中我将 yaml 作为一个 yaml.Node 对象递归遍历,该对象包含 yaml.Node 子节点。无论模式如何,该代码都能工作,你不需要事先知道模式。区别在于 vmware-labs/go-yaml-edit 是使用 text/transform.Transformer 来转换文本。
Bruce_Thompson:
唯一似乎能处理任意 yaml 文件的包不再受其创建公司(vmware)的支持,这有点令人遗憾。
它位于他们的 labs 项目中,该项目并非用于生产用途。来自该组织,强调部分为我所加:
此组织包含实验性开源项目。
再次强调,对于任意的 yaml 文件绝对有解决方案。上面的代码是通用的,对其正在遍历的模式一无所知。并且它可以适用于任何模式。
我认为你需要的是 yaml.Node。让我们创建一个名为 test.yaml 的文件,内容如下:
services: # These are the services
# OK new line comment
web: # web and ports and stuff
build: .
ports:
- "8000:5000"
redis:
image: "redis:alpine"
然后尝试以下 Go 代码:
func main() {
b, err := os.ReadFile("test.yaml")
if err != nil {
log.Fatalf("Problem opening file: %v", err)
}
var dockerCompose yaml.Node
yaml.Unmarshal(b, &dockerCompose)
// Swap out redis:alpine for redis:golang
imageNode := findChildNode("redis:alpine", &dockerCompose)
if imageNode != nil {
imageNode.SetString("redis:golang")
}
// Create a modified yaml file
f, err := os.Create("modified.yaml")
if err != nil {
log.Fatalf("Problem creating file: %v", err)
}
defer f.Close()
yaml.NewEncoder(f).Encode(dockerCompose.Content[0])
}
// Recusive function to find the child node by value that we care about.
// Probably needs tweaking so use with caution.
func findChildNode(value string, node *yaml.Node) *yaml.Node {
for _, v := range node.Content {
// If we found the value we are looking for, return it.
if v.Value == value {
return v
}
// Otherwise recursively look more
if child := findChildNode(value, v); child != nil {
return child
}
}
return nil
}
这将在 modified.yaml 中生成以下内容:
services: # These are the services
# OK new line comment
web: # web and ports and stuff
build: .
ports:
- "8000:5000"
redis:
image: "redis:golang"
这至少应该能让你走上寻找解决方案的道路。我唯一的另一个想法是:由于 Docker 是用 Go 编写的并且是开源的,你可以直接看看他们是如何解析相关的 yaml 文件的,然后复制他们的做法。然而,许多 yaml 解析器并不将注释视为数据序列化/反序列化的一部分,因此它们很可能会忽略注释。有用的阅读材料:
我想加载一个 YAML 文件,可能编辑数据,然后再转储它。如何保留格式?
标签: formatting, yaml
由 flyx 提问
对于修改YAML文件同时保持格式和注释的需求,推荐使用yaml.v3库的yaml.Node结构。以下是具体实现示例:
package main
import (
"fmt"
"io/ioutil"
"gopkg.in/yaml.v3"
)
func updateYAML(filePath string, targetPath []string, newValue interface{}) error {
// 读取YAML文件
data, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
// 解析为yaml.Node
var root yaml.Node
if err := yaml.Unmarshal(data, &root); err != nil {
return err
}
// 递归查找并修改目标节点
if err := findAndUpdateNode(&root, targetPath, newValue); err != nil {
return err
}
// 重新编组回YAML
output, err := yaml.Marshal(&root)
if err != nil {
return err
}
// 写回文件
return ioutil.WriteFile(filePath, output, 0644)
}
func findAndUpdateNode(node *yaml.Node, path []string, newValue interface{}) error {
if len(path) == 0 {
// 找到目标节点,更新值
node.Value = fmt.Sprintf("%v", newValue)
return nil
}
currentKey := path[0]
remainingPath := path[1:]
// 遍历当前节点的内容
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
if keyNode.Value == currentKey {
return findAndUpdateNode(valueNode, remainingPath, newValue)
}
}
return fmt.Errorf("key not found: %s", currentKey)
}
func main() {
// 示例:修改嵌套的键值
targetPath := []string{"services", "app", "environment", "LOG_LEVEL"}
newValue := "debug"
err := updateYAML("docker-compose.yml", targetPath, newValue)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
对于更复杂的Docker Compose文件修改,可以使用更通用的方法:
func updateDockerCompose(filePath string, serviceName string, envKey string, envValue string) error {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
var root yaml.Node
if err := yaml.Unmarshal(data, &root); err != nil {
return err
}
// 查找services节点
servicesNode := findNode(&root, "services")
if servicesNode == nil {
return fmt.Errorf("services not found")
}
// 查找特定服务
serviceNode := findNode(servicesNode, serviceName)
if serviceNode == nil {
return fmt.Errorf("service %s not found", serviceName)
}
// 查找或创建environment节点
envNode := findNode(serviceNode, "environment")
if envNode == nil {
// 创建新的environment节点
envNode = &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
Content: []*yaml.Node{},
}
addToNode(serviceNode, "environment", envNode)
}
// 更新环境变量
updateEnvVar(envNode, envKey, envValue)
output, err := yaml.Marshal(&root)
if err != nil {
return err
}
return ioutil.WriteFile(filePath, output, 0644)
}
func findNode(parent *yaml.Node, key string) *yaml.Node {
if parent.Kind == yaml.DocumentNode && len(parent.Content) > 0 {
return findNode(parent.Content[0], key)
}
if parent.Kind == yaml.MappingNode {
for i := 0; i < len(parent.Content); i += 2 {
if parent.Content[i].Value == key {
return parent.Content[i+1]
}
}
}
return nil
}
这种方法可以保持YAML文件的原始格式、注释和结构,同时只修改指定的键值。yaml.Node提供了对YAML文档的完整控制,包括行注释、头部注释等元数据。


