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


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


可以将一个小脚本传递给一些解释器,比如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的值的散列(或数组)。

作为奖励,它可以很好地解析杰基尔的正面问题。

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

我的用例可能与这篇原始文章所要求的完全相同,也可能不完全相同,但它肯定是相似的。

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

YAML看起来是这样的:

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

输出如下:

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删除=前面的所有空格


我已经用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'

以下几个要点:

all YAML types and syntax oddities are correctly handled, as multiline, quoted strings, inline sequences... \0 padded output is available for solid multiline entry manipulation. simple dotted notation to select sub-values (ie: subvalue.maintainer is a valid key). access by index is provided to sequences (ie: subvalue.things.-1 is the last element of the subvalue.things sequence.) access to all sequence/structs elements in one go for use in bash loops. you can output whole subpart of a YAML file as ... YAML, which blend well for further manipulations with shyaml.

更多的示例和文档可以在shyaml github页面或shyaml PyPI页面上找到。


perl -ne 'chomp; printf qq/%s="%s"\n/, split(/\s*:\s*/,$_,2)' file.yml > file.sh

下面是一个bash-only解析器,利用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)

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

它受到前面答案的启发,但编写了关联数组(是的,它需要Bash 4.x)而不是基本变量。它以一种允许在不事先了解键的情况下解析数据的方式进行操作,从而可以编写数据驱动的代码。

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

这是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

下面是一个如何使用它的例子:

#!/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
  }
}

下面是解析器:

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函数首先定位输入文件或退出,退出状态为1。接下来,它确定数据集前缀,要么显式指定,要么从文件名派生。

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

echo "declare -g -A $prefix;"

注意,数组声明是关联的(-A),这是Bash版本4的一个特性。声明也是全局的(-g),所以它们可以在函数中执行,但像yay helper一样可用于全局作用域:

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

最初使用sed处理输入数据。它删除不匹配Yamlesque格式规范的行,然后用ASCII文件分隔符分隔有效的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" |

这两种表达是相似的;它们的不同之处在于第一个选择了带引号的值,而第二个选择了不带引号的值。

使用文件分隔符(28/十六进制12/八进制034)是因为,作为一个不可打印字符,它不太可能出现在输入数据中。

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

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

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

接下来,它计算出为当前项使用什么前缀。这是添加到键名中以创建数组名的内容。顶级数组有一个root_prefix,它被定义为数据集名称和一个下划线:

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

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

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

接下来,将当前键插入到包含键的(awk-internal)数组中。该数组在整个awk会话中持续存在,因此包含先前行插入的键。键以其缩进作为数组索引插入数组。

keys[indent] = key;

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

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

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

最后一部分输出bash命令:不带值的输入行开始一个新的缩进级别(在YAML中是一个集合),带值的输入行向当前集合添加一个键。

集合的名称是当前行的前缀和parent_key的组合。

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

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

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

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

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

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

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


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

我写了一个简单的bash脚本,包含这个胶水-见Y2J项目在GitHub上


你也可以考虑使用Grunt (JavaScript任务运行器)。可以很容易地与shell集成。它支持读取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是否有可用的任务)。

此外,你可以实现exec:foo任务(grunt-exec)与输入变量从你的任务(foo: {cmd: 'echo bar <%= foo %>'})为了打印输出在任何格式你想要的,然后管道到另一个命令。


还有一个类似于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格式的选项,请查看YAML网站上可用的项目、库和其他资源,这些资源可以帮助您解析该格式。


其他工具:

Jshon 解析、读取和创建JSON


考虑到Python3和PyYAML是非常容易满足的依赖关系,下面的代码可能会有所帮助:

yaml() {
    python3 -c "import yaml;print(yaml.safe_load(open('$1'))$2)"
}

VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")

我知道这是非常具体的,但我认为我的回答可能对某些用户有帮助。 如果你的机器上安装了node和npm,你可以使用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解析器中使用该字符串。


如果你有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

以下是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"

as you can see the - items automatically get numbered in order to obtain different variable names for each item. In bash there are no multidimensional arrays, so this is one way to work around. Multiple levels are supported. To work around the problem with trailing white spaces mentioned by @briceburg one should enclose the values in single or double quotes. However, there are still some limitations: Expansion of the dictionaries and lists can produce wrong results when values contain commas. Also, more complex structures like values spanning multiple lines (like ssh-keys) are not (yet) supported.

