我如何从Linux shell脚本parsingYAML文件?

我希望为非技术用户提供一个结构化的configuration文件,尽可能简单(不幸的是它必须是一个文件),所以我想使用YAML。 但是我找不到任何从Unix shell脚本parsing的方法。

我的用例可能与原来的post不一样,也可能不一样,但是绝对是相似的。

我需要把一些YAML作为bashvariables。 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发现:并用="replace它,忽略:// (对于URL)
  • s/$/"/g追加"到每一行的末尾
  • s/ *=/=/g删除=之前的所有空格

这里是一个bash-onlyparsing器,利用sed和awk来parsing简单的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" 

其中,当使用下面的parsing:

 parse_yaml sample.yml 

会输出:

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

它也理解yaml文件,由ruby生成,其中可能包括ruby符号,如:

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

并将输出与前面的示例相同。

脚本中的典型用法是:

 eval $(parse_yaml sample.yml) 

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

 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 

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

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

我已经写了shyaml在Python命令行的YAML查询需求。

概述:

 $ pip install shyaml ## installation 

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

 $ 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' 

几个关键点:

  • 所有的YAMLtypes和语法古怪都被正确处理,如多行,带引号的string,内联序列…
  • \0填充输出可用于实体多行input操作。
  • 简单的虚线符号来select子值(即: subvalue.maintainer是一个有效的关键)。
  • 访问索引提供给序列(即: subvalue.things.-1subvalue.things序列的最后一个元素。)
  • 一次性访问所有序列/结构体元素以用于bash循环。
  • 你可以输出一个YAML文件的整个子部分作为… YAML,这与shyaml的进一步操作很好地融合。

shyaml github页面或shyaml PyPI页面上提供了更多示例和文档。

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

作为奖励,它会parsingJekyll的前端问题 。

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

很难说,因为它取决于你想要parsing器从你的YAML文档中提取什么。 对于简单的情况,您可以使用grepcutawk等。对于更复杂的parsing,您需要使用完整的parsing库,如Python的PyYAML或YAML :: Perl 。

我刚写了一个parsing器,我叫Yay!Yaml不是Yamlesque! ),它分析了YAML的一小部分Yamlesque 。 所以,如果你正在为Bash寻找100%兼容的YAMLparsing器,那么这不是它。 但是,引用OP时,如果您希望非技术用户编辑的结构化configuration文件尽可能容易编辑为YAML,可能会引起您的兴趣。

这是由较早的答案启发,但写联合数组( 是的,它需要Bash 4.x ),而不是基本的variables。 这样做的方式可以让数据在事先不知道密钥的情况下进行parsing,从而可以编写数据驱动的代码。

除了键/值数组元素外,每个数组都有一个包含键名称列表的键数组,一个包含子数组名称的子数组和一个引用parent键的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 } } 

这里是parsing器:

 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命令写入其标准输出,如果执行,则定义表示input数据文件内容的数组。 其中的第一个定义了顶层数组:

 echo "declare -g -A $prefix;" 

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

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

input数据最初是用sed处理的。 在使用ASCII 文件分隔符字符分隔有效的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" | 

这两个expression式是相似的; 他们之间的区别仅在于第一个挑选出引用的值,第二个挑选出没有引用的值。

使用文件分隔符 (28 /hex12 /八进制034),因为作为一个不可打印的字符,它不太可能在input数据。

结果是通过pipe道input一次处理其input的awk 。 它使用FS字符将每个字段分配给一个variables:

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

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

接下来,找出当前项目使用的prefix 。 这是被添加到一个密钥名称来创build一个数组名称。 顶层数组有一个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的连接。

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

接下来,当前键被插入一个包含键的(awk-internal)数组中。 这个数组在整个awk会话中保持不变,因此包含由前面的行插入的键。 该键使用其缩进作为数组索引插入到数组中。

 keys[indent] = key; 

因为此数组包含以前行中的键,所以将删除具有比当前行的缩进级别更大的缩进级别的任何键:

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

这会将包含key-chain的键数组从根目录的缩进级别0保留到当前行。 它将删除当前行比当前行更深时保留的旧键。

最后一节输出bash命令:一个没有值的input行开始一个新的缩进级别(YAML说法中的一个集合 ),一个带有值的input行将一个键添加到当前集合中。

集合的名字是当前行的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内置命令parsing为bash命令。

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

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

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

我发现这个Jshon工具是最好的,但是在JSON世界。

但是在YAML这个工具的互联网上我找不到任何痕迹。 您(至less现在)必须使用Perl / Python / Ruby脚本来做到这一点(如以前的答案)。

你也可以考虑使用Grunt (The JavaScript Task Runner)。 可以很容易地与壳集成。 它支持读取YAML( grunt.file.readYAML )和JSON( grunt.file.readJSON )文件。

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

 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获取可用的任务)。

更进一步,你可以通过任务传递的inputvariables( foo: { cmd: 'echo bar <%= foo %>' } )实现exec:foo任务( grunt-exec ),以便以任何你想要的格式打印输出,然后将其pipe入另一个命令。


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/')) 

为了更多的select来处理YAML格式 ,请检查YAML网站上的可用项目,库和其他资源,这可以帮助您parsing这种格式。


其他工具:

  • Jshon

    parsing,读取和创buildJSON