使用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

7 回复

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

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 解析器并不将注释视为数据序列化/反序列化的一部分,因此它们很可能会忽略注释。有用的阅读材料:

flyx

我想加载一个 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文档的完整控制,包括行注释、头部注释等元数据。

回到顶部