A few words about the code: The first sed command expands the short form of dictionaries { key: value, ...} to regular and converts them to more simple yaml style. The second sed call does the same for the short notation of lists and converts [ entry, ... ] to an itemized list with the - notation. The third sed call is the original one that handled normal dictionaries, now with the addition to handle lists with - and indentations. The awk part introduces an index for each indentation level and increases it when the variable name is empty (i.e. when processing a list). The current value of the counters are used instead of the empty vname. When going up one level, the counters are zeroed.

编辑:我已经为此创建了一个github存储库。


你可以用golang写成yq的等价形式:

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

返回:

62.0.3

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

这个项目的目标是yaml文件的jq或sed。

(https://github.com/mikefarah/yq #自述)

作为示例(直接从文档中窃取),给出一个示例。Yaml文件:

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

then

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

将输出

- bananas
- apples

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

sample.yaml的内容:

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

例子:

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

我知道我的回答很具体,但是如果已经安装了PHP和Symfony,那么使用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来输出解析后的数组,当然你还可以做更多…:)


Whenever you need a solution for "How to work with YAML/JSON/compatible data from a shell script" which works on just about every OS with Python (*nix, OSX, Windows), consider yamlpath, which provides several command-line tools for reading, writing, searching, and merging YAML, EYAML, JSON, and compatible files. Since just about every OS either comes with Python pre-installed or it is trivial to install, this makes yamlpath highly portable. Even more interesting: this project defines an intuitive path language with very powerful, command-line-friendly syntax that enables accessing one or more nodes.

针对您的具体问题,在使用Python的本地包管理器或您的操作系统的包管理器安装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

不过,您没有指定数据是一个简单的Scalar值,因此让我们提高赌注。如果你想要的结果是一个数组呢?更有挑战性的是,如果它是一个哈希数组,而你只想要每个结果的一个属性呢?进一步假设您的数据实际上分布在多个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

在应用数据2的更改后,如何仅报告库存中每个项目的sku。Yaml到data1。Yaml,所有从一个shell脚本?试试这个:

#!/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文件,也没有临时文件。

I realize this is "yet another tool to solve the same question" but after reading the other answers here, yamlpath appears more portable and robust than most alternatives. It also fully understands YAML/JSON/compatible files and it does not need to convert YAML to JSON to perform requested operations. As such, comments within the original YAML file are preserved whenever you need to change data in the source YAML file. Like some alternatives, yamlpath is also portable across OSes. More importantly, yamlpath defines a query language that is extremely powerful, enabling very specialized/filtered data queries. It can even operate against results from disparate parts of the file in a single query.

If you want to get or set many values in the data at once -- including complex data like hashes/arrays/maps/lists -- yamlpath can do that. Want a value but don't know precisely where it is in the document? yamlpath can find it and give you the exact path(s). Need to merge multiple data file together, including from STDIN? yamlpath does that, too. Further, yamlpath fully comprehends YAML anchors and their aliases, always giving or changing exactly the data you expect whether it is a concrete or referenced value.

免责声明:我编写并维护了yamlpath,它是基于ruamel的。yaml是基于PyYAML的。因此,yamlpath完全符合标准。


把我的答案从如何在bash中将json响应转换为yaml,因为这似乎是关于从命令行处理yaml文本解析的权威帖子。

我想添加一些关于yq YAML实现的细节。由于这个YAML解析器有两种实现,名称都是yq,如果不查看实现的DSL,就很难区分使用的是哪一种。有两个可用的实现

kislyuk/yq——更常被提及的版本,它是jq的包装器,用Python编写,使用PyYAML库进行YAML解析 mikefarah/yq -一个Go实现,使用Go -yaml v3解析器,有自己的动态DSL。

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

kislyuk/yq -安装说明 mikefarah/yq -安装说明

这两个版本都有一些优点和缺点,但有一些有效的点需要强调(从他们的回购指令中采用)

kislyuk - yq

