Comment fusionner des tableaux YAML?

113

Je voudrais fusionner des tableaux dans YAML et les charger via ruby ​​-

some_stuff: &some_stuff
 - a
 - b
 - c

combined_stuff:
  <<: *some_stuff
  - d
  - e
  - f

J'aimerais avoir le tableau combiné comme [a,b,c,d,e,f]

Je reçois l'erreur: je n'ai pas trouvé la clé attendue lors de l'analyse d'un mappage de bloc

Comment fusionner des tableaux dans YAML?

lfender6445
la source
6
Pourquoi voulez-vous faire cela en YAML plutôt que dans la langue avec laquelle vous l'analysez?
Patrick Collins
7
pour sécher la duplication dans un très gros fichier yaml
lfender6445
4
C'est une très mauvaise pratique. Vous devriez lire les yamls séparément, mettre les tableaux ensemble dans Ruby, puis les réécrire dans yaml.
sawa
74
Comment essaie d'être une mauvaise pratique sèche?
krak3n
13
@PatrickCollins J'ai trouvé cette question en essayant de réduire la duplication dans mon fichier .gitlab-ci.yml et malheureusement je n'ai aucun contrôle sur l'analyseur que GitLab CI utilise :(
rink.attendant.6

Réponses:

41

Si le but est d'exécuter une séquence de commandes shell, vous pourrez peut-être y parvenir comme suit:

# note: no dash before commands
some_stuff: &some_stuff |-
    a
    b
    c

combined_stuff:
  - *some_stuff
  - d
  - e
  - f

Cela équivaut à:

some_stuff: "a\nb\nc"

combined_stuff:
  - "a\nb\nc"
  - d
  - e
  - f

J'ai utilisé ceci sur mon gitlab-ci.yml(pour répondre à @ rink.attendant.6 commentaire sur la question).


Exemple de travail que nous utilisons pour prendre en charge requirements.txtles dépôts privés depuis gitlab:

.pip_git: &pip_git
- git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com".insteadOf "ssh://[email protected]"
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts

test:
    image: python:3.7.3
    stage: test
    script:
        - *pip_git
        - pip install -q -r requirements_test.txt
        - python -m unittest discover tests

use the same `*pip_git` on e.g. build image...

requirements_test.txtcontient par exemple

-e git+ssh://[email protected]/example/[email protected]#egg=example

Jorge Leitao
la source
3
Intelligent. Je l'utilise maintenant dans notre pipeline Bitbucket. Merci
Dariop
* Le tiret de fin n'est pas nécessaire ici, seul le tuyau à la fin suffit. * C'est une solution inférieure car lorsque le travail échoue sur une très longue instruction multiligne, on ne sait pas quelle commande a échoué.
Mina Luke
1
@MinaLuke, inférieur par rapport à quoi? Aucune des réponses actuelles ne permet de fusionner deux éléments en utilisant uniquement yaml ... De plus, rien dans la question n'indique que l'OP souhaite l'utiliser dans CI / CD. Enfin, lorsque cela est utilisé dans CI / CD, la journalisation dépend uniquement du CI / CD particulier utilisé, pas de la déclaration yaml. Donc, le cas échéant, le CI / CD auquel vous faites référence est celui qui fait un mauvais travail. Le yaml dans cette réponse est valide et résout le problème d'OP.
Jorge Leitao
@JorgeLeitao Je suppose que vous l'utilisez pour combiner des règles. Pouvez-vous fournir un exemple de gitlabci fonctionnel? J'ai essayé quelque chose en fonction de votre solution, mais j'obtiens toujours une erreur de validation.
niels
@niels, j'ai ajouté un exemple avec un exemple de travail gitlabci. Notez que certains IDE marquent ce yaml comme invalide, même s'il ne l'est pas.
Jorge Leitao
26

Mise à jour: 2019-07-01 14:06:12

  • Remarque : une autre réponse à cette question a été substantiellement modifiée avec une mise à jour sur les approches alternatives .
    • Cette réponse mise à jour mentionne une alternative à la solution de contournement dans cette réponse. Il a été ajouté à la section Voir aussi ci-dessous.

Le contexte

Cet article suppose le contexte suivant:

  • python 2.7
  • analyseur Python YAML

Problème

lfender6445 souhaite fusionner deux ou plusieurs listes dans un fichier YAML, et faire apparaître ces listes fusionnées comme une seule liste singulière une fois analysées.

Solution (solution de contournement)

