我希望提供一个结构化的配置文件,对于非技术用户而言,该文件应尽可能地容易编辑(不幸的是,该文件必须是文件),因此我想使用YAML。但是,我找不到从Unix Shell脚本解析此内容的任何方法。
yq
在外壳中读取/写入yaml文件。项目页面在这里:mikefarah.github.io/yq您可以使用来安装该工具brew
,apt
或者下载二进制文件。读取值就像yq r some.yaml key.value
我希望提供一个结构化的配置文件,对于非技术用户而言,该文件应尽可能地容易编辑(不幸的是,该文件必须是文件),因此我想使用YAML。但是,我找不到从Unix Shell脚本解析此内容的任何方法。
yq
在外壳中读取/写入yaml文件。项目页面在这里:mikefarah.github.io/yq您可以使用来安装该工具brew
,apt
或者下载二进制文件。读取值就像yq r some.yaml key.value
Answers:
我的用例可能与原始帖子要求的完全不同,但肯定是相似的。
我需要引入一些YAML作为bash变量。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
查找:
并替换为="
,而忽略://
(对于URL)s/$/"/g
追加"
到每一行的末尾s/ *=/=/g
删除所有空格 =
{KEY: 'value', ...}
; 可能还有其他。最重要的是,如果您打算将结果评估为shell代码,那将是非常不安全的。
这是仅使用bash的解析器,它利用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)
-
表示法转换为本地bash数组,那将是惊人的!
global__debug
代替global_debug
。
我已经用shyaml
Python 编写了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'
一些要点:
\0
填充输出可用于可靠的多行输入操作。 subvalue.maintainer
是有效键)。subvalue.things.-1
是序列的最后一个元素subvalue.things
。)shyaml github页面或shyaml PyPI页面上提供了更多示例和文档。
cat docker-compose.yml | shyaml get-value api.environment | grep -v null | awk -F': ' '{print $2 > ("envdir/" $1)}'
shyaml
速度太慢了
yq是轻巧且可移植的命令行YAML处理器
该项目的目的是将yaml文件作为jq或sed。
(https://github.com/mikefarah/yq#readme)
作为一个示例(直接从文档中被盗),给出了一个sample.yaml文件,其内容为:
---
bob:
item1:
cats: bananas
item2:
cats: apples
然后
yq r sample.yaml bob.*.cats
将输出
- bananas
- apples
可以将一个小的脚本传递给某些解释器,例如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值的哈希(或数组)。
作为奖励,它可以很好地解析Jekyll的前端问题。
ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)['tags']" example.md
RUBY_SCRIPT
变量是一个ruby脚本,可以将其写入文件(使用运行ruby -ryaml <rubyscript_filename>
)。它包含将输入文本转换为某些输出文本并将内部内容存储到data
变量中的逻辑。回声会输出yaml文本,但您可以使用cat <yaml_filename>
它代替管道内容。
stdout
其输入变量中,则不必依赖临时文件!请使用x=$(...)
甚至read a b c < <(...)
)。因此,当您确切地知道要在YAML文件中获取的内容并且知道如何编写红宝石行以访问此数据时,这是一个有效的解决方案。即使很粗糙,这也是恕我直言的完整概念证明。尽管如此,它确实不能为您提供完整的bash抽象。
鉴于Python3和PyYAML是当今很容易满足的依赖关系,以下内容可能会有所帮助:
yaml() {
python3 -c "import yaml;print(yaml.safe_load(open('$1'))$2)"
}
VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")
yaml.safe_load
因为它更安全。pyyaml.org/wiki/PyYAMLDocumentation
这是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-keys)。
关于代码的几句话:第一个sed
命令将字典的缩写形式扩展{ key: value, ...}
为正则,并将其转换为更简单的yaml样式。第二个sed
调用对列表的缩写表示法相同,并转换[ entry, ... ]
为带有该-
表示法的逐项列表。第三个sed
调用是处理普通字典的原始调用,现在添加了带有-
和缩进的句柄列表。该awk
部分为每个缩进级别引入一个索引,并在变量名称为空时(即在处理列表时)增加索引。使用计数器的当前值代替空的vname。当上升一级时,计数器清零。
编辑:我为此创建了一个github存储库。
很难说,因为这取决于您希望解析器从YAML文档中提取的内容。对于简单的情况下,可能能够使用grep
,cut
,awk
等对于更复杂的分析,你需要使用一个全面的解析库,如Python的PyYAML或YAML :: Perl的。
我刚刚写了一个解析器,叫做Yay!(Yaml不是Yamlesque!),它解析Yamlesque(YAML的一小部分)。因此,如果您正在寻找针对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
辅助函数:
yay() { eval $(yay_parse "$@"); }
输入数据最初使用进行处理sed
。在使用ASCII 文件分隔符分隔有效Yamlesque字段并删除value字段周围的所有双引号之前,它将删除与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 / hex 12 / octal 034),是因为它不可能作为不可打印的字符出现在输入数据中。
结果通过管道传输到awk
其中,一次处理一行输入。它使用FS字符将每个字段分配给一个变量:
indent = length($1)/2;
key = $2;
value = $3;
所有行都有一个缩进(可能为零)和一个键,但是它们都不都是值。它计算行的缩进级别,该行将包含前导空格的第一个字段的长度除以2。没有任何缩进的顶级项目的缩进级别为零。
接下来,计算出prefix
用于当前项目的内容。这就是添加到键名中以形成数组名的内容。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
。
对于最高级别(缩进级别为零),数据集前缀用作父键,因此没有前缀(将其设置为""
)。所有其他阵列均以根前缀为前缀。
接下来,将当前密钥插入包含密钥的(内部awk)数组中。该数组在整个awk会话中一直存在,因此包含先前行插入的键。使用键的缩进作为数组索引将键插入到数组中。
keys[indent] = key;
由于此数组包含前几行的键,因此删除缩进级别比当前行的缩进级别高的所有键:
for (i in keys) {if (i > indent) {delete keys[i]}}
这样就留下了包含从缩进级别0的根到当前行的钥匙串的钥匙数组。它将删除当前一行比当前行缩进更深时保留的陈旧键。
最后一部分输出bash
命令:不带值的输入行开始新的缩进级别(以YAML的说法是集合),而带值的输入行将键添加到当前集合。
集合的名称是当前行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
内置命令可以将所有的输出解析为bash 命令。
perl -ne 'chomp; printf qq/%s="%s"\n/, split(/\s*:\s*/,$_,2)' file.yml > file.sh
另一种选择是将YAML转换为JSON,然后使用jq与JSON表示进行交互以从中提取信息或对其进行编辑。
我编写了一个包含此胶水的简单bash脚本-请参阅GitHub上的Y2J项目
我知道这是非常具体的,但是我认为我的回答可能对某些用户有帮助。
如果已安装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
您也可以考虑使用Grunt(JavaScript任务运行器)。可以轻松地与外壳集成。它支持读取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网站上查看可用的项目,库和其他资源,以帮助您解析该格式。
其他工具:
解析,读取和创建JSON