Since the DSL is the adopted completely from jq, for users familiar with the latter, the parsing and manipulation becomes quite straightforward Supports mode to preserve YAML tags and styles, but loses comments during the conversion. Since jq doesn't preserve comments, during the round-trip conversion, the comments are lost. As part of the package, XML support is built in. An executable, xq, which transcodes XML to JSON using xmltodict and pipes it to jq, on which you can apply the same DSL to perform CRUD operations on the objects and round-trip the output back to XML. Supports in-place edit mode with -i flag (similar to sed -i)

迈克法拉/YQ

Prone to frequent changes in DSL, migration from 2.x - 3.x Rich support for anchors, styles and tags. But lookout for bugs once in a while A relatively simple Path expression syntax to navigate and match yaml nodes Supports YAML->JSON, JSON->YAML formatting and pretty printing YAML (with comments) Supports in-place edit mode with -i flag (similar to sed -i) Supports coloring the output YAML with -C flag (not applicable for JSON output) and indentation of the sub elements (default at 2 spaces) Supports Shell completion for most shells - Bash, zsh (because of powerful support from spf13/cobra used to generate CLI flags)


我对以下两个版本的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

对这两个实现执行的各种操作(一些常用操作)

修改根节点值—修改“root_key2”的值 修改数组内容,增加值-为coffee添加属性 修改数组内容,删除value - Delete属性从orange_juice 打印带有路径的键/值对—用于food下的所有项目

使用kislyuk / yq

Yq -y '。Root_key2 |= "this is a new value 你,你,喝。咖啡+={时间:"always"}' yaml Yq -y 'del(.drink.orange_juice.colour)' yaml yq - r的.food |路径(标量)美元p | (($ p |加入(“。”)),(getpath ($ p) | tojson)] | @tsv的yaml

这很简单。你所需要做的就是用-y标志将jq JSON输出转码回YAML。

用mikefarah - yq

Yq w yaml root_key2 "这是一个新值" Yq w yaml喝。咖啡。时间“总是” Yq d yaml饮料。橙汁。颜色 yq r yaml——printMode pv "food.**"


截至2020年12月21日,yq v4是测试版,支持许多强大的路径表达式,并支持类似于使用jq的DSL。阅读过渡说明-从V3升级


现在做这件事的一个快速方法(以前的方法对我没用):

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

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

如果您希望将所有YAML值解析为bash值,请尝试此脚本。这也可以处理注释。参见下面的示例用法:

# 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

如果您知道您感兴趣的标记和您期望的yaml结构,那么在Bash中编写一个简单的yaml解析器并不难。

在下面的示例中,解析器将一个结构化YAML文件读入环境变量、数组和关联数组。

注意:这个解析器的复杂性与YAML文件的结构有关。对于YAML文件的每个结构化组件,都需要一个单独的子例程。高度结构化的YAML文件可能需要更复杂的方法,例如通用的递归下降解析器。

圣诞节。yaml文件:

# Xmas YAML example
---
 # Values
 pear-tree: partridge
 turtle-doves: 2.718
 french-hens: 3

 # Array
 calling-birds:
   - huey
   - dewey
   - louie
   - fred

 # Structure
 xmas-fifth-day:
   calling-birds: four
   french-hens: 3
   golden-rings: 5
   partridges:
     count: 1
     location: "a pear tree"
   turtle-doves: two

解析器使用mapfile将文件作为数组读入内存,然后循环遍历每个标记并创建环境变量。

梨树、斑鸠和法国母鸡:最终成为简单的环境变量 呼叫鸟:变成一个数组 xmas-fifth-day:结构被表示为一个关联数组,但是如果您没有使用Bash 4.0或更高版本,您可以将这些数组编码为环境变量。 注释和空白将被忽略。

#!/bin/bash
# -------------------------------------------------------------------
# A simple parser for the xmas.yaml file
# -------------------------------------------------------------------
# 
# xmas.yaml tags
#  #                        - Ignored
#                           - Blank lines are ignored
#  ---                      - Initialiser for days-of-xmas 
#   pear-tree: partridge    - a string
#   turtle-doves: 2.718     - a string, no float type in Bash
#   french-hens: 3          - a number
#   calling-birds:          - an array of strings
#     - huey                - calling-birds[0]
#     - dewey
#     - louie
#     - fred
#   xmas-fifth-day:         - an associative array
#     calling-birds: four   - a string
#     french-hens: 3        - a number
#     golden-rings: 5       - a number
#     partridges:           - changes the key to partridges.xxx
#       count: 1            - a number
#       location: "a pear tree" - a string
#     turtle-doves: two     - a string
# 
# This requires the following routines
# ParseXMAS
#   parses #, ---, blank line
#   unexpected tag error
#   calls days-of-xmas
#
# days-of-xmas
#   parses pear-tree, turtle-doves, french-hens
#   calls calling-birds
#   calls xmas-fifth-day
# 
# calling-birds
#   elements of the array
#
# xmas-fifth-day
#   parses calling-birds, french-hens, golden-rings, turtle-doves
#   calls partridges
# 
# partridges
#   parses partridges.count, partridges.location
#