Cela peut être obtenu simplement en attribuant des ancres YAML aux mappages, où les listes souhaitées apparaissent comme des éléments enfants des mappages. Il y a cependant des réserves à cela (voir "Pièges" ci-dessous).

Dans l'exemple ci-dessous, nous avons trois mappages ( list_one, list_two, list_three) et trois ancres et alias qui font référence à ces mappages le cas échéant.

Lorsque le fichier YAML est chargé dans le programme, nous obtenons la liste souhaitée, mais elle peut nécessiter une petite modification après le chargement (voir les pièges ci-dessous).

Exemple

Fichier YAML d'origine

  list_one: & id001
   - une
   - b
   - c

  list_two: & id002
   - e
   - F
   - g

  list_three: & id003
   - h
   - je
   - j

  list_combined:
      - * id001
      - * id002
      - * id003

Résultat après YAML.safe_load

## list_combined
  [
    [
      "une",
      "b",
      "c"
    ],
    [
      "e",
      "F",
      "g"
    ],
    [
      "h",
      "je",
      "j"
    ]
  ]

Pièges

  • cette approche produit une liste imbriquée de listes, qui peut ne pas être la sortie exacte souhaitée, mais cela peut être post-traité à l'aide de la méthode flatten
  • les mises en garde habituelles aux ancres et alias YAML s'appliquent pour l'unicité et l'ordre des déclarations

Conclusion

Cette approche permet la création de listes fusionnées en utilisant la fonction d'alias et d'ancrage de YAML.

Bien que le résultat de sortie soit une liste imbriquée de listes, celle-ci peut être facilement transformée à l'aide de la flattenméthode.

Voir également

Mise à jour de l'approche alternative par @Anthon

Exemples de flattenméthode

dreftymac
la source
21

Ça ne va pas marcher:

  1. la fusion n'est prise en charge que par les spécifications YAML pour les mappages et non pour les séquences

  2. vous mélangez complètement les choses en ayant une clé de fusion << suivie du séparateur clé / valeur :et une valeur qui est une référence , puis continuez avec une liste au même niveau d'indentation

Ce n'est pas correct YAML:

combine_stuff:
  x: 1
  - a
  - b

Ainsi, votre exemple de syntaxe n'aurait même pas de sens en tant que proposition d'extension YAML.

Si vous souhaitez faire quelque chose comme la fusion de plusieurs tableaux, vous pouvez envisager une syntaxe telle que:

combined_stuff:
  - <<: *s1, *s2
  - <<: *s3
  - d
  - e
  - f

s1, s2, s3sont des ancres sur des séquences (non représentés) que vous souhaitez fusionner dans une nouvelle séquence, puis ont la d, eet f annexées à cela. Mais YAML résout d'abord ce type de profondeur de structures, il n'y a donc pas de contexte réel disponible pendant le traitement de la clé de fusion. Il n'y a pas de tableau / liste à votre disposition où vous pouvez attacher la valeur traitée (la séquence ancrée).

Vous pouvez adopter l'approche proposée par @dreftymac, mais cela présente l'énorme inconvénient que vous devez en quelque sorte connaître les séquences imbriquées à aplatir (c'est-à-dire en connaissant le "chemin" de la racine de la structure de données chargée à la séquence parente), ou que vous parcouriez récursivement la structure de données chargée à la recherche de tableaux / listes imbriqués et de les aplatir tous sans discernement.

Une meilleure solution IMO serait d'utiliser des balises pour charger des structures de données qui font l'aplatissement pour vous. Cela permet d'indiquer clairement ce qui doit être aplati et ce qui ne l'est pas et vous donne un contrôle total sur si cet aplatissement est effectué pendant le chargement ou pendant l'accès. Laquelle choisir est une question de facilité de mise en œuvre et d'efficacité en termes de temps et d'espace de stockage. C'est le même compromis qui doit être fait pour implémenter la fonctionnalité de clé de fusion et il n'y a pas de solution unique qui soit toujours la meilleure.

Par exemple, ma ruamel.yamlbibliothèque utilise les merge-dicts de force brute lors du chargement lors de l'utilisation de son chargeur sécurisé, ce qui entraîne des dictionnaires fusionnés qui sont des dictionnaires Python normaux. Cette fusion doit être effectuée à l'avance et duplique les données (espace inefficace) mais est rapide dans la recherche de valeur. Lorsque vous utilisez le chargeur aller-retour, vous voulez pouvoir vider les fusions non fusionnées, elles doivent donc être séparées. Le dict, comme la structure de données chargée à la suite du chargement aller-retour, est peu encombrant mais plus lent d'accès, car il doit essayer de rechercher une clé qui ne se trouve pas dans le dict lui-même dans les fusions (et cela n'est pas mis en cache, donc il doit être fait à chaque fois). Bien entendu, ces considérations ne sont pas très importantes pour les fichiers de configuration relativement petits.


