Como mesclar arrays YAML?

112

Eu gostaria de mesclar arrays em YAML e carregá-los via ruby ​​-

some_stuff: &some_stuff
 - a
 - b
 - c

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

Eu gostaria de ter a matriz combinada como [a,b,c,d,e,f]

Recebo o erro: não encontrei a chave esperada ao analisar um mapeamento de bloco

Como faço para mesclar matrizes em YAML?

lfender6445
fonte
6
Por que você deseja fazer isso em YAML, em vez da linguagem com a qual está analisando?
Patrick Collins
7
para secar a duplicação em um arquivo yaml muito grande
lfender6445
4
Esta é uma prática muito ruim. Você deve ler yamls separadamente, colocar os arrays juntos em Ruby e, em seguida, escrevê-los de volta no yaml.
sawa
74
Como tentar ser seco é uma má prática?
krak3n
13
@PatrickCollins Eu encontrei esta pergunta tentando reduzir a duplicação em meu arquivo .gitlab-ci.yml e, infelizmente, não tenho controle sobre o analisador que o GitLab CI usa :(
rink.attendant.6

Respostas:

40

Se o objetivo é executar uma sequência de comandos shell, você pode conseguir isso da seguinte maneira:

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

combined_stuff:
  - *some_stuff
  - d
  - e
  - f

Isso é equivalente a:

some_stuff: "a\nb\nc"

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

Tenho usado isso no meu gitlab-ci.yml(para responder ao comentário @rink.attendant.6 sobre a pergunta).


Exemplo requirements.txtprático que usamos para oferecer suporte a repositórios privados do 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...

onde requirements_test.txtcontém, por exemplo

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

Jorge Leitão
fonte
3
Inteligente. Estou usando em nosso pipeline do Bitbucket agora. Obrigado
Dariop
* O traço final não é necessário aqui, apenas o tubo no final é suficiente. * Esta é uma solução inferior, pois quando o trabalho falha em uma instrução de várias linhas muito longa, não está claro qual comando falhou.
Mina Luke
1
@MinaLuke, inferior em comparação a quê? Nenhuma das respostas atuais fornece uma maneira de mesclar dois itens usando apenas yaml ... Além disso, não há nada na pergunta afirmando que o OP deseja usar isso em CI / CD. Finalmente, quando isso é usado em CI / CD, o registro depende apenas do CI / CD específico usado, não da declaração yaml. Portanto, se houver alguma coisa, o CI / CD a que você se refere é aquele que está fazendo um trabalho ruim. O yaml nesta resposta é válido e resolve o problema de OP.
Jorge Leitão
@JorgeLeitao Acho que você o usa para combinar regras. Você pode fornecer um exemplo funcional do gitlabci? Tentei algo baseado na sua solução, mas sempre recebo um erro de validação.
niels
@niels, adicionei um exemplo com um exemplo funcional do gitlabci. Observe que alguns IDEs marcam este yaml como inválido, embora não seja.
Jorge Leitão
26

Atualização: 01-07-2019 14:06:12

  • Nota : outra resposta a esta pergunta foi substancialmente editada com uma atualização sobre abordagens alternativas .
    • Essa resposta atualizada menciona uma alternativa para a solução alternativa nesta resposta. Ele foi adicionado à seção Consulte também abaixo.

Contexto

Esta postagem assume o seguinte contexto:

  • python 2.7
  • analisador python YAML

Problema

lfender6445 deseja mesclar duas ou mais listas dentro de um arquivo YAML e fazer com que essas listas mescladas apareçam como uma lista única quando analisadas.

Solução (solução alternativa)

Isso pode ser obtido simplesmente atribuindo âncoras YAML aos mapeamentos, onde as listas desejadas aparecem como elementos filho dos mapeamentos. Existem ressalvas a isso, entretanto, (veja "Armadilhas" infra).

No exemplo abaixo, temos três mapeamentos ( list_one, list_two, list_three) e três âncoras e aliases que se referem a esses mapeamentos quando apropriado.

Quando o arquivo YAML é carregado no programa, obtemos a lista que desejamos, mas pode exigir uma pequena modificação após o carregamento (veja as armadilhas abaixo).

Exemplo

Arquivo YAML original

  list_one: & id001
   - uma
   - b
   - c

  list_two: & id002
   - e
   - f
   - g

  list_three: & id003
   - h
   - Eu
   - j

  list_combined:
      - * id001
      - * id002
      - * id003

Resultado após YAML.safe_load

## list_combined
  [
    [
      "uma",
      "b",
      "c"
    ],
    [
      "e",
      "f",
      "g"
    ],
    [
      "h",
      "Eu",
      "j"
    ]
  ]

Armadilhas

Conclusão

Essa abordagem permite a criação de listas mescladas usando o alias e o recurso âncora do YAML.

Embora o resultado da saída seja uma lista aninhada de listas, isso pode ser facilmente transformado usando o flattenmétodo.

Veja também

Abordagem alternativa atualizada por @Anthon

Exemplos do flattenmétodo

dreftymac
fonte
21

Isto não vai funcionar:

  1. merge só é compatível com as especificações YAML para mapeamentos e não para sequências

  2. você está misturando completamente as coisas tendo uma chave de mesclagem << seguida pelo separador de chave / valor :e um valor que é uma referência e então continua com uma lista no mesmo nível de indentação

YAML incorreto:

combine_stuff:
  x: 1
  - a
  - b

Portanto, sua sintaxe de exemplo nem mesmo faria sentido como uma proposta de extensão YAML.

Se você deseja fazer algo como mesclar várias matrizes, pode considerar uma sintaxe como:

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

onde s1, s2, s3são âncoras sobre sequências (não mostrados) que você deseja mesclar em uma nova sequência e, em seguida, têm a d, ee f em anexo a esse. Mas YAML está resolvendo esse tipo de profundidade de estruturas primeiro, portanto, não há contexto real disponível durante o processamento da chave de mesclagem. Não há matriz / lista disponível para você onde possa anexar o valor processado (a sequência ancorada).

Você pode seguir a abordagem proposta por @dreftymac, mas isso tem a grande desvantagem de que, de alguma forma, você precisa saber quais sequências aninhadas achatar (ou seja, conhecendo o "caminho" da raiz da estrutura de dados carregada até a sequência pai), ou que você percorre recursivamente a estrutura de dados carregada procurando por arrays / listas aninhados e nivela indiscriminadamente todos eles.

A melhor solução IMO seria usar tags para carregar estruturas de dados que fazem o achatamento para você. Isso permite denotar claramente o que precisa ser nivelado e o que não deve ser e oferece controle total sobre se esse nivelamento é feito durante o carregamento ou durante o acesso. Qual escolher é uma questão de facilidade de implementação e eficiência de tempo e espaço de armazenamento. Essa é a mesma compensação que precisa ser feita para implementar o recurso chave de mesclagem e não há uma solução única que seja sempre a melhor.

Por exemplo, minha ruamel.yamlbiblioteca usa merge-dicts de força bruta durante o carregamento ao usar seu carregador seguro, o que resulta em dicionários combinados que são dicts normais do Python. Essa mesclagem deve ser feita antecipadamente e duplica os dados (espaço ineficiente), mas é rápida na pesquisa de valor. Ao usar o carregador de ida e volta, você deseja poder despejar as mesclagens não mescladas, portanto, elas precisam ser mantidas separadas. O dicionário, como a estrutura de dados carregado como resultado do carregamento de ida e volta, é eficiente em termos de espaço, mas mais lento no acesso, pois precisa tentar e procurar uma chave não encontrada no próprio dicionário nas mesclagens (e isso não é armazenado em cache, então precisa ser feito o tempo todo). É claro que essas considerações não são muito importantes para arquivos de configuração relativamente pequenos.


O seguinte implementa um esquema de mesclagem para listas em python usando objetos com tag flatten que recorre em tempo real em itens que são listas e marcados toflatten. Usando essas duas tags, você pode ter um arquivo YAML:

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

(o uso de sequências de estilo de fluxo vs bloco é completamente arbitrário e não tem influência no resultado carregado).

Ao iterar os itens que são o valor da chave, m1isso "recorre" às ​​sequências marcadas com toflatten, mas exibe outras listas (com alias ou não) como um único item.

Uma maneira possível com o código Python de conseguir isso é:

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)