function ParseXMAS()
{

  # days-of-xmas
  #   parses pear-tree, turtle-doves, french-hens
  #   calls calling-birds
  #   calls xmas-fifth-day
  # 
  function days-of-xmas()
  {
    unset PearTree TurtleDoves FrenchHens

    while [ $CURRENT_ROW -lt $ROWS ]
    do
      LINE=( ${CONFIG[${CURRENT_ROW}]} )
      TAG=${LINE[0]}
      unset LINE[0]

      VALUE="${LINE[*]}"

      echo "  days-of-xmas[${CURRENT_ROW}] ${TAG}=${VALUE}"

      if [ "$TAG" = "pear-tree:" ]
      then
        declare -g PearTree=$VALUE
      elif [ "$TAG" = "turtle-doves:" ]
      then
        declare -g TurtleDoves=$VALUE
      elif [ "$TAG" = "french-hens:" ]
      then
        declare -g FrenchHens=$VALUE
      elif [ "$TAG" = "calling-birds:" ]
      then
        let CURRENT_ROW=$(($CURRENT_ROW + 1))
        calling-birds
        continue
      elif [ "$TAG" = "xmas-fifth-day:" ]
      then
        let CURRENT_ROW=$(($CURRENT_ROW + 1))
        xmas-fifth-day
        continue
      elif [ -z "$TAG" ] || [ "$TAG" = "#" ]
      then
        # Ignore comments and blank lines
        true
      else
        # time to bug out
        break
      fi

      let CURRENT_ROW=$(($CURRENT_ROW + 1))
    done
  }

  # calling-birds
  #   elements of the array
  function calling-birds()
  {
    unset CallingBirds

    declare -ag CallingBirds

    while [ $CURRENT_ROW -lt $ROWS ]
    do
      LINE=( ${CONFIG[${CURRENT_ROW}]} )
      TAG=${LINE[0]}
      unset LINE[0]

      VALUE="${LINE[*]}"

      echo "    calling-birds[${CURRENT_ROW}] ${TAG}=${VALUE}"

      if [ "$TAG" = "-" ]
      then
        CallingBirds[${#CallingBirds[*]}]=$VALUE
      elif [ -z "$TAG" ] || [ "$TAG" = "#" ]
      then
        # Ignore comments and blank lines
        true
      else
        # time to bug out
        break
      fi

      let CURRENT_ROW=$(($CURRENT_ROW + 1))
    done
  }

  # xmas-fifth-day
  #   parses calling-birds, french-hens, golden-rings, turtle-doves
  #   calls fifth-day-partridges
  # 
  function xmas-fifth-day()
  {
    unset XmasFifthDay

    declare -Ag XmasFifthDay

    while [ $CURRENT_ROW -lt $ROWS ]
    do
      LINE=( ${CONFIG[${CURRENT_ROW}]} )
      TAG=${LINE[0]}
      unset LINE[0]

      VALUE="${LINE[*]}"

      echo "    xmas-fifth-day[${CURRENT_ROW}] ${TAG}=${VALUE}"

      if [ "$TAG" = "calling-birds:" ]
      then
        XmasFifthDay[CallingBirds]=$VALUE
      elif [ "$TAG" = "french-hens:" ]
      then
        XmasFifthDay[FrenchHens]=$VALUE
      elif [ "$TAG" = "golden-rings:" ]
      then
        XmasFifthDay[GOLDEN-RINGS]=$VALUE
      elif [ "$TAG" = "turtle-doves:" ]
      then
        XmasFifthDay[TurtleDoves]=$VALUE
      elif [ "$TAG" = "partridges:" ]
      then
        let CURRENT_ROW=$(($CURRENT_ROW + 1))
        partridges
        continue
      elif [ -z "$TAG" ] || [ "$TAG" = "#" ]
      then
        # Ignore comments and blank lines
        true
      else
        # time to bug out
        break
      fi
 
      let CURRENT_ROW=$(($CURRENT_ROW + 1))
    done
  }

  function partridges()
  {
    while [ $CURRENT_ROW -lt $ROWS ]
    do
      LINE=( ${CONFIG[${CURRENT_ROW}]} )
      TAG=${LINE[0]}
      unset LINE[0]

      VALUE="${LINE[*]}"

      echo "      partridges[${CURRENT_ROW}] ${TAG}=${VALUE}"

      if [ "$TAG" = "count:" ]
      then
        XmasFifthDay[PARTRIDGES.COUNT]=$VALUE
      elif [ "$TAG" = "location:" ]
      then
        XmasFifthDay[PARTRIDGES.LOCATION]=$VALUE
      elif [ -z "$TAG" ] || [ "$TAG" = "#" ]
      then
        # Ignore comments and blank lines
        true
      else
        # time to bug out
        break
      fi
 
      let CURRENT_ROW=$(($CURRENT_ROW + 1))
    done
  }

  # ===================================================================
  # Load the configuration file

  mapfile CONFIG < xmas.yaml

  let ROWS=${#CONFIG[@]}
  let CURRENT_ROW=0

  # +
  # #
  #
  # ---
  # -
  while [ $CURRENT_ROW -lt $ROWS ]
  do
    LINE=( ${CONFIG[${CURRENT_ROW}]} )
    TAG=${LINE[0]}
    unset LINE[0]

    VALUE="${LINE[*]}"

    echo "[${CURRENT_ROW}] ${TAG}=${VALUE}"

    if [ "$TAG" = "---" ]
    then
        let CURRENT_ROW=$(($CURRENT_ROW + 1))
        days-of-xmas
        continue
    elif [ -z "$TAG" ] || [ "$TAG" = "#" ]
    then
        # Ignore comments and blank lines
        true
    else
        echo "Unexpected tag at line $(($CURRENT_ROW + 1)): <${TAG}>={${VALUE}}"
        break
    fi

    let CURRENT_ROW=$(($CURRENT_ROW + 1))
  done
}

echo =========================================
ParseXMAS

echo =========================================
declare -p PearTree
declare -p TurtleDoves
declare -p FrenchHens
declare -p CallingBirds
declare -p XmasFifthDay

这将产生以下输出

=========================================
[0] #=Xmas YAML example
[1] ---=
  days-of-xmas[2] #=Values
  days-of-xmas[3] pear-tree:=partridge
  days-of-xmas[4] turtle-doves:=2.718
  days-of-xmas[5] french-hens:=3
  days-of-xmas[6] =
  days-of-xmas[7] #=Array
  days-of-xmas[8] calling-birds:=
    calling-birds[9] -=huey
    calling-birds[10] -=dewey
    calling-birds[11] -=louie
    calling-birds[12] -=fred
    calling-birds[13] =
    calling-birds[14] #=Structure
    calling-birds[15] xmas-fifth-day:=
  days-of-xmas[15] xmas-fifth-day:=
    xmas-fifth-day[16] calling-birds:=four
    xmas-fifth-day[17] french-hens:=3
    xmas-fifth-day[18] golden-rings:=5
    xmas-fifth-day[19] partridges:=
      partridges[20] count:=1
      partridges[21] location:="a pear tree"
      partridges[22] turtle-doves:=two
    xmas-fifth-day[22] turtle-doves:=two
=========================================
declare -- PearTree="partridge"
declare -- TurtleDoves="2.718"
declare -- FrenchHens="3"
declare -a CallingBirds=([0]="huey" [1]="dewey" [2]="louie" [3]="fred")
declare -A XmasFifthDay=([CallingBirds]="four" [PARTRIDGES.LOCATION]="\"a pear tree\"" [FrenchHens]="3" [GOLDEN-RINGS]="5" [PARTRIDGES.COUNT]="1" [TurtleDoves]="two" )

我曾经使用python将yaml转换为json,并在jq中进行处理。

python -c "import yaml; import json; from pathlib import Path; print(json.dumps(yaml.safe_load(Path('file.yml').read_text())))" | jq '.'