Ce qui suit implémente un schéma de fusion pour les listes en python en utilisant des objets avec une balise flatten qui récursent à la volée dans des éléments qui sont des listes et des balises toflatten. En utilisant ces deux balises, vous pouvez avoir un fichier YAML:

l1: &x1 !toflatten
  - 1 
  - 2
l2: &x2
  - 3 
  - 4
m1: !flatten
  - *x1
  - *x2
  - [5, 6]
  - !toflatten [7, 8]

(l'utilisation de séquences de style flux vs bloc est complètement arbitraire et n'a aucune influence sur le résultat chargé).

Lors de l'itération sur les éléments qui sont la valeur de la clé, m1cela «revient» dans les séquences étiquetées avec toflatten, mais affiche d'autres listes (aliasées ou non) comme un élément unique.

Un moyen possible avec le code Python d'y parvenir est:

import sys
from pathlib import Path
import ruamel.yaml

yaml = ruamel.yaml.YAML()


@yaml.register_class
class Flatten(list):
   yaml_tag = u'!flatten'
   def __init__(self, *args):
      self.items = args

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(*constructor.construct_sequence(node, deep=True))
       return x

   def __iter__(self):
       for item in self.items:
           if isinstance(item, ToFlatten):
               for nested_item in item:
                   yield nested_item
           else:
               yield item


@yaml.register_class
class ToFlatten(list):
   yaml_tag = u'!toflatten'

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(constructor.construct_sequence(node, deep=True))
       return x



data = yaml.load(Path('input.yaml'))
for item in data['m1']:
    print(item)

qui sort:

1
2
[3, 4]
[5, 6]
7
8

Comme vous pouvez le voir, dans la séquence à aplatir, vous pouvez soit utiliser un alias pour une séquence balisée, soit utiliser une séquence balisée. YAML ne vous permet pas de faire:

- !flatten *x2

, c'est-à-dire étiqueter une séquence ancrée, car cela en ferait essentiellement une structure de données différente.

L'utilisation de balises explicites est préférable à l'OMI que de faire de la magie comme avec les clés de fusion YAML <<. Si rien d'autre, vous devez maintenant passer par des cerceaux si vous avez un fichier YAML avec un mappage qui a une clé <<que vous ne voulez pas agir comme une clé de fusion, par exemple lorsque vous faites un mappage des opérateurs C à leurs descriptions en anglais (ou dans une autre langue naturelle).

Anthon
la source
9

Si vous avez seulement besoin de fusionner un élément dans une liste, vous pouvez le faire

fruit:
  - &banana
    name: banana
    colour: yellow

food:
  - *banana
  - name: carrot
    colour: orange

qui donne

fruit:
  - name: banana
    colour: yellow

food:
  - name: banana
    colour: yellow
  - name: carrot
    colour: orange
Tamlyn
la source
-4

Vous pouvez fusionner les mappages puis convertir leurs clés en liste, dans ces conditions:

  • si vous utilisez la création de modèles jinja2 et
  • si l'ordre des articles n'est pas important
some_stuff: &some_stuff
 a:
 b:
 c:

combined_stuff:
  <<: *some_stuff
  d:
  e:
  f:

{{ combined_stuff | list }}
sm4rk0
la source
Quel est le problème avec cette réponse? Les votes négatifs ne me dérangent pas s'ils sont discutés. Je vais garder la réponse pour les personnes qui peuvent en profiter.
sm4rk0
3
Probablement parce que cette réponse repose sur le modèle jinja2, lorsque la question demande de le faire en yml. jinja2 nécessite un environnement Python, ce qui est contre-productif si l'OP tente de SÉCHER. De plus, de nombreux outils CI / CD n'acceptent pas une étape de création de modèles.
Jorge Leitao
Merci @JorgeLeitao. Ça a du sens. J'ai appris YAML et Jinja2 ensemble tout en développant des playbooks et des modèles Ansible et je ne peux pas penser à l'un sans l'autre
sm4rk0