jak odwołać się do „ustawienia” YAML z innego miejsca w tym samym pliku YAML?

145

Mam następujący YAML:

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

Jak mogę to „znormalizować”, usuwając /path/to/root/z trzech ścieżek i mieć to jako własne ustawienie, na przykład:

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

Oczywiście to nieważne, właśnie to wymyśliłem. Jaka jest prawdziwa składnia? Czy da się to zrobić?

Andrew Bullock
źródło
1
Zobacz też: stackoverflow.com/a/41620747/42223
dreftymac

Odpowiedzi:

127

Nie sądzę, żeby to było możliwe. Możesz ponownie użyć „węzła”, ale nie może być jego częścią.

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

Jest to całkowicie poprawne YAML i pola giveni familysą ponownie wykorzystywane w ship-tobloku. Możesz ponownie użyć węzła skalarnego w ten sam sposób, ale nie ma możliwości zmiany tego, co jest w środku i dodania tej ostatniej części ścieżki do niego z wewnątrz YAML.

Jeśli powtarzanie Ci tak bardzo przeszkadza, proponuję, aby Twoja aplikacja była świadoma rootwłaściwości i dodała ją do każdej ścieżki, która wygląda na względną, a nie absolutną.

vava
źródło
1
Ok, dzięki, tak źle muszę dołączyć rootkod w kodzie. Nic takiego.
Andrew Bullock,
2
Przyjęta odpowiedź nie jest dokładna. Zobacz moją odpowiedź na rozwiązanie.
Chris Johnson,
Jak to zrobić, jeśli Bill-to jest w innym pliku, który mamy importowane gdzie statek-to jest zdefiniowane?
Prateek Jain
@PrateekJain: jeśli masz do czynienia z wieloma plikami, prawdopodobnie najlepiej zrobisz, aby ocenić samodzielną bibliotekę rozszerzającą YAML, taką jak wymieniona tutaj. github.com/dreftymac/dynamic.yaml/blob/master/…
dreftymac
1
Zobacz przykład 2.9 w yaml.org/spec/1.2/spec.html ; można również odwoływać się do skalarów, co jest niesamowite
akostadinov
72

Tak, używając niestandardowych tagów. Przykład w Pythonie, !joinłączenie ze znacznikiem ciągów znaków w tablicy:

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

Co skutkuje w:

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

Tablica argumentów !joinmoże zawierać dowolną liczbę elementów dowolnego typu danych, o ile można je przekonwertować na łańcuch, więc !join [*a, "/", *b, "/", *c]robi to, czego można się spodziewać.

Chris Johnson
źródło
2
Podoba mi się twoje rozwiązanie, prostsze w kodowaniu niż moje, kosztem nieco mniej czytelnego YAML.
Anthon
7
Ta odpowiedź zasługuje na więcej pozytywnych głosów. Jest to technicznie najdokładniejsza odpowiedź zgodnie ze specyfikacją YAML. Jest jednak jedno zastrzeżenie, jednak zgodnie z rzeczywistymi implementacjami YAML , niewiele jest takich, które faktycznie implementują pełną specyfikację YAML. Pyyaml ​​Pythona przewyższa wiele innych pod względem zgodności ze specyfikacją.
dreftymac
5
Wydaje się, że pytanie dotyczy odwoływania się do wartości w pliku yaml. Dodanie dookoła kolejnej warstwy kodu nie byłoby moim preferowanym rozwiązaniem.
user2020056
1
@ChrisJohnson Dziękuję za tę odpowiedź. Zastanawiałem się, czy masz dokument referencyjny, w którym wymieniono tę składnię. Widziałem specyfikację YAML wyjaśnioną w wielu miejscach w Internecie, więc chcę się tylko upewnić, że patrzę na to samo odniesienie, co ty. Dzięki!
user5359531
3
U mnie to rozwiązanie nie zadziałało ( python3?) Jednak po prostej modyfikacji powyższego działa zgodnie z oczekiwaniami. W szczególności:yaml.SafeLoader.add_constructor(tag='!join', constructor=join) yaml.load(open(fpth, mode='r'), Loader=yaml.SafeLoader)
benjaminmgross,
20

