comment référencer un «paramètre» YAML ailleurs dans le même fichier YAML?

146

J'ai le YAML suivant:

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

Comment puis-je "normaliser" cela, en supprimant /path/to/root/des trois chemins, et en le définissant comme son propre paramètre, quelque chose comme:

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

Évidemment, c'est invalide, je viens de l'inventer. Quelle est la vraie syntaxe? Cela peut-il être fait?

Andrew Bullock
la source
1
Voir aussi: stackoverflow.com/a/41620747/42223
dreftymac

Réponses:

127

Je ne pense pas que ce soit possible. Vous pouvez réutiliser le "nœud" mais pas une partie de celui-ci.

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

C'est YAML et les champs parfaitement valides givenet familysont réutilisés en ship-tobloc. Vous pouvez réutiliser un nœud scalaire de la même manière, mais il n'y a aucun moyen de changer ce qu'il y a à l'intérieur et d'y ajouter cette dernière partie d'un chemin depuis l'intérieur de YAML.

Si la répétition vous dérange autant, je suggère de rendre votre application consciente de la rootpropriété et de l'ajouter à chaque chemin qui semble relatif et non absolu.

vava
la source
1
Ok merci, ouais je dois ajouter le rootcode. pas grand chose.
Andrew Bullock
2
La réponse acceptée n'est pas exacte. Voir ma réponse pour une solution.
Chris Johnson
comment faire cela, si la facturation est dans un autre fichier, que nous avons importé là où la livraison est définie?
Prateek Jain
@PrateekJain: si vous avez affaire à plusieurs fichiers, vous ferez probablement de mieux pour évaluer une bibliothèque d'amélioration YAML autonome, comme celle répertoriée ici. github.com/dreftymac/dynamic.yaml/blob/master/…
dreftymac
1
Voir l'exemple 2.9 dans yaml.org/spec/1.2/spec.html ; on peut également faire référence à des scalaires, ce qui est génial
akostadinov
72

Oui, en utilisant des balises personnalisées. Exemple en Python, permettant à la !joinbalise de joindre des chaînes dans un tableau:

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

Ce qui se traduit par:

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

Le tableau d'arguments de !joinpeut avoir n'importe quel nombre d'éléments de n'importe quel type de données, tant qu'ils peuvent être convertis en chaîne, il en !join [*a, "/", *b, "/", *c]va de même pour ce que vous attendez.

Chris Johnson
la source
2
J'aime votre solution, plus simple en codage que la mienne au prix d'un YAML un peu moins lisible.
Anthon
7
Cette réponse mérite plus de votes positifs. C'est techniquement la réponse la plus précise selon la spécification YAML. Il y a une mise en garde, cependant, conformément aux implémentations YAML réelles , il y en a peu qui implémentent réellement la spécification YAML complète. Le pyyaml ​​de Python est au-dessus de beaucoup d'autres en termes d'uniformité avec la spécification.
dreftymac
5
La question semble être de référencer une valeur dans un fichier yaml. Ajouter une autre couche de code autour d'elle ne serait pas ma solution préférée.
user2020056
1
@ChrisJohnson Merci pour cette réponse, je me demandais si vous aviez un document de référence qui listait cette syntaxe. J'ai vu les spécifications YAML expliquées à plusieurs endroits sur le Web, donc je veux juste m'assurer que je regarde la même référence que vous. Merci!
user5359531
3
Cette solution n'a pas fonctionné pour moi ( python3?) Cependant, avec une simple modification de ce qui précède, cela fonctionne comme prévu. Plus précisément:yaml.SafeLoader.add_constructor(tag='!join', constructor=join) yaml.load(open(fpth, mode='r'), Loader=yaml.SafeLoader)
benjaminmgross
20

Une autre façon de voir cela est d'utiliser simplement un autre champ.

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
la source
5

Définition YML:

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

Quelque part dans la feuille de thym

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

Sortie: / home / data / in / / home / data / in / p1

Pavol
la source
@AndrewBullock Je pense que cela devrait être la réponse acceptée, car elle résout exactement votre problème.
Honza Zidek
5
Non, ce n'est pas une utilisation native de variable dans YAML et il n'est spécifié dans aucune version de spécification. Après quelques tests, cela ne fonctionne pas.
Arthur Lacoste
2
Cela a probablement fonctionné pour Pavol en utilisant quelque chose qui a prétraité le yaml (c'est-à-dire le filtrage
maven
1
Pas standard Yaml
Dan Niero
3

J'ai créé une bibliothèque, disponible sur Packagist, qui remplit cette fonction: https://packagist.org/packages/grasmash/yaml-expander

Exemple de fichier 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}

Exemple de logique:

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

Tableau résultant:

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
la source
J'adore le concept d'expandeur!
Guillaume Roderick
2

Dans certains langages, vous pouvez utiliser une bibliothèque alternative.Par exemple, tampax est une implémentation de variables de gestion 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
la source
1

Votre exemple n'est pas valide uniquement parce que vous avez choisi un caractère réservé pour commencer vos scalaires. Si vous remplacez le *par un autre caractère non réservé (j'ai tendance à utiliser des caractères non ASCII pour cela car ils sont rarement utilisés dans le cadre de certaines spécifications), vous vous retrouvez avec un YAML parfaitement légal:

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

Cela se chargera dans la représentation standard des mappages dans le langage utilisé par votre parseur et n'étend rien par magie.
Pour ce faire, utilisez un type d'objet par défaut localement comme dans le programme Python suivant:

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

qui imprimera:

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

Le développement se fait à la volée et gère les définitions imbriquées, mais vous devez faire attention à ne pas invoquer une récursivité infinie.

En spécifiant le dumper, vous pouvez vider le YAML d'origine à partir des données chargées, en raison de l'extension à la volée:

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

cela changera l'ordre des clés de mappage. Si cela pose un problème, vous devez créer self.dun CommentedMap(importé de ruamel.yaml.comments.py)

Anthon
la source
0

J'ai écrit ma propre bibliothèque sur Python pour développer les variables chargées à partir de répertoires avec une hiérarchie comme:

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

La principale différence ici est que l'expansion doit être appliquée uniquement après le config.yamlchargement de tous les fichiers, où les variables du fichier suivant peuvent remplacer les variables du précédent, le pseudocode devrait donc ressembler à ceci:

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

Comme option supplémentaire, le xonshscript peut exporter les variables résultantes dans des variables d'environnement (voir la yaml_update_global_varsfonction).

Les scripts:

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

Avantages :

  • simple, ne prend pas en charge la récursivité et les variables imbriquées
  • peut remplacer une variable non définie par un espace réservé ( ${MYUNDEFINEDVAR}-> *$/{MYUNDEFINEDVAR})
  • peut développer une référence à partir de la variable d'environnement ( ${env:MYVAR})
  • peut remplacer tous \\à /une variable de chemin ( ${env:MYVAR:path})

Inconvénients :

  • ne prend pas en charge les variables imbriquées, donc ne peut pas étendre les valeurs dans les dictionnaires imbriqués (quelque chose comme ${MYSCOPE.MYVAR}n'est pas implémenté)
  • ne détecte pas la récursivité d'expansion, y compris la récursivité après qu'un espace réservé a mis
Andry
la source
0

Avec Yglu , vous pouvez écrire votre exemple comme suit :

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

Avertissement: je suis l'auteur de Yglu.

lbovet
la source
Il est bon d'être conscient d'une bibliothèque qui ajoute cette fonctionnalité en plus de YAML
Dhiraj