【问题标题】:How to parse YAML data into a custom Bash data array/hash structure?如何将 YAML 数据解析为自定义 Bash 数据数组/哈希结构?
【发布时间】:2019-08-11 00:11:30
【问题描述】:

我有以下 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 文件作为输入 ($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,并在最后(底部)级别创建带有字符串值的下划线连接变量名称,except,其中应尽可能将数据创建为关联数组或索引数组。 .. 此外,创建的任何关联数组都应按名称列出,在索引数组中。

所以,换句话说:

  • 只要最后一级数据可以转换为关联数组,那么它应该是 (foo.bar.hash => ${foo_bar_hash[@]}

  • 只要最后一级数据可以转换为索引数组,那么它应该是 (foo.bar.list => ${foo_bar_list[@]}

  • 每个 assoc 数组都应按名称列在一个索引数组中,该数组以其在 yaml 数据中的父级命名(参见示例中的 products

  • 否则,只需创建一个下划线连接的 var 名称并将值保存为字符串 (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 rip-off :)

编辑:

非常清楚:解决方案不能使用pipvirtualenv,或任何其他需要单独安装的外部部门——它必须是一个独立的脚本/函数(如@ 987654323@ 是),它可以存在于 CMS 项目目录中......或者我不需要在这里......

...

希望,一个很好的评论答案可能会帮助我避免回到这里;)

【问题讨论】:

  • 我不太清楚我尝试了什么,什么不起作用 - 我在 OP 中声明我以前的帖子对我不起作用,因为“我需要一个 site_header_links 的关联数组生成的也很好..它失败了,因为site_header_links 嵌套太深”我尝试了很多东西,但都没有工作——只是破解了以前的解决方案,但无济于事。我不认为它很宽泛——我只想要 90% 的基于 shell 的 YAML 解析器所做的事情。要创建 _ 串联变量——except 我想要最后一级的索引/关联数组(以及索引数组中按名称列出的关联数组)..
  • 我已经通过链接到另一个问题来总结我已经尝试过的内容,这是我在这里的旅程的一部分,并包含指向我试图破解的所有库的链接,如以及最接近的解决方案是什么,以及为什么它仍然不能完全满足我的需求...我认为用我遇到的所有 很多很多 失败向我的帖子发送垃圾邮件是不合适的,当我链接到的库比我得到的更接近时.. :/
  • 为什么输出中有变量product1而不是products_product1?哪里没有products_product1_name 变量?你如何决定哪个级别的 get 是一个关联数组,哪个用下划线命名?为什么会有site_header_links_contact?但是没有site=([title]="My blog")array?为什么是site_title 变量而不是site 数组?
  • 我不需要products_product1_name,因为我将拥有product1[name],可以像{{foreach product in products}}一样访问(在我的模板系统中)..这就是为什么..我不需要包含的变量数组中已经存在的东西 .. 可以 在底层创建数组的地方,它们应该是(而不是你本来拥有的连接变量)......没有site 数组,因为,作为众所周知,Bash 不做多维对象,也不在底层.....
  • 所以换句话说,我想要底层的数组,因为 Bash 可以做到,但它不能制作其他级别的多维数组,所以按名称列出数组是一个 hack ...一旦数据结构到位,我的模板系统就可以迭代事物就好像它们是 2 级(或更多)深 ..

标签: arrays bash yaml associative-array


【解决方案1】:

我决定使用以下组合:

  • Yay 的破解版:

    • 增加了对简单列表的支持
    • 多个缩进级别的修复
  • this yaml parser 的破解版:

    • 前缀是从 Yay 借来的,以保持一致性
function yaml_to_vars {
   # 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//-/_}_";
   }

   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}}

【讨论】:

    【解决方案2】:

    单凭纸牌游戏的规则是很难看出的 看着玩一轮的人。并且以类似的方式 很难确切地看到 YAML 文件的“规则”是什么。

    在下文中,我也对根级别做出了假设 作为一级、二级和三级节点以及它们的输出 产生。对节点进行假设也是有效的 基于它所拥有的操作父级级别,这更灵活(如您 然后可以添加例如根级别的序列),但这会 实施起来有点困难。

    保持声明和复合数组分配穿插 其他代码并为“类似”项目分组有点麻烦。 为此,您需要跟踪节点类型的转换(str, dict,嵌套 dict) 和组。所以每个根级密钥我都转储了 unset 首先,然后是所有声明,然后是所有分配,然后是 al 复合作业。我认为这属于“确切的东西 等效”。

    由于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(免责声明我是那个的作者 package) 为您提供完整的 YAML 解析器,例如允许您使用 cmets 和 flow-style 收藏:

    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. 从 PyPI 下载 latest tar file
    6. 解压 tar 文件并将生成的目录从 ruamel.yaml-X.Y.Z 重命名为 yaml

    ruamel.yaml 应该可以在没有依赖关系的情况下工作。在 2.7 上,ruamel.ordereddictruamel.yaml.clib 提供 C 版本的基本例程以加快速度。

    上面的程序需要稍微重写一下(f-strings -> "".format()pathlib.Path -> 老式的with open(...) as fp:

    【讨论】:

    • /opt/util 只是一个示例目录,您可以在脚本能够找到的任何位置下载并安装ruamel.yaml tar 文件。
    • 所以这个 ruamel.yaml 是一个单一的脚本(或单一目录中的脚本),没有外部依赖(如其他 pip 包、额外的 python 模块/库等),不需要任何其他比“香草”python 2.7 安装? ...如果是这样,我可以使用它..
    • Python 2.7 将于明年 1 月 1 日结束生命周期,所以我不确定用它做任何事情是一个好的选择(但你的整个系统听起来已经过时了)。我更新了我的答案。
    猜你喜欢
    • 2021-03-13
    • 2011-12-17
    • 2011-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 2013-11-28
    相关资源
    最近更新 更多