修改现有的 yaml 文件并添加新的数据和注释

Posted

技术标签:

【中文标题】修改现有的 yaml 文件并添加新的数据和注释【英文标题】:Modify existing yaml file and add new data and comments 【发布时间】:2019-09-04 14:09:18 【问题描述】:

我最近看到go yaml lib有新版本(V3)

具有nodes 功能(在我看来这是一个杀手级功能:))可以在不改变文件结构的情况下修改 yamls 帮助很多

但由于它是相当新的(从上周开始),我没有找到一些有用的文档和我需要的 上下文示例(添加新对象/节点并保留 文件结构相同,无需删除 cmets 等)

我需要的是操作yaml文件

例如

假设我有这个 yaml 文件

version: 1
type: verbose
kind : bfr

# my list of applications
applications:
  - name: app1
    kind: nodejs
    path: app1
    exec:
      platforms: k8s
      builder: test

现在我得到了一个 json 对象(例如 app2),我需要将其插入到现有文件中

[

    
        "comment: "Second app",
        "name": "app2",
        "kind": "golang",
        "path": "app2",
        "exec": 
            "platforms": "dockerh",
            "builder": "test"
        
    
]

我需要在第一个应用程序之后将它添加到 yml 文件中,(应用程序是应用程序的数组)

version: 1
type: verbose
kind : bfr

# my list of applications
applications:

#  First app
  - name: app1
    kind: nodejs
    path: app1
    exec:
      platforms: k8s
      builder: test

# Second app
  - name: app2
    kind: golang
    path: app2
    exec:
      platforms: dockerh
      builder: test

是否可以从 yaml 文件中添加新的 json 对象?也删除现有的

我也找到了这个博客 https://blog.ubuntu.com/2019/04/05/api-v3-of-the-yaml-package-for-go-is-available

这是代表对象的类型

type VTS struct 
    version string       `yaml:"version"`
    types   string       `yaml:"type"`
    kind    string       `yaml:"kind,omitempty"`
    apps    Applications `yaml:"applications,omitempty"`


type Applications []struct 
    Name string `yaml:"name,omitempty"`
    Kind string `yaml:"kind,omitempty"`
    Path string `yaml:"path,omitempty"`
    Exec struct 
        Platforms string `yaml:"platforms,omitempty"`
        Builder   string `yaml:"builder,omitempty"`
     `yaml:"exec,omitempty"`

更新

在测试了wiil7200 提供的解决方案后,我发现了 2 个问题

我最后使用将它写入文件 err = ioutil.WriteFile("output.yaml", b, 0644)

yaml 输出有 2 个问题。

    应用程序的数组是从 cmets 开始的,它应该 从名字开始

    name 条目之后,kind 属性和之后的所有其他属性都是 未与name对齐

知道如何解决这些问题吗?关于comments 问题,可以说我是从其他财产那里得到的 而不是来自 json(如果它更简单的话)

version: 1
type: verbose
kind: bfr


# my list of applications
applications:
-   #  First app
name: app1
    kind: nodejs
    path: app1
    exec:
        platforms: k8s
        builder: test
-   # test 1
name: app2
    kind: golang
    path: app2
    exec:
        platform: dockerh
        builder: test

【问题讨论】:

我谈到的第一件事是 go-yaml 包的问题。从 yaml.Node 中读取进行编组会产生无效的 yaml。提交了一个问题github.com/go-yaml/yaml/issues/454。 @will7200 - 非常感谢您付出额外的努力!,我将关闭问题作为答案,即使它不起作用,因为问题出在操作系统上,一个问题,是否可以添加一些示例,您将如何从文件中删除一个或两个应用程序? @will7200 - 我已经结束了这个问题并提供了赏金:),如果你能回答我关于从 yaml 文件中删除一个或所有应用程序的最后一个问题,那就太好了。非常感谢! 我添加了一个示例和函数来删除特定的应用程序,您必须提供标识符和值,并且一次只删除一个。如果需要,您可以扩展以一次删除多个。如果要全部删除,请查看我在代码中提供的 cmets。 【参考方案1】:

