¿Cómo hacer referencia a una "configuración" YAML de otra parte del mismo archivo YAML?

146

Tengo el siguiente YAML:

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

¿Cómo puedo "normalizar" esto, quitando /path/to/root/de los tres caminos, y tenerlo como su propia configuración, algo como:

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

Obviamente eso no es válido, lo acabo de inventar. ¿Cuál es la sintaxis real? Se puede hacer?

Andrew Bullock
fuente
1
Ver también: stackoverflow.com/a/41620747/42223
dreftymac el

Respuestas:

127

No creo que sea posible. Puede reutilizar "nodo" pero no parte de él.

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

Este es YAML y campos perfectamente válidos giveny familyse reutilizan en ship-tobloque. Puede reutilizar un nodo escalar de la misma manera, pero no hay forma de cambiar lo que hay dentro y agregarle la última parte de una ruta desde dentro de YAML.

Si la repetición le molesta tanto, sugiero que su aplicación conozca la rootpropiedad y la agregue a cada ruta que parezca relativa, no absoluta.

vava
fuente
1
Ok, gracias, sí, tendré que anteponer el rootcódigo. No es gran cosa.
Andrew Bullock
2
La respuesta aceptada no es precisa. Vea mi respuesta para una solución.
Chris Johnson
¿Cómo hacer esto, si bill-to está en otro archivo, que hemos importado donde se define el envío ?
Prateek Jain
@PrateekJain: si se trata de varios archivos, probablemente lo mejor sea evaluar una biblioteca de mejora de YAML independiente, como la que se muestra aquí. github.com/dreftymac/dynamic.yaml/blob/master/…
dreftymac
1
Ver ejemplo 2.9 en yaml.org/spec/1.2/spec.html ; uno también puede hacer referencia a escalares, lo cual es increíble
akostadinov
72

Sí, usando etiquetas personalizadas. Ejemplo en Python, haciendo que la !joinetiqueta una las cadenas en una matriz:

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]
""")

Lo que resulta en:

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

La matriz de argumentos !joinpuede tener cualquier cantidad de elementos de cualquier tipo de datos, siempre y cuando se puedan convertir en una cadena, así !join [*a, "/", *b, "/", *c]es lo que cabría esperar.

Chris Johnson
fuente
2
Me gusta su solución, más simple en la codificación que la mía a costa de un YAML ligeramente menos legible.
Anthon
77
Esta respuesta merece más votos positivos. Es técnicamente la respuesta más precisa de acuerdo con la especificación YAML. Hay una advertencia, sin embargo, de acuerdo con las implementaciones reales de YAML , hay pocas que realmente implementen la especificación completa de YAML. Pyyaml ​​de Python está por encima y más allá de muchos otros en términos de su uniformidad con la especificación.
dreftymac
55
La pregunta parece ser sobre hacer referencia a un valor EN un archivo yaml. Agregar otra capa de código a su alrededor no sería mi solución preferida.
user2020056
1
@ ChrisJohnson Gracias por esta respuesta, me preguntaba si tenía un documento de referencia que enumerara esta sintaxis. He visto explicaciones de YAML explicadas en varios lugares en la web, así que solo quiero asegurarme de que estoy viendo la misma referencia que tú. ¡Gracias!
user5359531
3
Esta solución no funcionó para mí ( python3?) Sin embargo, con una simple modificación a lo anterior, funciona como se esperaba. Específicamente:yaml.SafeLoader.add_constructor(tag='!join', constructor=join) yaml.load(open(fpth, mode='r'), Loader=yaml.SafeLoader)
benjaminmgross
20

Otra forma de ver esto es simplemente usar otro campo.

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
Brian Bruggeman
fuente
5

Definición YML:

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

En algún lugar de la hoja de tomillo

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

Salida: / home / data / in / / home / data / in / p1

Pavol
fuente
@ AndrewBullock Creo que esta debería ser la respuesta aceptada, ya que resuelve exactamente su problema.
Honza Zidek
55
No, no es un uso nativo de variable en YAML y no se especifica en ninguna versión de especificación. Después de alguna prueba, esto no funciona.
Arthur Lacoste
2
Esto probablemente trabajó para Pavol el uso de algo que el pre-procesado el yaml (es decir, maven-recursos-plugin filtrar)
DeezCashews
1
No estándar Yaml
Dan Niero
3

He creado una biblioteca, disponible en Packagist, que realiza esta función: https://packagist.org/packages/grasmash/yaml-expander

Ejemplo de archivo 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}

Lógica de ejemplo:

// 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);

Matriz resultante:

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',
);
grasmash
fuente
¡Me encanta el concepto de expansor!
Guillaume Roderick
2

En algunos idiomas, puede usar una biblioteca alternativa. Por ejemplo, tampax es una implementación de variables de manejo de 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."
Arthur Lacoste
fuente
1

Que su ejemplo no sea válido es solo porque eligió un carácter reservado para comenzar con sus escalares. Si reemplaza el *con algún otro carácter no reservado (tiendo a usar caracteres no ASCII para eso, ya que rara vez se usan como parte de alguna especificación), termina con YAML perfectamente legal:

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

Esto se cargará en la representación estándar para las asignaciones en el lenguaje que usa su analizador y no expande mágicamente nada.
Para hacerlo, use un tipo de objeto predeterminado localmente como en el siguiente programa 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]))

que imprimirá:

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

La expansión se realiza sobre la marcha y maneja definiciones anidadas, pero debe tener cuidado de no invocar una recursión infinita.

Al especificar el volquete, puede volcar el YAML original de los datos cargados, debido a la expansión sobre la marcha:

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

esto cambiará el orden de las claves de mapeo. Si eso es un problema, debe hacer self.dun CommentedMap(importado de ruamel.yaml.comments.py)

Anthon
fuente
0

He escrito mi propia biblioteca en Python para expandir las variables que se cargan desde directorios con una jerarquía como:

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

La diferencia clave aquí es que la expansión debe aplicarse solo después de config.yamlcargar todos los archivos, donde las variables del siguiente archivo pueden anular las variables del anterior, por lo que el pseudocódigo debería tener este aspecto:

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

Como una opción adicional, el xonshscript puede exportar las variables resultantes a variables de entorno (vea la yaml_update_global_varsfunción).

Los guiones:

https://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts/Tools/cmdoplib.yaml.py https://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts /Tools/cmdoplib.yaml.xsh

Pros :

  • simple, no admite recursividad y variables anidadas
  • puede reemplazar una variable indefinida a un marcador de posición ( ${MYUNDEFINEDVAR}-> *$/{MYUNDEFINEDVAR})
  • puede expandir una referencia desde la variable de entorno ( ${env:MYVAR})
  • puede sustituir a todos los \\que /en una variable de ruta ( ${env:MYVAR:path})

Contras :

  • no admite variables anidadas, por lo que no puede expandir valores en diccionarios anidados (algo así como ${MYSCOPE.MYVAR}no está implementado)
  • no detecta la recursividad de expansión, incluida la recursividad después de un marcador de posición
Andry
fuente
0

Con Yglu , puedes escribir tu ejemplo como:

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

Descargo de responsabilidad: soy el autor de Yglu.

lbovet
fuente
Es bueno estar al tanto de una biblioteca que agrega esta funcionalidad además de YAML
Dhiraj