que produz:

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

Como você pode ver, na sequência que precisa ser nivelada, você pode usar um alias para uma sequência marcada ou uma sequência marcada. YAML não permite que você faça:

- !flatten *x2

, ou seja, marcar uma sequência ancorada, pois isso essencialmente a transformaria em uma estrutura de dados diferente.

Usar tags explícitas é IMO melhor do que ter alguma mágica acontecendo com as teclas de mesclagem YAML <<. Se nada mais, você agora tem que passar por muitos obstáculos se acontecer de você ter um arquivo YAML com um mapeamento que tem uma chave <<que você não deseja que atue como uma chave de mesclagem, por exemplo, quando você faz um mapeamento de operadores C para suas descrições em inglês (ou alguma outra língua natural).

Anthon
fonte
9

Se você só precisa mesclar um item em uma lista, você pode fazer

fruit:
  - &banana
    name: banana
    colour: yellow

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

que produz

fruit:
  - name: banana
    colour: yellow

food:
  - name: banana
    colour: yellow
  - name: carrot
    colour: orange
Tamlyn
fonte
-4

Você pode mesclar mapeamentos e converter suas chaves em uma lista, nas seguintes condições:

  • se você estiver usando modelos jinja2 e
  • se o pedido do item não é importante
some_stuff: &some_stuff
 a:
 b:
 c:

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

{{ combined_stuff | list }}
sm4rk0
fonte
O que há de errado com essa resposta? Eu não me importo em votos negativos se eles forem argumentados. Guardarei a resposta para quem puder fazer uso dela.
sm4rk0
3
Provavelmente porque essa resposta depende da modelagem jinja2, quando a pergunta pede para fazê-lo em yml. jinja2 requer um ambiente Python, o que é contraproducente se o OP está tentando secar. Além disso, muitas ferramentas de CI / CD não aceitam uma etapa de modelagem.
Jorge Leitão
Obrigado @JorgeLeitao. Isso faz sentido. Aprendi YAML e Jinja2 juntos durante o desenvolvimento de manuais e modelos do Ansible e não consigo pensar em um sem o outro
sm4rk0