ruby 读写YAML文件而不破坏锚点和别名?

hivapdat  于 2022-11-22  发布在  Ruby
关注(0)|答案(4)|浏览(202)

我需要打开一个YAML文件,其中使用了别名:

defaults: &defaults
  foo: bar
  zip: button

node:
  <<: *defaults
  foo: other

这显然扩展为一个等效的YAML文档:

defaults:
  foo: bar
  zip: button

node:
  foo: other
  zip: button

YAML::load读起来是这样的。
我需要在这个YAML文档中设置新的键,然后将它写回磁盘,尽可能地保留原始结构。
我看过YAML::Store,但这完全破坏了别名和锚。
是否有任何可用的东西可以沿着如下:

thing = Thing.load("config.yml")
thing[:node][:foo] = "yet another"

将文档重新保存为:

defaults: &defaults
  foo: bar
  zip: button

node:
  <<: *defaults
  foo: yet another


我之所以选择使用YAML,是因为它可以很好地处理别名,但是编写包含别名的YAML在现实中看起来有点乏味。

exdqitrt

exdqitrt1#

使用<<来指示别名Map应该合并到当前Map中不是Yaml核心规范的一部分,但它是part of the tag repository
Ruby - Psych -提供的Yaml库提供了dumpload方法,它们允许Ruby对象的简单序列化和反序列化,并使用标记库中的各种隐式类型转换,包括<<来合并散列。如果需要,它还提供了一些工具来进行更低级的Yaml处理。不幸的是,它没有不要轻易地允许选择性地禁用或启用标记库的特定部分--这是一件全有或全无的事情。特别是<<的处理与哈希的处理非常相似。
一种实现方法是提供你自己的Psych的ToRuby类的子类并覆盖这个方法,这样它就可以把<<的Map键当作文字来处理。这涉及到覆盖Psych中的一个私有方法,所以你需要小心一点:

require 'psych'

class ToRubyNoMerge < Psych::Visitors::ToRuby
  def revive_hash hash, o
    @st[o.anchor] = hash if o.anchor

    o.children.each_slice(2) { |k,v|
      key = accept(k)
      hash[key] = accept(v)
    }
    hash
  end
end

然后,您可以像这样使用它:

tree = Psych.parse your_data
data = ToRubyNoMerge.new.accept tree

使用示例中的Yaml,data将如下所示

{"defaults"=>{"foo"=>"bar", "zip"=>"button"},
 "node"=>{"<<"=>{"foo"=>"bar", "zip"=>"button"}, "foo"=>"other"}}

注意,<<是一个文本键。另外,data["defaults"]键下的哈希值与data["node"]["<<"]键下的哈希值 * 相同 *,即它们具有相同的object_id。现在,您可以根据需要操作数据,当您将其写成Yaml时,锚点和别名仍然保持不变,尽管锚点名称已经更改:

data['node']['foo'] = "yet another"
puts Yaml.dump data

产生(Psych使用散列的object_id来确保唯一的锚名称(当前版本的Psych现在使用序列号而不是object_id)):

---
defaults: &2151922820
  foo: bar
  zip: button
node:
  <<: *2151922820
  foo: yet another

如果你想控制锚名称,你可以提供自己的Psych::Visitors::Emitter。下面是一个基于你的例子的简单例子,假设只有一个锚点:

class MyEmitter < Psych::Visitors::Emitter
  def visit_Psych_Nodes_Mapping o
    o.anchor = 'defaults' if o.anchor
    super
  end

  def visit_Psych_Nodes_Alias o
    o.anchor = 'defaults' if o.anchor
    super
  end
end

当与上面修改过的data哈希一起使用时:

#create an AST based on the Ruby data structure
builder = Psych::Visitors::YAMLTree.new
builder << data
ast = builder.tree

# write out the tree using the custom emitter
MyEmitter.new($stdout).accept ast

输出为:

---
defaults: &defaults
  foo: bar
  zip: button
node:
  <<: *defaults
  foo: yet another

(* 更新:* another question询问如何对多个锚执行此操作,我提出了possibly better way to keep anchor names when serializing。)

qvtsj1bj

qvtsj1bj2#

YAML有别名,它们可以往返,但是您可以通过哈希合并禁用它。<<作为Map键似乎是YAML的非标准扩展(在1.8的syck和1.9的psych中都有)。

require 'rubygems'
require 'yaml'

yaml = <<EOS
defaults: &defaults
  foo: bar
  zip: button

node: *defaults
EOS

data = YAML.load yaml
print data.to_yaml

印刷品

--- 
defaults: &id001 
  zip: button
  foo: bar
node: *id001

但是数据中的<<将别名哈希合并为一个不再是别名的新哈希。

1rhkuytd

1rhkuytd3#

你试过Psych吗?又一个心理问题。

jei2mxaa

jei2mxaa4#

我用Ruby脚本和ERB模板生成了我的CircleCI配置文件。我的脚本解析并重新生成YAML,所以我想保留所有的锚点。配置中的锚点都与定义它们的键同名,例如:

docker_images:
  docker_auth: &docker_auth
    username: '$DOCKERHUB_USERNAME'
    password: '$DOCKERHUB_TOKEN'
  cimg_base_image: &cimg_base_image
    image: cimg/base:2022.09
    auth: *docker_auth
jobs:
  tests:
    docker:
      - *cimg_ruby_image

所以我可以在生成的YAML字符串上使用正则表达式来解决这个问题。它写了一个#restore_yaml_anchors方法,将&1*1转换回&docker_auth*docker_auth

# Ruby 3.1.2
require 'rubygems'
require 'yaml'

yaml = <<EOS
docker_images:
  docker_auth: &docker_auth
    username: '$DOCKERHUB_USERNAME'
    password: '$DOCKERHUB_TOKEN'
  cimg_base_image: &cimg_base_image
    image: cimg/base:2022.09
    auth: *docker_auth
jobs:
  tests:
    docker:
      - *cimg_base_image
EOS

data = YAML.load yaml, aliases: true # needed for Ruby 3.x

def restore_yaml_anchors(yaml)
  yaml.scan(/([A-Z0-9a-z_]+|<<): &(\d+)/).each do |anchor_name, anchor_id|
    yaml.gsub!(/([:-]) (\*|&)#{anchor_id}/, "\\1 \\2#{anchor_name}")
  end
  yaml
end

puts [
  "Original #to_yaml:",
  data.to_yaml,
  "-----------------------", '',
  "With restored anchors:",
  restore_yaml_anchors(data.to_yaml)
].join("\n")

输出量:

Original #to_yaml:
---
docker_images:
  docker_auth: &1
    username: "$DOCKERHUB_USERNAME"
    password: "$DOCKERHUB_TOKEN"
  cimg_base_image: &2
    image: cimg/base:2022.09
    auth: *1
jobs:
  tests:
    docker:
    - *2

-----------------------

With restored anchors:
---
docker_images:
  docker_auth: &docker_auth
    username: "$DOCKERHUB_USERNAME"
    password: "$DOCKERHUB_TOKEN"
  cimg_base_image: &cimg_base_image
    image: cimg/base:2022.09
    auth: *docker_auth
jobs:
  tests:
    docker:
    - *cimg_base_image

它对我的CI配置工作得很好,但是您可能需要更新它以处理您自己的YAML中的一些其他情况。

相关问题