如何从Linux shell脚本解析YAML文件?

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

410448 次浏览

很难说,因为这取决于您希望解析器从YAML文档中提取什么。对于简单的情况,你可以使用grepcutawk等。对于更复杂的解析,你需要使用一个成熟的解析库,比如Python的PyYAMLYAML: 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

,其中__abc0是包含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'

以下几个要点:

  • 所有的YAML类型和语法都得到了正确的处理,如多行,带引号的字符串,内联序列…
  • \0填充输出可用于固体多行输入操作。
  • 简单的点符号来选择子值(例如:subvalue.maintainer是一个有效的键)。
  • 通过索引的访问提供给序列(即:subvalue.things.-1subvalue.things序列的最后一个元素。)
  • 在bash循环中一次性访问所有sequence/structs元素。
  • 你可以输出YAML文件的整个子部分…YAML,它与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)

我刚刚写了一个解析器,我称之为耶! (Yaml不是Yamlesque!),它解析YAML的一个小子集Yamlesque。因此,如果您正在为Bash寻找一个100%兼容的YAML解析器,那么这不是它。然而,为了引用OP,如果你想要类似yaml的一个结构化的配置文件,尽可能便于非技术用户编辑,这可能会很有趣。

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

除了键/值数组元素,每个数组都有一个包含键名列表的keys数组,一个包含子数组名称的children数组和一个指向父数组的parent键。

是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函数首先定位input文件或退出,退出状态为1。接下来,它确定数据集prefix,可以显式指定或从文件名派生。

它将有效的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。

接下来,它计算出当前项要使用的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的串联。

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

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

keys[indent] = key;

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

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

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

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

集合的名称是当前行的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命令。

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

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

你也可以考虑使用咕哝着说 (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是否有可用任务)。

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


还有一个类似于Grunt的工具,它被称为狼吞虎咽地吃,带有额外的插件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的网站中可用的项目、库和其他资源,这些资源可以帮助你解析该格式。


其他工具:

  • < p > Jshon

    解析、读取和创建JSON

    李< /引用> < / >

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

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


VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")
我知道这是非常具体的,但我认为我的回答可能对某些用户有帮助 如果你的机器上安装了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解析器中使用该字符串。

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

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

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

编辑:我已经为此创建了github库

你可以使用用golang写的yq等效:

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

返回:

62.0.3

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

该项目的目标是yaml文件的金桥或sed。

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

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

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

然后

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

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

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

针对您的具体问题,在使用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

你如何报告只有sku的每一个项目的库存后应用的变化从data2。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文件,也没有临时文件。

我意识到这是“解决同样问题的另一种工具”。但是在阅读了这里的其他答案之后,yamlpath似乎比大多数替代方案更可移植和健壮。它也完全理解YAML/JSON/兼容文件,它需要将YAML转换为JSON来执行所请求的操作。因此,当您需要更改源YAML文件中的数据时,原始YAML文件中的注释将被保留。与一些替代方案一样,yamlpath也是跨操作系统可移植的。更重要的是,yamlpath定义了一种非常强大的查询语言,支持非常专门化/过滤的数据查询。它甚至可以在单个查询中对来自文件不同部分的结果进行操作。

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

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

把我的答案从如何将json响应转换为yaml在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 - 安装说明
  2. mikefarah/yq - 安装说明

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

kislyuk / yq

  1. 由于DSL完全来自jq,对于熟悉后者的用户,解析和操作变得相当简单
  2. 支持模式到保留YAML标签和样式,但在转换过程中丢失注释。由于jq 不保留注释,在往返转换期间,注释将丢失。
  3. 作为包的一部分,XML支持是内置的。一个可执行文件xq,它使用xmltodict将XML转换为JSON,并将其输送到jq,在其上可以应用相同的DSL对对象执行CRUD操作,并将输出返回到XML。
  4. 支持带有-i标志的就地编辑模式(类似于sed -i)

mikefarah / yq

  1. 在DSL中容易频繁更改,2.迁移。X - 3.x
  2. 丰富的支持锚,样式和标签。但偶尔也要注意一下虫子
  3. 一个相对简单的路径表达式语法来导航和匹配yaml节点
  4. 支持YAML->JSON, JSON->YAML格式和漂亮的YAML打印(带有注释)
  5. 支持带有-i标志的就地编辑模式(类似于sed -i)
  6. 支持用-C标记为输出YAML着色(不适用于JSON输出)和子元素的缩进(默认为2个空格)
  7. 支持大多数Shell的Shell补全- Bash, zsh(因为spf13 /眼镜蛇用于生成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
    

这很简单。你所需要做的就是用-y标志将jq JSON输出转码回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是beta版,支持许多强大的路径表达式,并支持类似于使用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的PyYAMLYAML: 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将文件作为数组读入内存,然后循环遍历每个标记并创建环境变量。

  • pear-tree:turtle-doves:french-hens:是简单的环境变量
  • calling-birds:变成一个数组
  • 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 '.'