【问题标题】:How can I parse a YAML file from a Linux shell script?如何从 Linux shell 脚本解析 YAML 文件?
【发布时间】:2011-06-28 05:30:31
【问题描述】:

我希望提供一个结构化的配置文件,让非技术用户尽可能容易地编辑(不幸的是它必须是一个文件),所以我想使用 YAML。但是,我找不到任何从 Unix shell 脚本解析它的方法。

【问题讨论】:

  • 不是直接你的问题,但如果你的 shell 脚本特别是关于处理不同节点的远程管理(和 yaml 库存),你可能想看看 ansible
  • 尝试使用yq在shell中读/写yaml文件。项目页面在这里:mikefarah.github.io/yq 您可以使用brewapt 安装该工具或下载二进制文件。读取值就像yq r some.yaml key.value 一样简单
  • @kenorb JSON != yml/YAML
  • 我发现了密切相关的函数pkuczynski's github,其中最好的(对我来说)来自jasperes's, maintained in his own github
  • @swe 实际上是json == yamlyaml != json。意思是 yaml 是 json 的超集。

标签: shell yaml


【解决方案1】:

yq 是一个轻量级和可移植的命令行 YAML 处理器

该项目的目标是成为 yaml 文件的jq 或 sed。

(https://github.com/mikefarah/yq#readme)

作为示例(直接从文档中窃取),给定一个 sample.yaml 文件:

---
bob:
  item1:
    cats: bananas
  item2:
    cats: apples

然后

yq eval '.bob.*.cats' sample.yaml

会输出

- bananas
- apples

【讨论】:

【解决方案2】:

使用 Python 的 PyYAMLYAML::Perl 等库最容易进行复杂的解析。

如果您想将所有 YAML 值解析为 bash 值,请尝试此脚本。这也将处理 cmets。请参阅下面的示例用法:

# pparse.py

import yaml
import sys
            
def parse_yaml(yml, name=''):
    if isinstance(yml, list):
        for data in yml:
            parse_yaml(data, name)
    elif isinstance(yml, dict):
        if (len(yml) == 1) and not isinstance(yml[list(yml.keys())[0]], list):
            print(str(name+'_'+list(yml.keys())[0]+'='+str(yml[list(yml.keys())[0]]))[1:])
        else:
            for key in yml:
                parse_yaml(yml[key], name+'_'+key)

            
if __name__=="__main__":
    yml = yaml.safe_load(open(sys.argv[1]))
    parse_yaml(yml)

test.yml

- folders:
  - temp_folder: datasets/outputs/tmp
  - keep_temp_folder: false

- MFA:
  - MFA: false
  - speaker_count: 1
  - G2P: 
    - G2P: true
    - G2P_model: models/MFA/G2P/english_g2p.zip
    - input_folder: datasets/outputs/Youtube/ljspeech/wavs
    - output_dictionary: datasets/outputs/Youtube/ljspeech/dictionary.dict
  - dictionary: datasets/outputs/Youtube/ljspeech/dictionary.dict
  - acoustic_model: models/MFA/acoustic/english.zip
  - temp_folder: datasets/outputs/tmp
  - jobs: 4
  - align:
    - config: configs/MFA/align.yaml
    - dataset: datasets/outputs/Youtube/ljspeech/wavs
    - output_folder: datasets/outputs/Youtube/ljspeech-aligned

- TTS:
  - output_folder: datasets/outputs/Youtube
  - preprocess:
    - preprocess: true
    - config: configs/TTS_preprocess.yaml # Default Config 
    - textgrid_folder: datasets/outputs/Youtube/ljspeech-aligned
    - output_duration_folder: datasets/outputs/Youtube/durations
    - sampling_rate: 44000 # Make sure sampling rate is same here as in preprocess config

需要 YAML 值的脚本:

yaml() {
    eval $(python pparse.py "$1")
}

yaml "test.yml"

# What python printed to bash:

folders_temp_folder=datasets/outputs/tmp
folders_keep_temp_folder=False
MFA_MFA=False
MFA_speaker_count=1
MFA_G2P_G2P=True
MFA_G2P_G2P_model=models/MFA/G2P/english_g2p.zip
MFA_G2P_input_folder=datasets/outputs/Youtube/ljspeech/wavs
MFA_G2P_output_dictionary=datasets/outputs/Youtube/ljspeech/dictionary.dict
MFA_dictionary=datasets/outputs/Youtube/ljspeech/dictionary.dict
MFA_acoustic_model=models/MFA/acoustic/english.zip
MFA_temp_folder=datasets/outputs/tmp
MFA_jobs=4
MFA_align_config=configs/MFA/align.yaml
MFA_align_dataset=datasets/outputs/Youtube/ljspeech/wavs
MFA_align_output_folder=datasets/outputs/Youtube/ljspeech-aligned
TTS_output_folder=datasets/outputs/Youtube
TTS_preprocess_preprocess=True
TTS_preprocess_config=configs/TTS_preprocess.yaml
TTS_preprocess_textgrid_folder=datasets/outputs/Youtube/ljspeech-aligned
TTS_preprocess_output_duration_folder=datasets/outputs/Youtube/durations
TTS_preprocess_sampling_rate=44000

使用 bash 访问变量:

echo "$TTS_preprocess_sampling_rate";
>>> 44000

【讨论】:

    【解决方案3】:

    现在做这件事的快速方法(以前的方法对我不起作用):

    sudo wget https://github.com/mikefarah/yq/releases/download/v4.4.1/yq_linux_amd64 -O /usr/bin/yq &&\
    sudo chmod +x /usr/bin/yq
    

    asd.yaml 示例:

    a_list:
      - key1: value1
        key2: value2
        key3: value3
    

    解析根:

    user@vm:~$ yq e '.' asd.yaml                                                                                                         
    a_list:
      - key1: value1
        key2: value2
        key3: value3
    
    

    解析key3:

    user@vm:~$ yq e '.a_list[0].key3' asd.yaml                                                                                             
    value3
    

    【讨论】:

      【解决方案4】:

      将我的答案从How to convert a json response into yaml in bash 移出,因为这似乎是关于从命令行处理 YAML 文本解析的权威帖子。

      我想添加有关 yq YAML 实现的详细信息。由于这个 YAML 解析器有两个实现,都具有名称 yq,因此如果不查看实现的 DSL,就很难区分哪个正在使用。有两种可用的实现是

      1. kislyuk/yq - 更经常谈论的版本,它是对 jq 的包装,使用 Python 编写,使用 PyYAML 库进行 YAML 解析
      2. mikefarah/yq - Go 实现,使用 go-yaml v3 解析器拥有自己的动态 DSL。

      几乎所有主要发行版都可以通过标准安装包管理器进行安装

      1. kislyuk/yq - Installation instructions
      2. 麦克法拉/yq - Installation instructions

      这两个版本都有一些优点和缺点,但有几点需要强调(取自他们的回购说明)

      kislyuk/yq

      1. 由于DSL完全采用jq,对于熟悉后者的用户来说,解析和操作变得非常简单
      2. 支持模式为preserve YAML tags and styles,但在转换过程中会丢失cmets。由于jqdoesn't preserve comments,在往返转换过程中,cmets 丢失。
      3. 作为包的一部分,XML support 是内置的。一个可执行文件xq,它使用xmltodict 将 XML 转码为 JSON 并将其通过管道传输到 jq,您可以在其上应用相同的 DSL 来执行对对象执行 CRUD 操作并将输出往返返回到 XML。
      4. 支持带有-i标志的就地编辑模式(类似于sed -i

      麦克法拉/yq

      1. 易于频繁更改 DSL,migration from 2.x - 3.x
      2. 对锚点、样式和标签的丰富支持。但偶尔注意一下错误
      3. 一个相对简单的 Path expression 语法来导航和匹配 yaml 节点
      4. 支持 YAML->JSON、JSON->YAML 格式化和漂亮的打印 YAML(使用 cmets)
      5. 支持带有-i标志的就地编辑模式(类似于sed -i
      6. 支持使用 -C 标志(不适用于 JSON 输出)和子元素的缩进(默认为 2 个空格)为输出 YAML 着色
      7. 支持大多数 shell - Bash、zsh 的 Shell 补全(因为 spf13/cobra 的强大支持用于生成 CLI 标志)

      我对两个版本的以下 YAML(在其他答案中也有引用)的看法

      root_key1: this is value one
      root_key2: "this is value two"
      
      drink:
        state: liquid
        coffee:
          best_served: hot
          colour: brown
        orange_juice:
          best_served: cold
          colour: orange
      
      food:
        state: solid
        apple_pie:
          best_served: warm
      
      root_key_3: this is value three
      

      两种实现都要执行的各种操作(一些常用操作)

      1. 在根级别修改节点值 - 更改 root_key2 的值
      2. 修改数组内容,添加值 - 将属性添加到coffee
      3. 修改数组内容,删除值 - 从orange_juice 删除属性
      4. 打印带有路径的键/值对 - 对于food 下的所有项目

      使用 kislyuk/yq

      1. yq -y '.root_key2 |= "this is a new value"' yaml
        
      2. yq -y '.drink.coffee += { time: "always"}' yaml
        
      3. yq -y 'del(.drink.orange_juice.colour)' yaml
        
      4. yq -r '.food|paths(scalars) as $p | [($p|join(".")), (getpath($p)|tojson)] | @tsv' yaml
        

      这很简单。您只需将jq JSON 输出转码回带有-y 标志的YAML。

      使用 mikefarah/yq

      1.  yq w yaml root_key2 "this is a new value"
        
      2.  yq w yaml drink.coffee.time "always"
        
      3.  yq d yaml drink.orange_juice.colour
        
      4.  yq r yaml --printMode pv "food.**"
        

      截至今天 2020 年 12 月 21 日,yq v4 处于测试阶段,支持非常强大的路径表达式,并支持类似于使用 jq 的 DSL。阅读转换说明 - Upgrading from V3

      【讨论】:

        【解决方案5】:

        当您需要“如何使用来自 shell 脚本的 YAML/JSON/兼容数据”的解决方案时,该解决方案几乎适用于所有使用 Python 的操作系统(*nix、OSX、Windows),请考虑yamlpath,它提供几个用于读取、写入、搜索和合并 YAML、EYAML、JSON 和兼容文件的命令行工具。由于几乎每个操作系统都预装了 Python,或者安装起来很简单,这使得 yamlpath 具有高度可移植性。更有趣的是:这个项目定义了一种直观的路径语言,它具有非常强大、命令行友好的语法,可以访问一个或多个节点。

        针对您的具体问题并在使用 Python's native package manager 或您操作系统的包管理器安装 yamlpath 之后(yamlpath 可通过 RPM 用于某些操作系统):

        #!/bin/bash
        # Read values directly from YAML (or EYAML, JSON, etc) for use in this shell script:
        myShellVar=$(yaml-get --query=any.path.no[matter%how].complex source-file.yaml)
        
        # Use the value any way you need:
        echo "Retrieved ${myShellVar}"
        
        # Perhaps change the value and write it back:
        myShellVar="New Value"
        yaml-set --change=/any/path/no[matter%how]/complex --value="$myShellVar" source-file.yaml
        

        您没有指定数据是一个简单的标量值,所以让我们加大赌注。如果你想要的结果是一个数组怎么办?更具挑战性的是,如果它是一个 Array-of-Hashes 并且您只想要每个结果的一个属性怎么办?进一步假设您的数据实际上分布在 多个 YAML 文件中,并且您需要在单个查询中获得所有结果。这是一个更有趣的问题。所以,假设你有这两个 YAML 文件:

        文件:data1.yaml

        ---
        baubles:
          - name: Doohickey
            sku: 0-000-1
            price: 4.75
            weight: 2.7g
          - name: Doodad
            sku: 0-000-2
            price: 10.5
            weight: 5g
          - name: Oddball
            sku: 0-000-3
            price: 25.99
            weight: 25kg
        

        文件:data2.yaml

        ---
        baubles:
          - name: Fob
            sku: 0-000-4
            price: 0.99
            weight: 18mg
          - name: Doohickey
            price: 10.5
          - name: Oddball
            sku: 0-000-3
            description: This ball is odd
        

        在应用从 data2.yaml 到 data1.yaml 的更改(全部来自 shell 脚本)后,如何仅报告库存中每个项目的 sku?试试这个:

        #!/bin/bash
        baubleSKUs=($(yaml-merge --aoh=deep data1.yaml data2.yaml | yaml-get --query=/baubles/sku -))
        
        for sku in "${baubleSKUs[@]}"; do
            echo "Found bauble SKU:  ${sku}"
        done
        

        您只需几行代码即可获得所需的内容:

        Found bauble SKU:  0-000-1
        Found bauble SKU:  0-000-2
        Found bauble SKU:  0-000-3
        Found bauble SKU:  0-000-4
        

        如您所见,yamlpath 将非常复杂的问题转化为微不足道的解决方案。请注意,整个查询是作为流处理的;查询没有更改任何 YAML 文件,也没有临时文件。

        我意识到这是“解决同一问题的又一个工具”,但在阅读了此处的其他答案后,yamlpath 似乎比大多数替代方案更便携和健壮。它还完全理解 YAML/JSON/兼容文件,并且它确实不需要需要将 YAML 转换为 JSON 来执行请求的操作。因此,只要您需要更改源 YAML 文件中的数据,就会保留原始 YAML 文件中的 cmets。像一些替代方案一样,yamlpath 也可以跨操作系统移植。更重要的是,yamlpath 定义了一种非常强大的查询语言,可以实现非常专业化/过滤的数据查询。它甚至可以在单个查询中针对文件不同部分的结果进行操作。

        如果您想一次获取或设置数据中的多个值(包括哈希/数组/地图/列表等复杂数据),yamlpath 可以做到这一点。想要一个值但不知道它在文档中的确切位置? yamlpath 可以找到它并为您提供确切的路径。需要将多个数据文件合并在一起,包括来自 STDIN 的? yamlpath 也这样做。此外,yamlpath 完全理解 YAML 锚点及其别名,始终准确地提供或更改您期望的数据,无论它是具体值还是引用值。

        免责声明:我编写和维护 yamlpath,它基于 ruamel.yaml,而 ruamel.yaml 又基于 PyYAML。因此,yamlpath 完全符合标准。

        【讨论】:

          【解决方案6】:

          鉴于 Python3 和 PyYAML 现在是很容易满足的依赖项,以下可能会有所帮助:

          yaml() {
              python3 -c "import yaml;print(yaml.safe_load(open('$1'))$2)"
          }
          
          VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")
          

          【讨论】:

          • 我喜欢 shyaml,但在断开连接的系统上,这是一个救命稻草。也应该适用于绝大多数 python2,例如 RHEL。
          • 也许使用yaml.safe_load,因为它更安全。 pyyaml.org/wiki/PyYAMLDocumentation
          • 被低估的答案
          • 这很棒。我制作了一个经过调整的版本,将数组打印为每行一个项目。 python3 -c "import yaml;f=yaml.safe_load(open('$1'))$2;print('\n'.join(str(i) for i in f) if type(f)==list else f);"
          【解决方案7】:

          我知道我的回答很具体,但如果已经安装了 PHPSymfony,那么使用 Symfony 的 YAML 解析器会非常方便。

          例如:

          php -r "require '$SYMFONY_ROOT_PATH/vendor/autoload.php'; \
              var_dump(\Symfony\Component\Yaml\Yaml::parse(file_get_contents('$YAML_FILE_PATH')));"
          

          这里我只是使用var_dump 来输出解析后的数组,但当然你可以做更多... :)

          【讨论】:

            【解决方案8】:

            如果您需要单个值,您可以使用一个工具将您的 YAML 文档转换为 JSON 并提供给 jq,例如 yq

            sample.yaml 的内容:

            ---
            bob:
              item1:
                cats: bananas
              item2:
                cats: apples
              thing:
                cats: oranges
            

            例子:

            $ yq -r '.bob["thing"]["cats"]' sample.yaml 
            oranges
            

            【讨论】:

              【解决方案9】:

              您可以使用用 golang 编写的 yq 中的 equivalent

              ./go-yg -yamlFile /home/user/dev/ansible-firefox/defaults/main.yml -key
              firefox_version
              

              返回:

              62.0.3
              

              【讨论】:

                【解决方案10】:

                这里是 Stefan Farestam 答案的扩展版本:

                function parse_yaml {
                   local prefix=$2
                   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);
                      }
                   }'
                }
                

                此版本支持- 表示法以及字典和列表的短表示法。以下输入:

                global:
                  input:
                    - "main.c"
                    - "main.h"
                  flags: [ "-O3", "-fpic" ]
                  sample_input:
                    -  { property1: value, property2: "value2" }
                    -  { property1: "value3", property2: 'value 4' }
                

                产生这个输出:

                global_input_1="main.c"
                global_input_2="main.h"
                global_flags_1="-O3"
                global_flags_2="-fpic"
                global_sample_input_1_property1="value"
                global_sample_input_1_property2="value2"
                global_sample_input_2_property1="value3"
                global_sample_input_2_property2="value 4"
                

                如您所见,- 项目会自动编号,以便为每个项目获取不同的变量名称。在bash 中没有多维数组,因此这是一种解决方法。支持多个级别。 要解决@briceburg 提到的尾随空格问题,应将值括在单引号或双引号中。但是,仍然存在一些限制:当值包含逗号时,字典和列表的扩展可能会产生错误的结果。此外,(尚)不支持更复杂的结构,例如跨越多行的值(如 ssh 键)。

                关于代码的几句话:第一个sed 命令将字典{ key: value, ...} 的缩写形式扩展为常规并将它们转换为更简单的yaml 样式。第二个sed 调用对列表的短符号执行相同的操作,并将[ entry, ... ] 转换为带有- 符号的逐项列表。第三个sed 调用是处理普通字典的原始调用,现在增加了带有- 和缩进的处理列表。 awk 部分为每个缩进级别引入了一个索引,并在变量名称为空时(即处理列表时)增加它。使用计数器的当前值而不是空的 vname。上升一级时,计数器归零。

                编辑:我为此创建了一个github repository

                【讨论】:

                  【解决方案11】:

                  如果你有 python 2 和 PyYAML,你可以使用我写的这个解析器 parse_yaml.py。它所做的一些更简洁的事情是让您选择一个前缀(如果您有多个具有相似变量的文件)并从 yaml 文件中选择一个值。

                  例如,如果您有这些 yaml 文件:

                  staging.yaml:

                  db:
                      type: sqllite
                      host: 127.0.0.1
                      user: dev
                      password: password123
                  

                  prod.yaml:

                  db:
                      type: postgres
                      host: 10.0.50.100
                      user: postgres
                      password: password123
                  

                  您可以同时加载两者而不会发生冲突。

                  $ eval $(python parse_yaml.py prod.yaml --prefix prod --cap)
                  $ eval $(python parse_yaml.py staging.yaml --prefix stg --cap)
                  $ echo $PROD_DB_HOST
                  10.0.50.100
                  $ echo $STG_DB_HOST
                  127.0.0.1
                  

                  甚至可以挑选你想要的值。

                  $ prod_user=$(python parse_yaml.py prod.yaml --get db_user)
                  $ prod_port=$(python parse_yaml.py prod.yaml --get db_port --default 5432)
                  $ echo prod_user
                  postgres
                  $ echo prod_port
                  5432
                  

                  【讨论】:

                    【解决方案12】:

                    我知道这非常具体,但我认为我的回答可能对某些用户有所帮助。
                    如果您的机器上安装了nodenpm,则可以使用js-yaml
                    首次安装:

                    npm i -g js-yaml
                    # or locally
                    npm i js-yaml
                    

                    然后在你的 bash 脚本中

                    #!/bin/bash
                    js-yaml your-yaml-file.yml
                    

                    如果你使用jq,你也可以这样做

                    #!/bin/bash
                    json="$(js-yaml your-yaml-file.yml)"
                    aproperty="$(jq '.apropery' <<< "$json")"
                    echo "$aproperty"
                    

                    因为js-yaml 将 yaml 文件转换为 json 字符串文字。然后,您可以将该字符串与您的 unix 系统中的任何 json 解析器一起使用。

                    【讨论】:

                      【解决方案13】:

                      另一种选择是将 YAML 转换为 JSON,然后使用 jq 与 JSON 表示进行交互,以从中提取信息或对其进行编辑。

                      我编写了一个包含这种胶水的简单 bash 脚本 - 请参阅 Y2J project on GitHub

                      【讨论】:

                        【解决方案14】:

                        可以将一个小脚本传递给一些解释器,比如 Python。使用 Ruby 及其 YAML 库的一种简单方法如下:

                        $ RUBY_SCRIPT="data = YAML::load(STDIN.read); puts data['a']; puts data['b']"
                        $ echo -e '---\na: 1234\nb: 4321' | ruby -ryaml -e "$RUBY_SCRIPT"
                        1234
                        4321
                        

                        ,其中data 是带有来自 yaml 的值的哈希(或数组)。

                        作为奖励,它可以很好地解析 Jekyll's front matter

                        ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)['tags']" example.md
                        

                        【讨论】:

                        • 可以用吗?您已通过 echo 将 yaml 放入 ruby​​ 解释器。但是在 bash 脚本的其余部分应该如何使用这个变量呢?
                        • 是的,它可以使用。 RUBY_SCRIPT 变量是一个 ruby​​ 脚本,可以改为写入文件(使用 ruby -ryaml &lt;rubyscript_filename&gt; 运行)。它包含将输入文本转换为一些输出文本的逻辑,在内部将内容存储到data 变量中。 echo 输出一个 yaml 文本,但您可以使用 cat &lt;yaml_filename&gt; 代替管道传输文件的内容。
                        • 很抱歉,我在上面的示例中没有看到这一点。首先,变量 RUBY_SCRIPT 保存 ruby​​ 解释器的代码。接下来 echo -e 模拟任何 yaml 数据,这是通过堆重定向到 ruby​​ 解释器的。这将 ruby​​ 代码调用为内联脚本,最后打印输出示例 'a' 和 'b' 变量。那么他的其余可执行代码在哪里将变量加载到 bash 中?我只看到一种解决方法。将 ruby​​ outout 放入temporary_file,这应该包含行: variable='value' ,然后通过'.临时文件'。但这是解决方法,而不是解决方法。
                        • @Znik 一旦你在标准输出上得到了一些东西,由标准输入提供的东西产生,其余的依赖于 bash 编码器的手中(提醒一下,如果你需要 stdout 被喂到变量中,您不必依赖临时文件!使用x=$(...) 甚至read a b c &lt; &lt;(...))。因此,当您确切地知道要在 YAML 文件中获取什么并且知道如何编写 ruby​​ 行来访问这些数据时,这是一个有效的解决方案。即使它很粗糙,它也是恕我直言的概念的完整证明。尽管如此,它确实没有为您提供完整的 bash 抽象。
                        • 是的。你很正经谢谢你的诡计。使用一个变量很简单。但许多 warables 不是。读取变量列表的技巧
                        【解决方案15】:

                        我的用例可能与原始帖子所要求的完全一样,也可能不完全相同,但绝对相似。

                        我需要引入一些 YAML 作为 bash 变量。 YAML 的深度永远不会超过一级。

                        YAML 看起来像这样:

                        KEY:                value
                        ANOTHER_KEY:        another_value
                        OH_MY_SO_MANY_KEYS: yet_another_value
                        LAST_KEY:           last_value
                        

                        输出类似dis:

                        KEY="value"
                        ANOTHER_KEY="another_value"
                        OH_MY_SO_MANY_KEYS="yet_another_value"
                        LAST_KEY="last_value"
                        

                        我用这一行实现了输出:

                        sed -e 's/:[^:\/\/]/="/g;s/$/"/g;s/ *=/=/g' file.yaml > file.sh
                        
                        • s/:[^:\/\/]/="/g 找到 : 并将其替换为 =",同时忽略 ://(对于 URL)
                        • s/$/"/g" 附加到每一行的末尾
                        • s/ *=/=/g 删除 = 之前的所有空格

                        【讨论】:

                        • 不确定您的意思,但如果您的意思是这不适用于所有 YAML,那么您是对的。这就是为什么我开了几个资格。我只是分享了对我的用例有用的东西,因为它在当时比其他任何人都更好地回答了这个问题。这绝对可以扩展。
                        • 对代码注入也有点开放,但正如你所说,这是向前迈出的一步
                        • 我只编写过在本地使用的 shell 脚本,所以这对我来说不是问题。但是,如果您知道如何保护它和/或想详细说明,我将不胜感激。
                        • 一级深 yaml 有多种形式 — 值可以拆分为以下缩进行;值可以以多种方式引用,shell 不会解析;一切都可以用大括号写在一行上:{KEY: 'value', ...};可能还有其他人。最重要的是,如果您打算将结果评估为 shell 代码,那将是非常不安全的。
                        • 如果您的 yaml 文件顶部有 ---,请使用此表达式将其删除:sed -e '/^---$/d;s/:[^:\/\/]/="/g;s/$/"/g;s/ *=/=/g' /^---$/d;... 是额外的位。
                        【解决方案16】:

                        您也可以考虑使用Grunt(JavaScript 任务运行器)。可以很容易地与外壳集成。它支持读取 YAML (grunt.file.readYAML) 和 JSON (grunt.file.readJSON) 文件。

                        这可以通过在Gruntfile.js(或Gruntfile.coffee)中创建一个任务来实现,例如:

                        module.exports = function (grunt) {
                        
                            grunt.registerTask('foo', ['load_yml']);
                        
                            grunt.registerTask('load_yml', function () {
                                var data = grunt.file.readYAML('foo.yml');
                                Object.keys(data).forEach(function (g) {
                                  // ... switch (g) { case 'my_key':
                                });
                            });
                        
                        };
                        

                        然后从 shell 只需简单地运行 grunt foo(检查 grunt --help 以获取可用任务)。

                        此外,您还可以使用从您的任务 (foo: { cmd: 'echo bar &lt;%= foo %&gt;' }) 传递的输入变量来实现 exec:foo 任务 (grunt-exec),以便以您想要的任何格式打印输出,然后将其通过管道传递给另一个命令。


                        还有一个与 Grunt 类似的工具,叫做gulp,带有额外的插件gulp-yaml

                        通过以下方式安装:npm install --save-dev gulp-yaml

                        示例用法:

                        var yaml = require('gulp-yaml');
                        
                        gulp.src('./src/*.yml')
                          .pipe(yaml())
                          .pipe(gulp.dest('./dist/'))
                        
                        gulp.src('./src/*.yml')
                          .pipe(yaml({ space: 2 }))
                          .pipe(gulp.dest('./dist/'))
                        
                        gulp.src('./src/*.yml')
                          .pipe(yaml({ safe: true }))
                          .pipe(gulp.dest('./dist/'))
                        

                        要处理YAML format 的更多选项,请查看YAML site 以获取可帮助您解析该格式的可用项目、库和其他资源。


                        其他工具:

                        • Jshon

                          解析、读取和创建 JSON

                        【讨论】:

                          【解决方案17】:

                          我刚刚编写了一个解析器,我称之为 Yay!Yaml 不是 Yamlesque!),它解析 Yamlesque,它是YAML。因此,如果您正在为 Bash 寻找 100% 兼容的 YAML 解析器,那么这不是它。但是,引用 OP,如果您想要一个结构化的配置文件,该文件对于非技术用户来说尽可能容易编辑,它类似于 YAML,这可能会引起您的兴趣。

                          它是 inspred by the earlier answer,但写入关联数组(是的,它需要 Bash 4.x)而不是基本变量。这样做的方式是允许在事先不知道键的情况下解析数据,以便编写数据驱动的代码。

                          除了键/值数组元素之外,每个数组还有一个包含键名称列表的keys 数组、一个包含子数组名称的children 数组和一个引用其父数组的parent 键。

                          This是Yamlesque的一个例子:

                          root_key1: this is value one
                          root_key2: "this is value two"
                          
                          drink:
                            state: liquid
                            coffee:
                              best_served: hot
                              colour: brown
                            orange_juice:
                              best_served: cold
                              colour: orange
                          
                          food:
                            state: solid
                            apple_pie:
                              best_served: warm
                          
                          root_key_3: this is value three
                          

                          Here 是一个展示如何使用它的示例:

                          #!/bin/bash
                          # An example showing how to use Yay
                          
                          . /usr/lib/yay
                          
                          # helper to get array value at key
                          value() { eval echo \${$1[$2]}; }
                          
                          # print a data collection
                          print_collection() {
                            for k in $(value $1 keys)
                            do
                              echo "$2$k = $(value $1 $k)"
                            done
                          
                            for c in $(value $1 children)
                            do
                              echo -e "$2$c\n$2{"
                              print_collection $c "  $2"
                              echo "$2}"
                            done
                          }
                          
                          yay example
                          print_collection example
                          

                          哪个输出:

                          root_key1 = this is value one
                          root_key2 = this is value two
                          root_key_3 = this is value three
                          example_drink
                          {
                            state = liquid
                            example_coffee
                            {
                              best_served = hot
                              colour = brown
                            }
                            example_orange_juice
                            {
                              best_served = cold
                              colour = orange
                            }
                          }
                          example_food
                          {
                            state = solid
                            example_apple_pie
                            {
                              best_served = warm
                            }
                          }
                          

                          here 是解析器:

                          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%.*}
                             }
                          
                             echo "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" |
                             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 (length(value) > 0) {
                                   # value
                                   printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
                                   printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
                                } else {
                                   # collection
                                   printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
                                   printf("declare -g -A %s%s;\n", root_prefix, key);
                                   printf("%s%s[parent]=\"%s%s\";\n", root_prefix, key, prefix, parent_key);
                                }
                             }'
                          }
                          
                          # helper to load yay data file
                          yay() { eval $(yay_parse "$@"); }
                          

                          链接的源文件中有一些文档,下面是对代码作用的简短说明。

                          yay_parse 函数首先定位 input 文件或以退出状态 1 退出。接下来,它确定数据集 prefix,无论是明确指定还是从文件名派生。

                          它将有效的bash 命令写入其标准输出,如果执行,则定义表示输入数据文件内容的数组。其中第一个定义了顶级数组:

                          echo "declare -g -A $prefix;"
                          

                          请注意,数组声明是关联的 (-A),这是 Bash 版本 4 的一个特性。声明也是全局的 (-g),因此它们可以在函数中执行,但可以在全局范围内使用,例如 @ 987654344@帮手:

                          yay() { eval $(yay_parse "$@"); }
                          

                          输入数据最初使用sed 处理。在使用 ASCII File Separator 字符分隔有效的 Yamlesque 字段并删除值字段周围的所有双引号之前,它会删除与 Yamlesque 格式规范不匹配的行。

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

                          这两种表达方式相似;它们的不同之处仅在于第一个选择引用的值,而第二个选择未引用的值。

                          之所以使用File Separator (28/hex 12/octal 034),是因为作为不可打印字符,它不太可能出现在输入数据中。

                          结果被传送到awk,它一次处理一行输入。它使用FS 字符将每个字段分配给一个变量:

                          indent       = length($1)/2;
                          key          = $2;
                          value        = $3;
                          

                          所有行都有一个缩进(可能为零)和一个键,但它们并不都有值。它计算将包含前导空格的第一个字段的长度除以 2 的行的缩进级别。没有任何缩进的顶级项目的缩进级别为零。

                          接下来,它会计算出prefix 用于当前项目的内容。这就是添加到键名以创建数组名的内容。顶层数组有一个root_prefix,定义为数据集名称和下划线:

                          root_prefix  = "'$prefix'_";
                          if (indent ==0 ) {
                            prefix = "";          parent_key = "'$prefix'";
                          } else {
                            prefix = root_prefix; parent_key = keys[indent-1];
                          }
                          

                          parent_key 是当前行缩进级别之上缩进级别的键,表示当前行所属的集合。集合的键/值对将存储在一个数组中,其名称定义为prefixparent_key 的串联。

                          对于顶级(缩进级别为零),数据集前缀用作父键,因此它没有前缀(设置为"")。所有其他数组都以根前缀为前缀。

                          接下来,当前键被插入到包含这些键的(awk 内部)数组中。该数组在整个 awk 会话中持续存在,因此包含由先前行插入的键。使用其缩进作为数组索引将键插入到数组中。

                          keys[indent] = key;
                          

                          因为这个数组包含前几行的键,所以任何缩进级别大于当前行缩进级别的键都会被删除:

                           for (i in keys) {if (i > indent) {delete keys[i]}}
                          

                          这会留下包含从缩进级别 0 的根到当前行的密钥链的 keys 数组。当前一行缩进比当前行更深时,它会删除保留的陈旧键。

                          最后一部分输出bash 命令:没有值的输入行开始一个新的缩进级别(YAML 用语中的 collection),一个有值的输入行将一个键添加到当前收藏。

                          集合的名称是当前行的prefixparent_key 的串联。

                          当一个键有一个值时,具有该值的键被分配给当前集合,如下所示:

                          printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
                          printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
                          

                          第一个语句输出将值分配给以键命名的关联数组元素的命令,第二个语句输出将键添加到集合的空格分隔的keys 列表的命令:

                          <current_collection>[<key>]="<value>";
                          <current_collection>[keys]+=" <key>";
                          

                          当一个键没有值时,一个新的集合会像这样开始:

                          printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
                          printf("declare -g -A %s%s;\n", root_prefix, key);
                          

                          第一个语句输出将新集合添加到当前集合的空格分隔的children 列表的命令,第二个语句输出为新集合声明新关联数组的命令:

                          <current_collection>[children]+=" <new_collection>"
                          declare -g -A <new_collection>;
                          

                          yay_parse 的所有输出都可以通过 bash evalsource 内置命令解析为 bash 命令。

                          【讨论】:

                          • 你考虑过在 GitHub 上做这个项目吗?还是已经有了?
                          • @daniel,它在 GitHub 中,但不在它自己的仓库中 - 你可以在 here 中找到它。请参阅examplesusr/lib 目录,这些都链接在我对问题的回答中。如果有兴趣,我可以将其分解为自己的回购。
                          • 向 YAY 致敬。起初,我将它重写为纯 bash,但后来我无法阻止自己并将其重新实现为一个基本的解析器,支持数组和嵌套结构,不能踩到彼此的名字。它位于github.com/binaryphile/y2s
                          【解决方案18】:

                          我已经在 python 中编写了shyaml,以满足来自 shell 命令行的 YAML 查询需求。

                          概述:

                          $ pip install shyaml      ## installation
                          

                          示例的 YAML 文件(具有复杂功能):

                          $ cat <<EOF > test.yaml
                          name: "MyName !!"
                          subvalue:
                              how-much: 1.1
                              things:
                                  - first
                                  - second
                                  - third
                              other-things: [a, b, c]
                              maintainer: "Valentin Lab"
                              description: |
                                  Multiline description:
                                  Line 1
                                  Line 2
                          EOF
                          

                          基本查询:

                          $ cat test.yaml | shyaml get-value subvalue.maintainer
                          Valentin Lab
                          

                          对复杂值进行更复杂的循环查询:

                          $ cat test.yaml | shyaml values-0 | \
                            while read -r -d $'\0' value; do
                                echo "RECEIVED: '$value'"
                            done
                          RECEIVED: '1.1'
                          RECEIVED: '- first
                          - second
                          - third'
                          RECEIVED: '2'
                          RECEIVED: 'Valentin Lab'
                          RECEIVED: 'Multiline description:
                          Line 1
                          Line 2'
                          

                          几个关键点:

                          • 所有 YAML 类型和语法异常都得到正确处理,如多行、带引号的字符串、内联序列...
                          • \0 填充输出可用于可靠的多行输入操作。
                          • 用于选择子值的简单点表示法(即:subvalue.maintainer 是有效键)。
                          • 为序列提供索引访问(即:subvalue.things.-1subvalue.things 序列的最后一个元素。)
                          • 一次性访问所有序列/结构元素以用于 bash 循环。
                          • 您可以将 YAML 文件的整个子部分输出为 ... YAML,它可以很好地与 shyaml 进行进一步操作。

                          更多示例和文档可通过shyaml github pageshyaml PyPI page 获得。

                          【讨论】:

                          • 这太棒了!如果有一个标志可以忽略输出中为空白的 yaml 值,那就太好了。现在它输出“null”。我将它与 envdir 一起使用以将 docker-compose 文件输出到 envdir cat docker-compose.yml | shyaml get-value api.environment | grep -v null | awk -F': ' '{print $2 &gt; ("envdir/" $1)}'
                          • @JiminyCricket 请使用 github 问题页面!至少我会很高兴跟踪这一点。 ;)
                          • 不幸的是,shyaml 速度慢得离谱
                          【解决方案19】:

                          这是一个仅 bash 的解析器,它利用 sed 和 awk 来解析简单的 yaml 文件:

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

                          它可以理解以下文件:

                          ## global definitions
                          global:
                            debug: yes
                            verbose: no
                            debugging:
                              detailed: no
                              header: "debugging started"
                          
                          ## output
                          output:
                             file: "yes"
                          

                          当使用以下方法解析时:

                          parse_yaml sample.yml
                          

                          将输出:

                          global_debug="yes"
                          global_verbose="no"
                          global_debugging_detailed="no"
                          global_debugging_header="debugging started"
                          output_file="yes"
                          

                          它还理解由 ruby​​ 生成的 yaml 文件,其中可能包含 ruby​​ 符号,例如:

                          ---
                          :global:
                            :debug: 'yes'
                            :verbose: 'no'
                            :debugging:
                              :detailed: 'no'
                              :header: debugging started
                            :output: 'yes'
                          

                          并且将输出与上一个示例相同的输出。

                          脚本中的典型用法是:

                          eval $(parse_yaml sample.yml)
                          

                          parse_yaml 接受一个前缀参数,以便导入的设置都有一个共同的前缀(这将降低命名空间冲突的风险)。

                          parse_yaml sample.yml "CONF_"
                          

                          产量:

                          CONF_global_debug="yes"
                          CONF_global_verbose="no"
                          CONF_global_debugging_detailed="no"
                          CONF_global_debugging_header="debugging started"
                          CONF_output_file="yes"
                          

                          请注意,文件中以前的设置可以被以后的设置引用:

                          ## global definitions
                          global:
                            debug: yes
                            verbose: no
                            debugging:
                              detailed: no
                              header: "debugging started"
                          
                          ## output
                          output:
                             debug: $global_debug
                          

                          另一个不错的用法是先解析默认文件,然后解析用户设置,因为后者的设置会覆盖第一个设置:

                          eval $(parse_yaml defaults.yml)
                          eval $(parse_yaml project.yml)
                          

                          【讨论】:

                          • 酷斯特凡!如果它也能将 yaml - 符号转换为原生 bash 数组,那就太棒了!
                          • 如果您更改 awk 脚本中的 printf 行,这应该很容易做到。请注意,尽管 bash 不支持多维关联数组,因此您最终会得到一个数组 + 每个值一个键。嗯,应该把它移到 github...
                          • 这需要 2 个空格的标准 yml 缩进。如果您使用 4 个空格,则变量将得到两个下划线作为分隔符,例如global__debug 而不是 global_debug
                          • 嗨 vaab - 虽然我确信您是正确的,许多读者都希望从 shell 解析真正的 YAML 文件,但(至少对我而言)结果不是很清楚。通过这个脚本,我解决了这个问题,并定义了一个子集,该子集具有到标准变量的合理映射。肯定没有假装解决了解析真实 YAML 文件的更大问题。
                          • 它只在屏幕上打印输出。以后如何访问这些值?
                          【解决方案20】:
                          perl -ne 'chomp; printf qq/%s="%s"\n/, split(/\s*:\s*/,$_,2)' file.yml > file.sh
                          

                          【讨论】:

                          • 仅对平面配置有用。它不适用于结构化 yaml。另,如何防止使用临时file.sh?
                          【解决方案21】:

                          很难说,因为这取决于您希望解析器从您的 YAML 文档中提取什么。对于简单的情况,您可以使用grepcutawk 等。对于更复杂的解析,您需要使用成熟的解析库,例如 Python 的 PyYAMLYAML::Perl

                          【讨论】:

                            猜你喜欢
                            • 2014-05-05
                            • 1970-01-01
                            • 2012-07-10
                            • 1970-01-01
                            • 1970-01-01
                            • 2016-06-21
                            • 1970-01-01
                            相关资源
                            最近更新 更多