如何从同一 YAML 文件中的其他位置引用 YAML“设置”?

问题描述 投票:0回答:10

我有以下 YAML 内容:

paths:
  patha: /path/to/root/a
  pathb: /path/to/root/b
  pathc: /path/to/root/c

如何通过从三个路径中删除

/path/to/root/
来“规范化”这一点,并将其作为自己的设置,例如:

paths:
  root: /path/to/root/
  patha: *root* + a
  pathb: *root* + b
  pathc: *root* + c

显然这是无效的;我刚刚弥补了。真正的语法是什么?可以做吗?

syntax yaml template-engine transclusion
10个回答
203
投票

我认为这是不可能的。您可以重复使用“节点”,但不能重复使用它的一部分。

bill-to: &id001
    given  : Chris
    family : Dumars
ship-to: *id001

这是完全有效的 YAML,字段

given
family
ship-to
块中重用。您可以以相同的方式重用标量节点,但无法更改内部内容并从 YAML 内部添加路径的最后一部分。

如果重复让您感到困扰,我建议让您的应用程序了解

root
属性并将其添加到每个看起来相对而不是绝对的路径。


105
投票

是的,使用自定义标签。 Python 中的示例,使

!join
标签将字符串连接到数组中:

import yaml

## define custom tag handler
def join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])

## register the tag handler
yaml.add_constructor('!join', join)

## using your sample data
yaml.load("""
paths:
    root: &BASE /path/to/root/
    patha: !join [*BASE, a]
    pathb: !join [*BASE, b]
    pathc: !join [*BASE, c]
""")

结果是:

{
    'paths': {
        'patha': '/path/to/root/a',
        'pathb': '/path/to/root/b',
        'pathc': '/path/to/root/c',
        'root': '/path/to/root/'
     }
}

!join
的参数数组可以包含任何数据类型的任意数量的元素,只要它们可以转换为字符串,因此
!join [*a, "/", *b, "/", *c]
可以实现您所期望的功能。


32
投票

另一种看待这个问题的方法是简单地使用另一个字段。

paths:
  root_path: &root
     val: /path/to/root/
  patha: &a
    root_path: *root
    rel_path: a
  pathb: &b
    root_path: *root
    rel_path: b
  pathc: &c
    root_path: *root
    rel_path: c

8
投票

我创建了一个可在 Packagist 上使用的库,它执行此功能: https://packagist.org/packages/grasmash/yaml-expander

示例 YAML 文件:

type: book
book:
  title: Dune
  author: Frank Herbert
  copyright: ${book.author} 1965
  protaganist: ${characters.0.name}
  media:
    - hardcover
characters:
  - name: Paul Atreides
    occupation: Kwisatz Haderach
    aliases:
      - Usul
      - Muad'Dib
      - The Preacher
  - name: Duncan Idaho
    occupation: Swordmaster
summary: ${book.title} by ${book.author}
product-name: ${${type}.title}

逻辑示例:

// Parse a yaml string directly, expanding internal property references.
$yaml_string = file_get_contents("dune.yml");
$expanded = \Grasmash\YamlExpander\Expander::parse($yaml_string);
print_r($expanded);

结果数组:

array (
  'type' => 'book',
  'book' => 
  array (
    'title' => 'Dune',
    'author' => 'Frank Herbert',
    'copyright' => 'Frank Herbert 1965',
    'protaganist' => 'Paul Atreides',
    'media' => 
    array (
      0 => 'hardcover',
    ),
  ),
  'characters' => 
  array (
    0 => 
    array (
      'name' => 'Paul Atreides',
      'occupation' => 'Kwisatz Haderach',
      'aliases' => 
      array (
        0 => 'Usul',
        1 => 'Muad\'Dib',
        2 => 'The Preacher',
      ),
    ),
    1 => 
    array (
      'name' => 'Duncan Idaho',
      'occupation' => 'Swordmaster',
    ),
  ),
  'summary' => 'Dune by Frank Herbert',
);

5
投票

在某些语言中,您可以使用替代库,例如,tampax是YAML处理变量的实现:

const tampax = require('tampax');

const yamlString = `
dude:
  name: Arthur
weapon:
  favorite: Excalibur
  useless: knife
sentence: "{{dude.name}} use {{weapon.favorite}}. The goal is {{goal}}."`;

const r = tampax.yamlParseString(yamlString, { goal: 'to kill Mordred' });
console.log(r.sentence);

// output : "Arthur use Excalibur. The goal is to kill Mordred."

编者注:海报者也是此包的作者。


4
投票

YML定义:

dir:
  default: /home/data/in/
  proj1: ${dir.default}p1
  proj2: ${dir.default}p2
  proj3: ${dir.default}p3 

在百里香叶的某个地方

<p th:utext='${@environment.getProperty("dir.default")}' />
<p th:utext='${@environment.getProperty("dir.proj1")}' /> 

输出: /主页/数据/中/ /home/data/in/p1


3
投票

使用 Yglu,您可以将示例编写为:

paths:
  root: /path/to/root/
  patha: !? .paths.root + a
  pathb: !? .paths.root + b
  pathc: !? .paths.root + c

免责声明:我是 Yglu 的作者。


3
投票

使用 OmegaConf

OmegaConf 是一个基于 YAML 的分层配置系统,在“变量插值”功能下对此提供支持。使用 OmegaConf v2.2.2: 创建YAML文件

paths.yaml

如下:

paths:
  root: /path/to/root/
  patha: ${.root}a
  pathb: ${.root}b
  pathc: ${.root}c

然后我们就可以读取带有可变路径的文件了:

