Как ссылаться на «настройки» YAML из другого места в том же файле YAML?

146

У меня есть следующий YAML:

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

Как я могу «нормализовать» это, удалив /path/to/root/из трех путей и использовать его в качестве собственной настройки, что-то вроде:

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

Очевидно, что это неверно, я просто выдумал. Какой реальный синтаксис? Это можно сделать?

Эндрю Баллок
источник
1
Смотрите также: stackoverflow.com/a/41620747/42223
dreftymac

Ответы:

127

Я не думаю, что это возможно. Вы можете повторно использовать «узел», но не его часть.

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

Это совершенно допустимый YAML и поля givenи familyповторно используются в ship-toблоке. Вы можете повторно использовать скалярный узел таким же образом, но вы не сможете изменить то, что находится внутри, и добавить к нему последнюю часть пути изнутри YAML.

Если повторение вас беспокоит, я предлагаю сообщить вашему приложению о rootсвойствах и добавить его к каждому пути, который выглядит относительно не абсолютным.

Вава
источник
1
Хорошо, спасибо, да, мне придется добавить rootв код. нет, важная персона
Эндрю Баллок
2
Принятый ответ не является точным. Смотрите мой ответ для решения.
Крис Джонсон
как это сделать, если bill-to находится в другом файле, который мы импортировали, где определен ship-to ?
Пратик Джейн
@PrateekJain: если вы имеете дело с несколькими файлами, вы, вероятно, приложите все усилия, чтобы оценить автономную библиотеку расширения YAML, например, перечисленную здесь. github.com/dreftymac/dynamic.yaml/blob/master/…
dreftymac
1
Смотрите пример 2.9 в yaml.org/spec/1.2/spec.html ; можно также сослаться на скаляры, что просто здорово
akostadinov
72

Да, используя пользовательские теги. Пример в Python, создание !joinтега для объединения строк в массив:

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

Что приводит к:

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

Массив аргументов to !joinможет иметь любое количество элементов любого типа данных, если они могут быть преобразованы в строку, что !join [*a, "/", *b, "/", *c]делает то, что вы ожидаете.

Крис Джонсон
источник
2
Мне нравится ваше решение, более простое в кодировании, чем мое, за счет немного менее читаемого YAML.
Антон
7
Этот ответ заслуживает большего количества голосов. Технически это самый точный ответ согласно спецификации YAML. Существует одно предостережение, однако, в соответствии с фактическими реализациями YAML , немногие действительно реализуют полную спецификацию YAML. Pyyaml ​​Python превосходит многие другие с точки зрения его единообразия со спецификацией.
dreftymac
5
Похоже, вопрос касается ссылки на значение в файле yaml. Добавление другого слоя кода вокруг него не будет моим предпочтительным решением.
user2020056
1
@ChrisJohnson Спасибо за этот ответ, мне было интересно, есть ли у вас справочный документ, в котором указан этот синтаксис. Я видел спецификации YAML, описанные в нескольких местах в Интернете, поэтому я просто хочу убедиться, что я смотрю на ту же ссылку, что и вы. Спасибо!
user5359531
3
Это решение не сработало для меня ( python3?), Но с простой модификацией вышеописанного оно работает как положено. В частности:yaml.SafeLoader.add_constructor(tag='!join', constructor=join) yaml.load(open(fpth, mode='r'), Loader=yaml.SafeLoader)
benjaminmgross
20

Другой способ взглянуть на это - просто использовать другое поле.

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
Брайан Брюггеман
источник
5

Определение YML:

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

Где-то в тимелист

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

Вывод: / home / data / in / / home / data / in / p1

Павол
источник
@ AndrewBullock Я думаю, что это должен быть принятый ответ, так как он точно решит вашу проблему.
Хонза Зидек
5
Нет, это не родное использование переменных в YAML, и оно не указано ни в одной версии спецификации. После некоторого теста это не работает.
Артур Лакост
2
Вероятно, это сработало для Павола с использованием чего-то, что предварительно обрабатывало yaml (то есть фильтрация плагинов maven-resources)
DeezCashews
1
Не стандартный Ямл
Дан Ньеро
3

Я создал библиотеку, доступную на Packagist, которая выполняет эту функцию: https://packagist.org/packages/grasmash/yaml-expander

Пример файла 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}

Пример логики:

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

Результирующий массив:

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
источник
Любить концепцию расширителя!
Гийом Родерик
2

В некоторых языках вы можете использовать альтернативную библиотеку. Например, тампакс является реализацией переменных обработки 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."
Артур Лакост
источник
1

Ваш пример недопустим только потому, что вы выбрали зарезервированный символ для начала своих скаляров. Если вы замените его *каким-либо другим незарезервированным символом (я склонен использовать для этого не-ASCII-символы, поскольку они редко используются как часть какой-либо спецификации), вы получите совершенно допустимый YAML:

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

Это загрузит стандартное представление для отображений на языке, который использует ваш синтаксический анализатор, и волшебным образом ничего не расширит.
Для этого используйте локальный тип объекта по умолчанию, как в следующей программе 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]))

который напечатает:

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

Расширение выполняется на лету и обрабатывает вложенные определения, но вы должны быть осторожны, чтобы не вызывать бесконечную рекурсию.

Указав дампер, вы можете выгружать исходный YAML из загруженных данных из-за расширения на лету:

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

это изменит порядок отображения ключей сопоставления. Если это проблема , которую вы должны сделать (импортирован из )self.dCommentedMapruamel.yaml.comments.py

Энтон
источник
0

Я написал свою собственную библиотеку на Python для расширения переменных, загружаемых из каталогов с такой иерархией, как:

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

Ключевое отличие здесь заключается в том, что расширение должно применяться только после config.yamlзагрузки всех файлов, где переменные из следующего файла могут переопределять переменные из предыдущего, поэтому псевдокод должен выглядеть следующим образом:

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

В качестве дополнительной опции xonshскрипт может экспортировать полученные переменные в переменные окружения (см. yaml_update_global_varsФункцию).

Сценарии:

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

Плюсы :

  • простой, не поддерживает рекурсию и вложенные переменные
  • может заменить неопределенную переменную на заполнитель ( ${MYUNDEFINEDVAR}-> *$/{MYUNDEFINEDVAR})
  • может расширить ссылку из переменной окружения ( ${env:MYVAR})
  • может заменить все \\до /в переменной пути ( ${env:MYVAR:path})

Минусы :

  • не поддерживает вложенные переменные, поэтому не может раскрывать значения во вложенных словарях (что-то подобное ${MYSCOPE.MYVAR}не реализовано)
  • не обнаруживает рекурсию расширения, включая рекурсию после размещения заполнителя
Andry
источник
0

С Yglu вы можете написать свой пример как:

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

Отказ от ответственности: я автор Yglu.

lbovet
источник
Хорошо знать библиотеку, которая добавляет эту функциональность поверх YAML
Dhiraj