Właśnie napisałem parser, który nazwałem Yay! ( Yaml to nie Jamlesque! ), Który analizuje Yamlesque , niewielki podzbiór YAML. Jeśli więc szukasz parsera YAML w 100% zgodnego z Bash, to nie o to chodzi. Jednak, aby zacytować OP, jeśli chcesz ustrukturyzowanego pliku konfiguracyjnego, który jest jak najłatwiejszy dla użytkownika nietechnicznego, który jest podobny do YAML, może to być interesujące.
Jest inspirowany wcześniejszą odpowiedzią, ale zapisuje tablice asocjacyjne ( tak, wymaga Bash 4.x ) zamiast podstawowych zmiennych. Robi to w sposób, który pozwala na parsowanie danych bez uprzedniej wiedzy o kluczach, aby można było napisać kod sterowany danymi.
Oprócz elementów tablicy klucz / wartość każda tablica ma keys
tablicę zawierającą listę nazw kluczy, children
tablicę zawierającą nazwy tablic podrzędnych i parent
klucz, który odnosi się do jego elementu nadrzędnego.
To jest przykład 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
Oto przykład pokazujący, jak z niego korzystać:
#!/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
które wyjścia:
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
}
}
A oto parser:
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 "$@"); }
W połączonym pliku źródłowym znajduje się dokumentacja, a poniżej znajduje się krótkie objaśnienie działania kodu.
yay_parse
Funkcja najpierw lokalizuje input
plik lub wyjścia ze stanem wyjściowym 1. Następnie, określa zbiór danych prefix
, albo wyraźnie określony lub pochodzącą od nazwy pliku.
Zapisuje prawidłowe bash
polecenia na swoim standardowym wyjściu, które, jeśli zostaną wykonane, definiują tablice reprezentujące zawartość pliku danych wejściowych. Pierwszy z nich określa tablicę najwyższego poziomu:
echo "declare -g -A $prefix;"
Zauważ, że deklaracje tablicowe są stowarzyszone ( -A
), co jest cechą wersji Bash 4. Deklaracje są również globalne ( -g
), więc można je wykonywać w funkcji, ale być dostępne dla zakresu globalnego, takiego jak yay
pomocnik:
yay() { eval $(yay_parse "$@"); }
Dane wejściowe są wstępnie przetwarzane za pomocą sed
. Porzuca linie, które nie pasują do specyfikacji formatu Yamlesque, przed rozdzieleniem prawidłowych pól Yamlesque znakiem separatora plików ASCII i usunięciem podwójnych cudzysłowów otaczających pole wartości.
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" |
Te dwa wyrażenia są podobne; różnią się tylko tym, że pierwszy wybiera wartości cytowane, a drugi wybiera niecytowane.
File Separator (28 / hex 12 / ósemkowy 034) służy, bo jak niedrukowalny charakterem, jest mało prawdopodobne, aby być w danych wejściowych.
Wynik jest przesyłany do procesora, awk
który przetwarza dane wejściowe pojedynczo. Używa znaku FS, aby przypisać każde pole do zmiennej:
indent = length($1)/2;
key = $2;
value = $3;
Wszystkie linie mają wcięcie (prawdopodobnie zero) i klucz, ale nie wszystkie mają wartość. Oblicza poziom wcięcia dla linii dzielącej długość pierwszego pola zawierającego wiodące białe znaki przez dwa. Elementy najwyższego poziomu bez wcięcia znajdują się na poziomie wcięcia zero.
Następnie zastanawia się, czego prefix
użyć dla bieżącego elementu. To jest dodawane do nazwy klucza, aby utworzyć nazwę tablicy. Istnieje root_prefix
tablica najwyższego poziomu, która jest zdefiniowana jako nazwa zestawu danych i znak podkreślenia:
root_prefix = "'$prefix'_";
if (indent ==0 ) {
prefix = ""; parent_key = "'$prefix'";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
Jest parent_key
to klucz na poziomie wcięcia powyżej poziomu wcięcia bieżącej linii i reprezentuje kolekcję, której częścią jest bieżąca linia. Pary klucz / wartość kolekcji zostaną zapisane w tablicy o nazwie zdefiniowanej jako konkatenacja prefix
i parent_key
.
W przypadku najwyższego poziomu (poziom wcięcia zero) prefiks zestawu danych jest używany jako klucz nadrzędny, więc nie ma on prefiksu (jest ustawiony na ""
). Wszystkie pozostałe tablice są poprzedzone prefiksem głównym.
Następnie bieżący klucz jest wstawiany do (awk-wewnętrznej) tablicy zawierającej klucze. Tablica ta utrzymuje się przez całą sesję awk i dlatego zawiera klucze wstawiane przez wcześniejsze linie. Klucz jest wstawiany do tablicy za pomocą jego wcięcia jako indeksu tablicy.
keys[indent] = key;
Ponieważ ta tablica zawiera klucze z poprzednich linii, wszelkie klucze z wyższym poziomem wcięcia niż poziom wcięcia bieżącej linii są usuwane:
for (i in keys) {if (i > indent) {delete keys[i]}}
Pozostawia to tablicę kluczy zawierającą łańcuch kluczy od korzenia na poziomie wcięcia 0 do bieżącej linii. Usuwa przestarzałe klucze, które pozostają, gdy poprzednia linia została wcięta głębiej niż bieżąca linia.
Ostatnia sekcja zawiera bash
polecenia: linia wejściowa bez wartości rozpoczyna nowy poziom wcięcia ( zbiór w języku YAML), a linia wejściowa z wartością dodaje klucz do bieżącej kolekcji.
Nazwa kolekcji to konkatenacja bieżącej linii prefix
i parent_key
.
Gdy klucz ma wartość, klucz o tej wartości jest przypisywany do bieżącej kolekcji w następujący sposób:
printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
Pierwsza instrukcja wypisuje polecenie przypisania wartości do elementu tablicy asocjacyjnej nazwanej po kluczu, a druga wypisuje polecenie dodania klucza do keys
listy rozdzielanej spacjami kolekcji :
<current_collection>[<key>]="<value>";
<current_collection>[keys]+=" <key>";
Gdy klucz nie ma wartości, rozpoczyna się nowa kolekcja w następujący sposób:
printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);
Pierwsza instrukcja wypisuje polecenie dodania nowej kolekcji do children
listy rozdzielanej spacjami kolekcji bieżącej, a druga wypisuje polecenie zadeklarowania nowej tablicy asocjacyjnej dla nowej kolekcji:
<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;
Wszystkie dane wyjściowe z yay_parse
mogą być analizowane jako polecenia bash przez bash eval
lub source
wbudowane polecenia.