Вопрос:

Как разобрать данные YAML в пользовательский массив данных Bash / структуру хеша?

arrays bash yaml associative-array

63 просмотра

2 ответа

34 Репутация автора

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

site:
  title: My blog
  domain: example.com
  author1:
    name: bob
    url: /author/bob
  author2:
    name: jane
    url: /author/jane
  header_links:
    about:
      title: About
      url: about.html
    contact:
      title: Contact Us
      url: contactus.html
  js_deps:
    - cashjs
    - jets

products:
  product1:
    name: Prod One
    price: 10
  product2:
    name: Prod Two
    price: 20

И я хотел бы использовать функцию или сценарий Bash, Python или AWK, которые могут взять указанный выше файл YAML в качестве input ( $1) и сгенерировать , а затем выполнить следующий код (или что-то точно эквивалентное):

unset site_title 
unset site_domain
unset site_author1
unset site_author2
unset site_header_links
unset site_header_links_about
unset site_header_links_contact
unset js_deps

site_title="My blog"
site_domain="example.com"

declare -A site_author1
declare -A site_author2

site_author1=(
  [name]="bob"
  [url]="/author/bob"
)

site_author2=(
  [name]="jane"
  [url]="/author/jane"
)

declare -A site_header_links_about
declare -A site_header_links_contact

site_header_links_about=(
  [name]="About"
  [url]="about.html"
)

site_header_links_contact=(
  [name]="Contact Us"
  [url]="contact.html"
)

site_header_links=(site_header_links_about  site_header_links_contact)

js_deps=(cashjs jets)

unset products
unset product1
unset product2

declare -A product1
declare -A product2

product1=(
  [name]="Prod One"
  [price]=10
)

product2=(
  [name]="Prod Two"
  [price]=20
)

products=(product1 product2)

Итак, логика такова:

Пройдите через YAML и создайте подчеркиваемые объединенные имена переменных со строковыми значениями, за исключением последнего (нижнего) уровня, где данные должны создаваться как ассоциативный массив или индексный массив, где это возможно ... Кроме того, любые созданные ассоциированные массивы должны быть перечислены по имени в индексированном массиве.

