Yayというパーサーを書いたところです。(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
ヘルパーのようなグローバルスコープで使用できます。
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" |
2つの式は似ています。2つ目は引用符で囲まれていない値を選択するのに対し、最初のものは引用符で囲まれた値を選択するためです。
ファイル区切り、非印刷可能文字として、入力されたデータであることにくい、ので(28 /ヘキサン12 /進034)が使用されます。
結果はパイプawk
処理され、入力が一度に1行ずつ処理されます。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
現在の行のインデントレベル以上のインデントレベルでの鍵であり、現在の行がその一部となっているコレクションを表します。コレクションのキーと値のペアは、prefix
との連結として定義された名前で配列に格納されparent_key
ます。
トップレベル(インデントレベル0)の場合、データセットプレフィックスは親キーとして使用されるため、プレフィックスはありません(次のように設定されています) ""
)。他のすべての配列には、ルートプレフィックスがプレフィックスされます。
次に、現在のキーが、キーを含む(awk-internal)配列に挿入されます。この配列は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);
最初のステートメントは、キーにちなんで名付けられた連想配列要素に値を割り当てるコマンドを出力し、2番目のステートメントは、コレクションのスペース区切り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
リストに新しいコレクションを追加するコマンドを出力し、2番目のステートメントは、新しいコレクションの新しい連想配列を宣言するコマンドを出力します。
<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;
からのすべての出力は、yay_parse
bashコマンドeval
またはsource
組み込みコマンドによってbashコマンドとして解析できます。