我如何从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.-1是subvalue.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文档中提取什么。 对于简单的情况,您可以使用grep , cut , awk等。对于更复杂的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是当前行缩进级别上方缩进级别的键,表示当前行所属的集合。 集合的键/值对将被存储在数组中,其名称被定义为prefix和parent_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行将一个键添加到当前集合中。 
 集合的名字是当前行的prefix和parent_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 eval或source内置命令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