首先,让我先说 using yaml.Node 在从有效 yaml 编组时不会产生有效 yaml,如下例所示。可能应该提出问题。

package main

import (
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

var (
    sourceYaml = `version: 1
type: verbose
kind : bfr

# my list of applications
applications:

#  First app
  - name: app1
    kind: nodejs
    path: app1
    exec:
      platforms: k8s
      builder: test
`
)

func main() 
    t := yaml.Node

    err := yaml.Unmarshal([]byte(sourceYaml), &t)
    if err != nil 
        log.Fatalf("error: %v", err)
    

    b, err := yaml.Marshal(&t)
    if err != nil 
        log.Fatal(err)
    
    fmt.Println(string(b))

在 go 版本 go1.12.3 windows/amd64 中产生以下无效 yaml

version: 1
type: verbose
kind: bfr


# my list of applications
applications:
-   #  First app
name: app1
    kind: nodejs
    path: app1
    exec:
        platforms: k8s
        builder: test

其次,使用结构如

type VTS struct 
    Version string       `yaml:"version" json:"version"`
    Types   string       `yaml:"type" json:"type"`
    Kind    string       `yaml:"kind,omitempty" json:"kind,omitempty"`
    Apps    yaml.Node `yaml:"applications,omitempty" json:"applications,omitempty"`

从 ubuntu 的博客和源文档看来,它似乎可以正确识别结构中作为节点的字段并单独构建该树,但事实并非如此。 解组时,它将给出正确的节点树,但重新编组时,它将生成以下 yaml,其中包含 yaml.Node 公开的所有字段。可惜我们不能走这条路,必须另寻出路。

version: "1"
type: verbose
kind: bfr
applications:
    kind: 2
    style: 0
    tag: '!!seq'
    value: ""
    anchor: ""
    alias: null
    content:
    -   #  First app
name: app1
        kind: nodejs
        path: app1
        exec:
            platforms: k8s
            builder: test
    headcomment: ""
    linecomment: ""
    footcomment: ""
    line: 9
    column: 3

忽略第一个问题和结构中 yaml.Nodes 的编组错误(在 gopkg.in/yaml.v3 v3.0.0-20190409140830-cdc409dda467 上),我们现在可以开始操作包公开的节点。不幸的是,没有可以轻松添加节点的抽象,因此用途可能会有所不同,识别节点可能会很痛苦。反思在这里可能会有所帮助,所以我把它留给你作为练习。

您会发现注释 spew.Dumps 以一种很好的格式转储整个节点树,这有助于在将节点添加到源树时进行调试。

您当然也可以删除节点,您只需要确定需要删除哪些特定节点。如果父节点是地图或序列,您只需确保删除父节点。

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

var (
    sourceYaml = `version: 1
type: verbose
kind : bfr

# my list of applications
applications:

#  First app
  - name: app1
    kind: nodejs
    path: app1
    exec:
      platforms: k8s
      builder: test
`
    modifyJsonSource = `
[

    
        "comment": "Second app",
        "name": "app2",
        "kind": "golang",
        "path": "app2",
        "exec": 
            "platforms": "dockerh",
            "builder": "test"
        
    
]
`
)

// VTS Need to Make Fields Public otherwise unmarshalling will not fill in the unexported fields.
type VTS struct 
    Version string       `yaml:"version" json:"version"`
    Types   string       `yaml:"type" json:"type"`
    Kind    string       `yaml:"kind,omitempty" json:"kind,omitempty"`
    Apps    Applications `yaml:"applications,omitempty" json:"applications,omitempty"`


type Applications []struct 
    Name string `yaml:"name,omitempty" json:"name,omitempty"`
    Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
    Path string `yaml:"path,omitempty" json:"path,omitempty"`
    Exec struct 
        Platforms string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
        Builder   string `yaml:"builder,omitempty" json:"builder,omitempty"`
     `yaml:"exec,omitempty" json:"exec,omitempty"`
    Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`


func main() 
    t := yaml.Node

    err := yaml.Unmarshal([]byte(sourceYaml), &t)
    if err != nil 
        log.Fatalf("error: %v", err)
    

    // Look for the Map Node with the seq array of items
    applicationNode := iterateNode(&t, "applications")

    // spew.Dump(iterateNode(&t, "applications"))

    var addFromJson Applications
    err = json.Unmarshal([]byte(modifyJsonSource), &addFromJson)
    if err != nil 
        log.Fatalf("error: %v", err)
    

    // Delete the Original Applications the following options:
    // applicationNode.Content = []*yaml.Node
    // deleteAllContents(applicationNode)
    deleteApplication(applicationNode, "name", "app1")


    for _, app := range addFromJson 
        // Build New Map Node for new sequences coming in from json
        mapNode := &yaml.NodeKind: yaml.MappingNode, Tag: "!!map"

        // Build Name, Kind, and Path Nodes
        mapNode.Content = append(mapNode.Content, buildStringNodes("name", app.Name, app.Comment)...)
        mapNode.Content = append(mapNode.Content, buildStringNodes("kind", app.Kind, "")...)
        mapNode.Content = append(mapNode.Content, buildStringNodes("path", app.Path, "")...)

        // Build the Exec Nodes and the Platform and Builder Nodes within it
        keyMapNode, keyMapValuesNode := buildMapNodes("exec")
        keyMapValuesNode.Content = append(keyMapValuesNode.Content, buildStringNodes("platform", app.Exec.Platforms, "")...)
        keyMapValuesNode.Content = append(keyMapValuesNode.Content, buildStringNodes("builder", app.Exec.Builder, "")...)

        // Add to parent map Node
        mapNode.Content = append(mapNode.Content, keyMapNode, keyMapValuesNode)

        // Add to applications Node
        applicationNode.Content = append(applicationNode.Content, mapNode)
    
    // spew.Dump(t)
    b, err := yaml.Marshal(&t)
    if err != nil 
        log.Fatal(err)
    
    fmt.Println(string(b))


// iterateNode will recursive look for the node following the identifier Node,
// as go-yaml has a node for the key and the value itself
// we want to manipulate the value Node
func iterateNode(node *yaml.Node, identifier string) *yaml.Node 
    returnNode := false
    for _, n := range node.Content 
        if n.Value == identifier 
            returnNode = true
            continue
        
        if returnNode 
            return n
        
        if len(n.Content) > 0 
            ac_node := iterateNode(n, identifier)
            if ac_node != nil 
                return ac_node
            
        
    
    return nil


// deleteAllContents will remove all the contents of a node
// Mark sure to pass the correct node in otherwise bad things will happen
func deleteAllContents(node *yaml.Node) 
    node.Content = []*yaml.Node


// deleteApplication expects that a sequence Node with all the applications are present
// if the key value are not found it will not log any errors, and return silently
// this is expecting a map like structure for the applications
func deleteApplication(node *yaml.Node, key, value string) 
    state := -1
    indexRemove := -1
    for index, parentNode := range node.Content 
        for _, childNode := range parentNode.Content 
            if key == childNode.Value && state == -1 
                state += 1
                continue // found expected move onto next
            
            if value == childNode.Value && state == 0 
                state += 1
                indexRemove = index
                break // found the target exit out of the loop
             else if state == 0 
                state = -1
            
        
    
    if state == 1 
        // Remove node from contents
        // node.Content = append(node.Content[:indexRemove], node.Content[indexRemove+1:]...)
        // Don't Do this you might have a potential memory leak source: https://github.com/golang/go/wiki/SliceTricks
        // Since the underlying nodes are pointers
        length := len(node.Content)
        copy(node.Content[indexRemove:], node.Content[indexRemove+1:])
        node.Content[length-1] = nil
        node.Content = node.Content[:length-1]
    



// buildStringNodes builds Nodes for a single key: value instance
func buildStringNodes(key, value, comment string) []*yaml.Node 
    keyNode := &yaml.Node
        Kind:        yaml.ScalarNode,
        Tag:         "!!str",
        Value:       key,
        HeadComment: comment,
    
    valueNode := &yaml.Node
        Kind:  yaml.ScalarNode,
        Tag:   "!!str",
        Value: value,
    
    return []*yaml.NodekeyNode, valueNode


// buildMapNodes builds Nodes for a key: map instance
func buildMapNodes(key string) (*yaml.Node, *yaml.Node) 
    n1, n2 := &yaml.Node
        Kind:  yaml.ScalarNode,
        Tag:   "!!str",
        Value: key,
    , &yaml.NodeKind: yaml.MappingNode,
        Tag: "!!map",
    
    return n1, n2


生成 yaml

version: 1
type: verbose
kind: bfr


# my list of applications
applications:
-   #  First app
name: app1
    kind: nodejs
    path: app1
    exec:
        platforms: k8s
        builder: test
-   # Second app
name: app2
    kind: golang
    path: app2
    exec:
        platform: dockerh
        builder: test

【讨论】:

非常感谢! 1+ 您的解决方案存在一些问题,请查看我的更新【参考方案2】:

您可以创建一个新节点并直接附加到内容,而无需删除先前的节点。下面的例子说明了这一点:

package main

import (
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

var (
    sourceYaml = `version: 1
type: verbose
kind : bfr

# my list of applications
applications:

#  First app
  - name: app1
    kind: nodejs
    path: app1
    exec:
      platforms: k8s
      builder: test
`
)

type Application struct 
    Name string `yaml:"name,omitempty" json:"name,omitempty"`
    Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
    Path string `yaml:"path,omitempty" json:"path,omitempty"`
    Exec struct 
        Platforms string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
        Builder   string `yaml:"builder,omitempty" json:"builder,omitempty"`
     `yaml:"exec,omitempty" json:"exec,omitempty"`


func newApplicationNode(
    name string,
    kind string,
    path string,
    platforms string,
    builder string,
    comment string) (*yaml.Node, error) 

    app := Application
        Name: name,
        Kind: kind,
        Path: path,
        Exec: struct 
            Platforms string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
            Builder   string `yaml:"builder,omitempty" json:"builder,omitempty"`
        platforms, builder,
    
    marshalledApp, err := yaml.Marshal(&app)
    if err != nil 
        return nil, err
    

    node := yaml.Node
    if err := yaml.Unmarshal(marshalledApp, &node); err != nil 
        return nil, err
    
    node.Content[0].HeadComment = comment
    return &node, nil


func main() 
    yamlNode := yaml.Node

    err := yaml.Unmarshal([]byte(sourceYaml), &yamlNode)
    if err != nil 
        log.Fatalf("error: %v", err)
    

    newApp, err := newApplicationNode("app2", "golang", "app2", "dockerh",
        "test", "Second app")
    if err != nil 
        log.Fatalf("error: %v", err)
    

    appIdx := -1
    for i, k := range yamlNode.Content[0].Content 
        if k.Value == "applications" 
            appIdx = i + 1
            break
        
    

    yamlNode.Content[0].Content[appIdx].Content = append(
        yamlNode.Content[0].Content[appIdx].Content, newApp.Content[0])

    out, err := yaml.Marshal(&yamlNode)
    if err != nil 
        log.Fatal(err)
    
    fmt.Println(string(out))


显然,您可以从您的 JSON 中正确解组,而不是像我在 newApplicationNode 中那样采用 hacky 方式。但是,如之前的答案所述,重要的是要注意,键和实际值在Content 内的后续索引中,因此在修改文档时需要考虑到这一点。 (例如,查找 applications 键,然后考虑其内容的下一个索引(在我的示例中为 appIdx = i + 1)。

希望有帮助!

【讨论】:

以上是关于修改现有的 yaml 文件并添加新的数据和注释的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Charles 代理修改 json 有效负载请求并添加新的 json 参数

如何在 C 中修改现有的 YAML 节点?

JAVA并发-为现有的线程安全类添加原子方法

创建新的节点并添加到现有的节点树上

将目标属性添加到 CMake 中现有的导入库

如何为现有的 CakePHP (2) 项目添加新的 url 路由