我如何从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