Я только что написал парсер, который назвал Yay! ( Yaml - не Yamlesque! ), который анализирует Yamlesque , небольшое подмножество YAML. Так что, если вы ищете 100% совместимый YAML-парсер для Bash, то это не так. Однако, если процитировать 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, перед тем, как разграничить допустимые поля Yamlesque символом ASCII File Separator и удалить все двойные кавычки, окружающие поле значения.
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 / шестнадцатеричный 12 / восьмеричный 034) используется потому, что, будучи непечатным символом, он вряд ли будет во входных данных.
Результат передается в awk
, который обрабатывает входные данные по одной строке за раз. Он использует символ FS для назначения каждого поля переменной:
indent = length($1)/2;
key = $2;
value = $3;
У всех строк есть отступ (возможно, ноль) и ключ, но не у всех есть значение. Он вычисляет уровень отступа для строки, разделяющей длину первого поля, которое содержит начальный пробел, на два. Элементы верхнего уровня без отступа находятся на нулевом уровне отступа.
Далее выясняется, что 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
.
Для верхнего уровня (нулевого уровня отступа) префикс набора данных используется в качестве родительского ключа, поэтому у него нет префикса (он установлен на ""
). Все остальные массивы имеют префикс root.
Затем текущий ключ вставляется в (awk-internal) массив, содержащий ключи.Этот массив сохраняется в течение всего сеанса awk и поэтому содержит ключи, вставленные предыдущими строками.Ключ вставляется в массив, используя его отступ в качестве индекса массива.
keys[indent] = key;
Поскольку этот массив содержит ключи из предыдущих строк, удаляются все ключи с уровнем отступа, меньшим, чем уровень отступа текущей строки:
for (i in keys) {if (i > indent) {delete keys[i]}}
В результате получается массив ключей, содержащийцепочка ключей от корня на уровне отступа 0 до текущей строки.Он удаляет устаревшие ключи, которые остаются, когда предыдущая строка была смещена глубже текущей строки.
В последнем разделе выводятся команды bash
: строка ввода без значения начинает новый уровень отступа ( collection на языке 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встроенными командами bash eval
или source
.