from omegaconf import OmegaConf conf = OmegaConf.load("test_paths.yaml") >>> conf.paths.root '/path/to/root/' >>> conf.paths.patha '/path/to/root/a' >>> conf.paths.pathb '/path/to/root/b' >>> conf.paths.pathc '/path/to/root/c'

深度和交叉参考

可以使用变量相对于其他变量的相对深度来定义更复杂(嵌套)的结构:

创建另一个文件

nested_paths.yaml

:

data:
    base: data
    sub_dir_A:
        name: a
        # here we note that `base` is two levels above this variable
        # hence we will use `..base` two dots but the `name` variable is
        # at the same level hence a single dot `.name`
        nested_dir: ${..base}/sub_dir/${.name}/last_dir 
    sub_dir_B:
        # add another level of depth
        - name: b
          # due to another level of depth, we have to use three dots
          # to access `base` variable as `...base`
          nested_file: ${...base}/sub_dir/${.name}/dirs.txt
        - name: c
          # we can also make cross-references to other variables
          cross_ref_dir: ${...sub_dir_A.nested_dir}/${.name}

我们可以再次检查:

conf = OmegaConf.load("nested_paths.yaml") # 1-level of depth reference >>> conf.data.sub_dir_A.nested_dir 'data/sub_dir/a/last_dir' # 2-levels of depth reference >>> conf.data.sub_dir_B[0].nested_file 'data/sub_dir/b/dirs.txt' # cross-reference example >>> conf.data.sub_dir_B[1].cross_ref_dir 'data/sub_dir/a/last_dir/c'

如果引用无效(例如错误的深度、错误的变量名称),OmegaConf 将抛出错误
omegaconf.errors.InterpolationResolutionError

。它还在

Hydra
中用于配置复杂的应用程序。


2
投票

,因为您选择了保留字符来启动标量。如果您将 * 替换为其他一些非保留字符(我倾向于使用非 ASCII 字符,因为它们很少用作某些规范的一部分),您最终会得到完全合法的 YAML:


paths: root: /path/to/root/ patha: ♦root♦ + a pathb: ♦root♦ + b pathc: ♦root♦ + c

这将加载到解析器使用的语言中的映射的标准表示形式中,并且不会神奇地扩展任何内容。
为此,请使用本地默认对象类型,如以下 Python 程序所示:


# coding: utf-8 from __future__ import print_function import ruamel.yaml as yaml class Paths: def __init__(self): self.d = {} def __repr__(self): return repr(self.d).replace('ordereddict', 'Paths') @staticmethod def __yaml_in__(loader, data): result = Paths() loader.construct_mapping(data, result.d) return result @staticmethod def __yaml_out__(dumper, self): return dumper.represent_mapping('!Paths', self.d) def __getitem__(self, key): res = self.d[key] return self.expand(res) def expand(self, res): try: before, rest = res.split(u'♦', 1) kw, rest = rest.split(u'♦ +', 1) rest = rest.lstrip() # strip any spaces after "+" # the lookup will throw the correct keyerror if kw is not found # recursive call expand() on the tail if there are multiple # parts to replace return before + self.d[kw] + self.expand(rest) except ValueError: return res yaml_str = """\ paths: !Paths root: /path/to/root/ patha: ♦root♦ + a pathb: ♦root♦ + b pathc: ♦root♦ + c """ loader = yaml.RoundTripLoader loader.add_constructor('!Paths', Paths.__yaml_in__) paths = yaml.load(yaml_str, Loader=yaml.RoundTripLoader)['paths'] for k in ['root', 'pathc']: print(u'{} -> {}'.format(k, paths[k]))

将打印:

root -> /path/to/root/ pathc -> /path/to/root/c

扩展是动态完成的并处理嵌套定义,但您必须小心不要调用无限递归。

通过指定转储器,您可以从加载的数据中转储原始 YAML,因为动态扩展:

dumper = yaml.RoundTripDumper dumper.add_representer(Paths, Paths.__yaml_out__) print(yaml.dump(paths, Dumper=dumper, allow_unicode=True))

这将更改映射键顺序。如果这是你遇到的问题
将 
self.d

变成

CommentedMap
(从
ruamel.yaml.comments.py
导入)
    


0
投票

/root | +- /proj1 | +- config.yaml | +- /proj2 | +- config.yaml | ... and so on ...

这里的关键区别在于,只有在加载所有 
config.yaml

文件之后才必须应用扩展,其中下一个文件中的变量可以覆盖前一个文件中的变量,因此伪代码应如下所示:

env = YamlEnv()
env.load('/root/proj1/config.yaml')
env.load('/root/proj1/proj2/config.yaml')
...
env.expand()

作为附加选项,
xonsh

脚本可以将结果变量导出到环境变量中(请参阅

yaml_update_global_vars
函数)。
脚本:

https://github.com/andry81/tacklelib/tree/HEAD/python/cmdoplib/cmdoplib.yaml.xsh

https://github.com/andry81/tacklelib/tree/HEAD/python/tacklelib/tacklelib.yaml.py

优点

简单,不支持递归和嵌套变量
  • 可以将未定义的变量替换为占位符 (
  • ${MYUNDEFINEDVAR}
  • ->
    *$/{MYUNDEFINEDVAR}
    )
    可以扩展环境变量的引用(
  • ${env:MYVAR}
  • )
    可以将路径变量中的所有 
  • \\
  • 替换为
    /
    (
    ${env:MYVAR:path}
    )
    
    
缺点

不支持嵌套变量,因此无法扩展嵌套字典中的值(未实现类似
    ${MYSCOPE.MYVAR}
  • 的功能)
    不检测扩展递归,包括占位符放置后的递归
© www.soinside.com 2019 - 2024. All rights reserved.