Итак, другими словами:

  • где последний уровень данных может быть превращен в ассоциативный массив, тогда он должен быть ( foo.bar.hash=>${foo_bar_hash[@]}

  • где последний уровень данных может быть преобразован в индексированный массив, тогда он должен быть ( foo.bar.list=>${foo_bar_list[@]}

  • каждый ассоциированный массив должен быть указан по имени в индексированном массиве, который назван в честь своего родителя в данных yaml (см. productsв примере)

  • в противном случае просто создайте подчеркнутое объединенное имя переменной и сохраните значение в виде строки ( foo.bar.string=>${foo_bar_string}

... Причина, по которой мне нужна эта конкретная структура данных Bash, заключается в том, что я использую систему шаблонов на основе Bash, которая требует этого.

Когда у меня появится нужная функция, я смогу легко использовать данные YAML в своих шаблонах, например:

{{site_title}}

...

{{#foreach link in site_header_links}}
  <a href="{{link.url}}">{{link.name}}</a>
{{/foreach}}

...

{{#js_deps}}
  {{.}}
{{/js_deps}}

...

{{#foreach item in products}}
  {{item.name}}
  {{item.price}}
{{/foreach}}

Что я пробовал:

Это полностью связано с предыдущим вопросом, который я задал:

Это так близко, но мне нужно, site_header_linksчтобы и сгенерированный ассоциативный массив был в порядке ... он терпит неудачу, потому что site_header_linksвложен слишком глубоко.

Я все еще хотел бы использовать https://github.com/azohra/yaml.sh в решении, поскольку это обеспечило бы легкий плагиат руля lookupдля системы шаблонов :)

РЕДАКТИРОВАТЬ:

Чтобы быть супер ясным: решение не может использовать pip, virtualenvили любые другие внешние программы, которые нужно установить отдельно - это должен быть автономный скрипт / func (например, https://github.com/azohra/yaml.sh ), который может жить внутри проекта CMS dir ... или мне не нужно быть здесь ..

...

Надеюсь, хорошо прокомментированный ответ может помочь мне избежать возвращения сюда;)

Автор: sc0ttj Источник Размещён: 11.08.2019 12:11

Ответы (2)


0 плюса

36343 Репутация автора

Трудно понять, каковы правила карточной игры, просто глядя на людей, играющих в один раунд. И точно так же трудно понять, каковы «правила» вашего файла YAML.

Далее я сделал предположения о корневом уровне, а также об узлах первого, второго и третьего уровня и о том, какой вывод они генерируют. Также было бы правильно делать предположения об узле на основе уровня его родительских операций, который является более гибким (поскольку вы можете просто добавить, например, последовательность на корневом уровне), но это будет несколько сложнее реализовать.

Хранение объявлений объявляемых и составных массивов, перемежающихся с другим кодом и сгруппированных для «похожих» элементов, довольно обременительно. Для этого вам нужно будет отслеживать переходы типов узлов (str, dict, nested dict) и группировать по ним. Таким образом, для каждого корневого ключа я unsetсначала выкидываю все , затем объявляю все, затем все назначения, а затем все составные назначения. Я думаю, что подпадает под "нечто абсолютно эквивалентное".

Поскольку products-> product1/ product2обрабатывается совершенно иначе, чем site-> author1/, authro2которые имеют одинаковую структуру узлов, я создал отдельную функцию для обработки каждого ключа корневого уровня.

Чтобы запустить это, вы должны установить виртуальную среду для Python (3.7 / 3.6), установите библиотеку YAML в этом:

$ python -m venv /opt/util/yaml2bash
$ /opt/util/yaml2bash/bin/pip install ruamel.yaml

Затем сохраните следующую программу, например, в /opt/util/yaml2bash/bin/yaml2bash и сделайте ее исполняемой ( chmod +x /opt/util/yaml2bash/bin/yaml2bash)

#! /opt/util/yaml2bash/bin/python

import sys
from pathlib import Path
import ruamel.yaml

if len(sys.argv) > 0:
    input = Path(sys.argv[1])
else:
    input = sys.stdin


def bash_site(k0, v0, fp):
    """this function takes a root-level key and its value (v0 a dict), constructs the 
    list of unsets and outputs based on the keys, values and type of values of v0,
    then dumps these to fp
    """
    unsets = []
    declares = []
    assignments = []
    compounds = {}
    for k1, v1 in v0.items():
        if isinstance(v1, str):
            k = k0 + '_' + k1
            unsets.append(k)
            assignments.append(f'{k}="{v1}"')
        elif isinstance(v1, dict):
            first_val = list(v1.values())[0]
            if isinstance(first_val, str):
                k = k0 + '_' + k1
                unsets.append(k)
                declares.append(k)
                assignments.append(f'{k}=(')
                for k2, v2 in v1.items():
                    q = '"' if isinstance(v2, str) else ''
                    assignments.append(f'  [{k2}]={q}{v2}{q}')
                assignments.append(')')
            elif isinstance(first_val, dict):
                for k2, v2 in v1.items(): # assume all the same type
                    k = k0 + '_' + k1 + '_' + k2   
                    unsets.append(k)
                    declares.append(k)
                    assignments.append(f'{k}=(')
                    for k3, v3 in v2.items():
                        q = '"' if isinstance(v3, str) else ''
                        assignments.append(f'  [{k2}]={q}{v3}{q}')
                    assignments.append(')')
                    compounds.setdefault(k0 + '_' + k1, []).append(k)
            else:
                raise NotImplementedError("unknown val: " + repr(first_val))
        elif isinstance(v1, list):
            unsets.append(k1)
            compounds[k1] = v1
        else:
            raise NotImplementedError("unknown val: " + repr(v1))


    if unsets:
        for item in unsets:
            print('unset', item, file=fp)
        print(file=fp)
    if declares:
        for item in declares:
            print('declare -A', item, file=fp)
        print(file=fp)
    if assignments:
        for item in assignments:
            print(item, file=fp)
        print(file=fp)
    if compounds:
        for k in compounds:
            v = ' '.join(compounds[k])
            print(f'{k}=({v})', file=fp)
        print(file=fp)


def bash_products(k0, v0, fp):
    """this function takes a root-level key and its value (v0 a dict), constructs the 
    list of unsets and outputs based on the keys, values and type of values of v0,
    then dumps these to fp
    """
    unsets = [k0]
    declares = []
    assignments = []
    compounds = {}
    for k1, v1 in v0.items():
        if isinstance(v1, dict):
            first_val = list(v1.values())[0]
            if isinstance(first_val, str):
                unsets.append(k1)
                declares.append(k1)
                assignments.append(f'{k1}=(')
                for k2, v2 in v1.items():
                    q = '"' if isinstance(v2, str) else ''
                    assignments.append(f'  [{k2}]={q}{v2}{q}')
                assignments.append(')')
                compounds.setdefault(k0, []).append(k1)
            else:
                raise NotImplementedError("unknown val: " + repr(first_val))
        else:
            raise NotImplementedError("unknown val: " + repr(v1))


    if unsets:
        for item in unsets:
            print('unset', item, file=fp)
        print(file=fp)
    if declares:
        for item in declares:
            print('declare -A', item, file=fp)
        print(file=fp)
    if assignments:
        for item in assignments:
            print(item, file=fp)
        print(file=fp)
    if compounds:
        for k in compounds:
            v = ' '.join(compounds[k])
            print(f'{k}=({v})', file=fp)
        print(file=fp)




yaml = ruamel.yaml.YAML()
data = yaml.load(input)

output = sys.stdout  # make it easier to redirect to file if necessary at some point in the future

bash_site('site', data['site'], output)
bash_products('products', data['products'], output)

если вы запустите эту программу и предоставите входной файл YAML в качестве аргумента ( /opt/util/yaml2bash/bin/yaml2bash input.yaml), который дает:

unset site_title
unset site_domain
unset site_author1
unset site_author2
unset site_header_links_about
unset site_header_links_contact
unset js_deps

declare -A site_author1
declare -A site_author2
declare -A site_header_links_about
declare -A site_header_links_contact

site_title="My blog"
site_domain="example.com"
site_author1=(
  [name]="bob"
  [url]="/author/bob"
)
site_author2=(
  [name]="jane"
  [url]="/author/jane"
)
site_header_links_about=(
  [about]="About"
  [about]="about.html"
)
site_header_links_contact=(
  [contact]="Contact Us"
  [contact]="contactus.html"
)

site_header_links=(site_header_links_about site_header_links_contact)
js_deps=(cashjs jets)

unset products
unset product1
unset product2

declare -A product1
declare -A product2

product1=(
  [name]="Prod One"
  [price]=10
)
product2=(
  [name]="Prod Two"
  [price]=20
)

products=(product1 product2)

Вы можете использовать сделать что-то вроде, source $(/opt/util/yaml2bash/bin/yaml2bash input.yaml) чтобы получить все эти значения в Bash.

Обратите внимание, что все двойные кавычки в вашем файле YAML излишни.

Использование Python и ruamel.yaml (заявление об отказе от ответственности, я являюсь автором этого пакета) дает вам полный синтаксический анализатор YAML, например, позволяющий использовать комментарии и коллекции в стиле потока:

jsdeps: [cashjs, jets]    # more compact

Если вы застряли на Python 2.7 с почти истекшим сроком эксплуатации и не имеете полного контроля над своей машиной (в этом случае вы должны установить / скомпилировать Python 3.7 для нее), вы все равно можете использовать ruamel yaml.

  1. Определите, куда пойдет ваша программа, например ~/bin
  2. Создать ~/bin/ruamel (настроить согласно 1.)
  3. cd ~/bin/ruamel
  4. touch __init__.py
  5. Загрузите последний tar-файл из PyPI
  6. распакуйте файл tar и переименуйте получившийся каталог из ruamel.yaml-XYZ в просто yaml

ruamel.yamlдолжен работать без его зависимостей. На 2,7 те ruamel.ordereddictи ruamel.yaml.clibкоторые обеспечивают версии C базовых процедур для ускорения.

Приведенную выше программу нужно будет немного переписать (f-строки -> "".format()и pathlib.Path-> старомодноwith open(...) as fp:

Автор: Anthon Размещён: 11.08.2019 08:44

0 плюса

34 Репутация автора

Решение

Я решил использовать комбинацию из следующего:

  • взломанная версия Yay :

    • с добавленной поддержкой простых списков
    • исправления для нескольких уровней отступов
  • взломанная версия этого парсера yaml :

    • с префиксом, позаимствованным у Yay, для согласованности
function yaml_to_vars {
   # use given dataset prefix or imply from file name
   [[ -n "$2" ]] && local prefix="$2" || {
     local prefix=$(basename "$input"); prefix=${prefix%.*}; prefix="${prefix//-/_}_";
   }

   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
   sed -ne "s|,$s\]$s\$|]|" \
        -e ":1;s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: [\3]\n\1  - \4|;t1" \
        -e "s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s\]|\1\2:\n\1  - \3|;p" $1 | \
   sed -ne "s|,$s}$s\$|}|" \
        -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1  \3: \4|;t1" \
        -e    "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1  \2|;p" | \
   sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p" \
        -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|p" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" | \
   awk -F$fs '{
      indent = length($1)/2;
      vname[indent] = $2;
      for (i in vname) {if (i > indent) {delete vname[i]; idx[i]=0}}
      if(length($2)== 0){  vname[indent]= ++idx[indent] };
      if (length($3) > 0) {
         vn=""; for (i=0; i<indent; i++) { vn=(vn)(vname[i])("_")}
         printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, vname[indent], $3);
      }
   }'
}

yay_parse() {

   # find input file
   for f in "$1" "$1.yay" "$1.yml"
   do
     [[ -f "$f" ]] && input="$f" && break
   done
   [[ -z "$input" ]] && exit 1

   # use given dataset prefix or imply from file name
   [[ -n "$2" ]] && local prefix="$2" || {
     local prefix=$(basename "$input"); prefix=${prefix%.*}; prefix=${prefix//-/_};
   }

   echo "unset $prefix; declare -g -a $prefix;"

   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
   #sed -n -e "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
   #       -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$input" |
   sed -ne "s|,$s\]$s\$|]|" \
        -e ":1;s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: [\3]\n\1  - \4|;t1" \
        -e "s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s\]|\1\2:\n\1  - \3|;p" $1 | \
   sed -ne "s|,$s}$s\$|}|" \
        -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1  \3: \4|;t1" \
        -e    "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1  \2|;p" | \
   sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p" \
        -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|p" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" | \
   awk -F$fs '{
      indent       = length($1)/2;
      key          = $2;
      value        = $3;

      # No prefix or parent for the top level (indent zero)
      root_prefix  = "'$prefix'_";
      if (indent == 0) {
        prefix = "";          parent_key = "'$prefix'";
      } else {
        prefix = root_prefix; parent_key = keys[indent-1];
      }

      keys[indent] = key;

      # remove keys left behind if prior row was indented more than this row
      for (i in keys) {if (i > indent) {delete keys[i]}}

      # if we have a value
      if (length(value) > 0) {

        # set values here

        # if the "key" is missing, make array indexed, not assoc..

        if (length(key) == 0) {
          # array item has no key, only a value..
          # so, if we didnt already unset the assoc array
          if (unsetArray == 0) {
            # unset the assoc array here
            printf("unset %s%s; ", prefix, parent_key);
            # switch the flag, so we only unset once, before adding values
            unsetArray = 1;
          }
          # array was unset, has no key, so add item using indexed array syntax
          printf("%s%s+=(\"%s\");\n", prefix, parent_key, value);

        } else {
          # array item has key and value, add item using assoc array syntax
          printf("%s%s[%s]=\"%s\";\n", prefix, parent_key, key, value);
        }

      } else {

        # declare arrays here

        # reset this flag for each new array we work on...
        unsetArray = 0;

        # if item has no key, declare indexed array
        if (length(key) == 0) {
          # indexed
          printf("unset %s%s; declare -g -a %s%s;\n", root_prefix, key, root_prefix, key);

        # if item has numeric key, declare indexed array
        } else if (key ~ /^[[:digit:]]/) {
          printf("unset %s%s; declare -g -a %s%s;\n", root_prefix, key, root_prefix, key);

        # else (item has a string for a key), declare associative array
        } else {
          printf("unset %s%s; declare -g -A %s%s;\n", root_prefix, key, root_prefix, key);
        }

        # set root level values here

        if (indent > 0) {
          # add to associative array
          printf("%s%s[%s]+=\"%s%s\";\n", prefix, parent_key , key, root_prefix, key);
        } else {
          # add to indexed array
          printf("%s%s+=( \"%s%s\");\n", prefix, parent_key , root_prefix, key);
        }

      }
   }'
}


# helper to load yay data file
yay() {
  # yaml_to_vars "$@"  ## uncomment to debug (prints data to stdout)
  eval $(yaml_to_vars "$@")

  # yay_parse "$@"  ## uncomment to debug (prints data to stdout)
  eval $(yay_parse "$@")
}

Используя код выше, когда products.ymlсодержит:

  product1
    name: Foo
    price: 100
  product2
    name: Bar
    price: 200

парсер можно назвать так:

source path/to/yml-parser.sh
yay products.yml

И он генерирует, а затем оценивает этот код:

products_product1_name="Foo"
products_product1_price="100"
products_product2_name="Bar"
products_product2_price="200"
unset products;
declare -g -a products;
unset products_product1;
declare -g -A products_product1;
products+=( "products_product1");
products_product1[name]="Foo";
products_product1[price]="100";
unset products_product2;
declare -g -A products_product2;
products+=( "products_product2");
products_product2[name]="Bar";
products_product2[price]="200";

Итак, я получаю следующие массивы и переменные Bash:

declare -a products=([0]="products_product1" [1]="products_product2")
declare -A products_product1=([price]="100" [name]="Foo" )
declare -A products_product2=([price]="200" [name]="Bar" )

И в моей системе шаблонов я теперь могу получить доступ к этим данным yml следующим образом:

{{#foreach product in products}}
  Name:  {{product.name}}
  Price: {{product.price}}
{{/foreach}}

:)

Другой пример:

файл site.yml

meta_info:
  title: My cool blog
  domain: foo.github.io
author1:
  name: bob
  url: /author/bob
author2:
  name: jane
  url: /author/jane
header_links:
  link1:
    title: About
    url: about.html
  link2:
    title: Contact Us
    url: contactus.html
js_deps:
  cashjs: cashjs
  jets: jets
Foo:
  - one
  - two
  - three

Производит:

declare -a site=([0]="site_meta_info" [1]="site_author1" [2]="site_author2" [3]="site_header_links" [4]="site_js_deps" [5]="site_Foo")
declare -A site_meta_info=([title]="My cool blog" [domain]="foo.github.io" )
declare -A site_author1=([url]="/author/bob" [name]="bob" )
declare -A site_author2=([url]="/author/jane" [name]="jane" )
declare -A site_header_links=([link1]="site_link1" [link2]="site_link2" )
declare -A site_link1=([url]="about.html" [title]="About" )
declare -A site_link2=([url]="contactus.html" [title]="Contact Us" )
declare -A site_js_deps=([cashjs]="cashjs" [jets]="jets" )
declare -a site_Foo=([0]="one" [1]="two" [2]="three")

В моих шаблонах я могу получить доступ site_header_linksтак:

{{#foreach link in site_header_links}}
  * {{link.title}} - {{link.url}}
{{/foreach}}

и site_Foo(точечная нотация или простой список) примерно так:

{{#site_Foo}}
  * {{.}}
{{/site_Foo}}
Автор: sc0ttj Размещён: 21.08.2019 09:53
Вопросы из категории :
32x32