Innym sposobem spojrzenia na to jest po prostu użycie innego pola.

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
źródło
5

Definicja YML:

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

Gdzieś w grasicy

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

Wyjście: / home / data / in / / home / data / in / p1

Pavol
źródło
@AndrewBullock Myślę, że to powinna być akceptowana odpowiedź, ponieważ dokładnie rozwiązuje twój problem.
Honza Zidek
5
Nie, nie jest to natywne użycie zmiennej w YAML i nie jest określone w żadnej wersji specyfikacji. Po pewnym teście to nie działa.
Arthur Lacoste
2
Prawdopodobnie zadziałało to dla Pavola przy użyciu czegoś, co wstępnie przetworzyło yaml (tj. Filtrowanie wtyczki maven-resources-plugin)
DeezCashews
1
Niestandardowy Yaml
Dan Niero
3

Utworzyłem bibliotekę, dostępną na Packagist, która spełnia tę funkcję: https://packagist.org/packages/grasmash/yaml-expander

Przykładowy plik 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}

Przykładowa logika:

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

Wynikowa tablica:

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
źródło
Uwielbiam koncepcję ekspandera!
Guillaume Roderick
2

W niektórych językach możesz użyć alternatywnej biblioteki, na przykład tampax jest implementacją zmiennych obsługujących 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
źródło
1

Twój przykład jest niepoprawny tylko dlatego, że wybrałeś zarezerwowany znak, aby rozpocząć skalary. Jeśli *zastąpisz znak innym niezastrzeżonym znakiem (zwykle używam do tego znaków innych niż ASCII, ponieważ rzadko są one używane jako część jakiejś specyfikacji), otrzymasz całkowicie legalny YAML:

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

Spowoduje to załadowanie do standardowej reprezentacji mapowań w języku używanym przez parser i niczego w magiczny sposób nie rozszerzy.
Aby to zrobić, użyj lokalnie domyślnego typu obiektu, jak w następującym programie w języku 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]))

który wydrukuje:

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

Rozwijanie jest wykonywane w locie i obsługuje zagnieżdżone definicje, ale musisz uważać, aby nie wywoływać nieskończonej rekurencji.

Określając zrzut, możesz zrzucić oryginalny YAML z załadowanych danych, ponieważ rozwija się w locie:

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

spowoduje to zmianę kolejności kluczy mapowania. Jeśli to jest problem, trzeba dokonać (importowane z )self.dCommentedMapruamel.yaml.comments.py

Anthon
źródło
0

Napisałem własną bibliotekę w Pythonie, aby rozwinąć zmienne ładowane z katalogów z hierarchią, taką jak:

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

Kluczowa różnica polega na tym, że rozwinięcie należy zastosować dopiero po config.yamlzaładowaniu wszystkich plików, gdzie zmienne z następnego pliku mogą nadpisać zmienne z poprzedniego, więc pseudokod powinien wyglądać następująco:

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

Jako dodatkową opcję xonshskrypt może eksportować wynikowe zmienne do zmiennych środowiskowych (patrz yaml_update_global_varsfunkcja).

Skrypty:

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

Plusy :

  • prosta, nie obsługuje rekursji i zmiennych zagnieżdżonych
  • może zamienić niezdefiniowaną zmienną na symbol zastępczy ( ${MYUNDEFINEDVAR}-> *$/{MYUNDEFINEDVAR})
  • może rozwinąć odniesienie ze zmiennej środowiskowej ( ${env:MYVAR})
  • można zamienić wszystko \\na /w zmiennej ścieżki ( ${env:MYVAR:path})

Wady :

  • nie obsługuje zagnieżdżonych zmiennych, więc nie może rozszerzać wartości w zagnieżdżonych słownikach (coś takiego ${MYSCOPE.MYVAR}nie jest zaimplementowane)
  • nie wykrywa rekursji rozwinięcia, w tym rekursji po wstawieniu symbolu zastępczego
Andry
źródło
0

Z Yglu możesz zapisać swój przykład jako:

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

Zastrzeżenie: jestem autorem Yglu.

lbovet
źródło
Warto mieć świadomość, że biblioteka dodaje tę funkcjonalność do YAML
Dhiraj