Jak mogę przeanalizować plik YAML ze skryptu powłoki Linux?


193

Chciałbym dostarczyć ustrukturyzowany plik konfiguracyjny, który jest jak najłatwiejszy do edycji dla nietechnicznego użytkownika (niestety musi to być plik), więc chciałem użyć YAML. Nie mogę jednak znaleźć sposobu na parsowanie tego ze skryptu powłoki Uniksa.


nie bezpośrednio twoje pytanie, ale możesz chcieć spojrzeć na odpowiedź, jeśli skrobanie powłoki dotyczy przede wszystkim zdalnego zarządzania różnymi węzłami (i ekwipunkiem yaml)
eckes

9
Spróbuj użyć yqdo odczytu / zapisu plików yaml w powłoce. Na stronie projektu znajduje się tutaj: mikefarah.github.io/yq można zainstalować narzędzie z brew, aptlub pobrać plik binarny. Odczyt wartości jest tak prosty, jakyq r some.yaml key.value
vdimitrov

@kenorb JSON! = yml / YAML
swe

Znalazłem ściśle powiązane funkcje githuba pkuczyńskiego, z których najlepszy (dla mnie) był ten z jaspisów, utrzymany we własnym
githubie

Odpowiedzi:


56

Mój przypadek użycia może, ale nie musi być taki sam, jak ten, o który pytał ten oryginalny post, ale jest zdecydowanie podobny.

Muszę pobrać trochę YAML jako zmienne bash. YAML nigdy nie będzie głębszy niż jeden poziom.

YAML wygląda tak:

KEY:                value
ANOTHER_KEY:        another_value
OH_MY_SO_MANY_KEYS: yet_another_value
LAST_KEY:           last_value

Wyjście jak dis:

KEY="value"
ANOTHER_KEY="another_value"
OH_MY_SO_MANY_KEYS="yet_another_value"
LAST_KEY="last_value"

Osiągnąłem wynik dzięki tej linii:

sed -e 's/:[^:\/\/]/="/g;s/$/"/g;s/ *=/=/g' file.yaml > file.sh
  • s/:[^:\/\/]/="/gznajduje :i zamienia go =", ignorując ://(w przypadku adresów URL)
  • s/$/"/gdołącza "na końcu każdej linii
  • s/ *=/=/g usuwa wszystkie spacje wcześniej =

13
Nie jestem pewien, do czego zmierzasz, ale jeśli masz na myśli, że to nie działa dla wszystkich YAML, masz rację. Dlatego otworzyłem z kilkoma kwalifikacjami. Właśnie podzieliłem się tym, co zadziałało w moim przypadku użycia, ponieważ odpowiedział na pytanie lepiej niż jakikolwiek inny w tym czasie. Można to zdecydowanie rozszerzyć.
Curtis Blackwell

3
nieco otwarty na wstrzyknięcie kodu, ale, jak powiedziałeś, jest krok do przodu
Oriettaxx,

1
Pisałem tylko skrypty powłoki do użytku lokalnego, więc nie było to dla mnie problemem. Jeśli jednak wiesz, jak go zabezpieczyć i / lub chciałbyś go rozwinąć, zdecydowanie byłbym wdzięczny.
Curtis Blackwell

2
Yaml o głębokości jednego poziomu ma wiele postaci - wartości można podzielić na następującą wciętą linię; wartości mogą być cytowane na wiele sposobów, których powłoka nie będzie analizować; wszystko może być napisane w jednej linii z szelkami: {KEY: 'value', ...}; i ewentualnie inni. Co najważniejsze, jeśli zamierzasz ocenić wynik jako kod powłoki, byłoby to bardzo niepewne.
Beni Cherniavsky-Paskin

281

Oto analizator składający się tylko z basha, który wykorzystuje sed i awk do analizowania prostych plików 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);
      }
   }'
}

Rozumie pliki takie jak:

## global definitions
global:
  debug: yes
  verbose: no
  debugging:
    detailed: no
    header: "debugging started"

## output
output:
   file: "yes"

Które po przeanalizowaniu za pomocą:

parse_yaml sample.yml

wyświetli:

global_debug="yes"
global_verbose="no"
global_debugging_detailed="no"
global_debugging_header="debugging started"
output_file="yes"

rozumie również pliki yaml, generowane przez ruby, które mogą zawierać symbole ruby, takie jak:

---
:global:
  :debug: 'yes'
  :verbose: 'no'
  :debugging:
    :detailed: 'no'
    :header: debugging started
  :output: 'yes'

i wyświetli to samo, co w poprzednim przykładzie.

typowe zastosowanie w skrypcie to:

eval $(parse_yaml sample.yml)

parse_yaml przyjmuje argument przedrostka, dzięki czemu wszystkie importowane ustawienia mają wspólny przedrostek (co zmniejsza ryzyko kolizji w przestrzeni nazw).

parse_yaml sample.yml "CONF_"

daje:

CONF_global_debug="yes"
CONF_global_verbose="no"
CONF_global_debugging_detailed="no"
CONF_global_debugging_header="debugging started"
CONF_output_file="yes"

Pamiętaj, że do poprzednich ustawień w pliku można odwoływać się w późniejszych ustawieniach:

## global definitions
global:
  debug: yes
  verbose: no
  debugging:
    detailed: no
    header: "debugging started"

## output
output:
   debug: $global_debug

Innym miłym zastosowaniem jest najpierw parsowanie pliku domyślnego, a następnie ustawień użytkownika, co działa, ponieważ te ostatnie zastępują pierwsze:

eval $(parse_yaml defaults.yml)
eval $(parse_yaml project.yml)

3
Fajny Stefan! Byłoby niesamowicie, gdyby mógł zmienić -notację yaml w rodzime tablice bash!
szybka zmiana

3
Powinno to być dość łatwe, jeśli zmienisz wiersz printf w skrypcie awk. Zauważ jednak, że bash nie obsługuje wielowymiarowych tablic asocjacyjnych, więc otrzymujesz tablicę + pojedynczy klucz na wartość. Hmm, prawdopodobnie powinien przenieść to do github ...
Stefan Farestam

5
Oczekuje to standardowego wcięcia yml 2 spacji. Jeśli używasz 4 spacji, zmienne otrzymają dwa znaki podkreślenia jako separator, np. global__debugZamiast global_debug.
k0pernikus

3
Cześć vaab - Choć jestem pewien, że masz rację, że wielu czytelników chciałoby parsować prawdziwe pliki YAML ze powłoki, nie jest całkiem jasne (przynajmniej dla mnie), jaki byłby wynik. Za pomocą tego skryptu zająłem się problemem i zdefiniowałem podzbiór, który ma rozsądne odwzorowanie na standardowe zmienne. Z pewnością nie ma pretensji, że rozwiązano większy problem analizowania prawdziwych plików YAML.
Stefan Farestam

3
Drukuje tylko dane wyjściowe na ekranie. Jak uzyskasz dostęp do wartości później?
sattu

96

Napisałem shyamlw pythonie dla potrzeb zapytania YAML z wiersza poleceń powłoki.

Przegląd:

$ pip install shyaml      ## installation

Przykładowy plik YAML (ze złożonymi funkcjami):

$ 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

Podstawowe zapytanie:

$ cat test.yaml | shyaml get-value subvalue.maintainer
Valentin Lab

Bardziej złożone zapytanie o zapętlenie dotyczące złożonych wartości:

$ 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'

Kilka kluczowych punktów:

  • wszystkie typy YAML i osobliwości składniowe są poprawnie obsługiwane, jako ciągi wielowierszowe, cytowane ciągi, sekwencje wbudowane ...
  • \0 wyściełane wyjście jest dostępne do solidnego manipulowania wejściem wielowierszowym.
  • prosta kropkowana notacja w celu wybrania podwartości (tj.: subvalue.maintainerjest prawidłowym kluczem).
  • dostęp do indeksu jest zapewniony dla sekwencji (tzn. subvalue.things.-1jest ostatnim elementem subvalue.thingssekwencji).
  • dostęp do wszystkich elementów sekwencji / struktur za jednym razem do użycia w pętlach bash.
  • możesz wypisać całą podsekcję pliku YAML jako ... YAML, która dobrze komponuje się do dalszych manipulacji z shyaml.

Więcej próbek i dokumentacji dostępnych jest na stronie nieśmiałyaml github lub stronie nieśmiałyam PyPI .


1
To jest niesamowite! Byłoby wspaniale, gdyby istniała flaga ignorująca wartości yaml, które są puste w danych wyjściowych. W tej chwili wyświetla „null”. Korzystam z niego wraz z envdir, aby wyprowadzić plik komponujący okno dokowane do envdircat docker-compose.yml | shyaml get-value api.environment | grep -v null | awk -F': ' '{print $2 > ("envdir/" $1)}'
JiminyCricket

@JiminyCricket Skorzystaj ze strony wydania github! Z przyjemnością przynajmniej bym to śledził. ;)
Vaab

1
Niestety shyamljest absurdalnie wolny
teraz

42

yq to lekki i przenośny procesor wiersza poleceń YAML

Celem tego projektu jest jq lub sed plików yaml.

( https://github.com/mikefarah/yq#readme )

Jako przykład (skradziony bezpośrednio z dokumentacji ) podano przykładowy plik:

---
bob:
  item1:
    cats: bananas
  item2:
    cats: apples

następnie

yq r sample.yaml bob.*.cats

wyjdzie

- bananas
- apples

po prostu brakuje możliwości filtrowania
Antonin,

formulae.brew.sh/formula/yq ma 26 679 instalacji w ciągu ostatniego roku.
dustinevan

1
@Antonin Nie jestem pewien, czy to masz na myśli, ale wygląda na to, że ma teraz pewne możliwości filtrowania: mikefarah.gitbook.io/yq/usage/path-expressions
bmaupin

32

Możliwe jest przekazanie małego skryptu niektórym tłumaczom, takim jak Python. Prostym sposobem na to przy użyciu Ruby i jego biblioteki YAML jest:

$ 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

, gdzie datajest skrót (lub tablica) z wartościami yaml.

Jako bonus dokładnie przeanalizuje przód Jekylla .

ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)['tags']" example.md

1
czy to jest użyteczne? umieściłeś yaml za pomocą echa w tłumaczu ruby. ale jak używać tej zmiennej w pozostałej części skryptu bash?
Znik

Tak, jest użyteczny. RUBY_SCRIPTZmienna jest skrypt Ruby, które mogą być zapisane do pliku zamiast biegu (z ruby -ryaml <rubyscript_filename>). Zawiera logikę przekształcania tekstu wejściowego na tekst wyjściowy, wewnętrznie przechowując zawartość w datazmiennej. Echo generuje tekst yaml, ale cat <yaml_filename>zamiast tego możesz użyć do potokowania zawartości pliku.
Rafael

Przepraszam, ale nie widzę tego w powyższym przykładzie. Przy pierwszej zmiennej RUBY_SCRIPT przechowuje kod dla interpretera ruby. Następnie echo -e symuluje dowolne dane yaml, to jest przez przekierowanie stosu do interpretera ruby. To wywołuje kod ruby ​​jako skrypt wbudowany i na końcu drukuje, aby wyprowadzić przykłady zmiennych „a” i „b”. Więc gdzie zmienna ładuje się do bash dla swojego kodu wykonywalnego? Widzę tylko jedno obejście. umieszczenie ruby ​​outout w pliku tymczasowym, które powinno zawierać linie: zmienna = „wartość”, a następnie załadować go do bash przez „. plik tymczasowy'. ale jest to obejście, a nie rozdzielczość.
Znik

1
@Znik, gdy masz już coś na stdout, wyprodukowane przez coś karmionego stdin, reszta opiera się na rękach programisty bash (a dla przypomnienia, jeśli potrzebujesz stdoutzasilać zmienną, nie musisz polegać na pliki tymczasowe! użyj x=$(...)lub nawet read a b c < <(...)). Jest to więc prawidłowe rozwiązanie, gdy wiesz dokładnie, co chcesz pobrać z pliku YAML i wiesz, jak napisać ruby, aby uzyskać dostęp do tych danych. Nawet jeśli jest szorstki, jest to pełny dowód koncepcji IMHO. Prawdą jest jednak, że nie zapewnia pełnej abstrakcji.
vaab

Tak to jest. Jesteś sztywna. Dzięki za tę sztuczkę. Korzystanie z jednej zmiennej jest proste. ale wiele gwarancji nie jest. sztuczka z listą zmiennych do odczytu <<(wykonanie do standardowego wyjścia) jest bardzo przydatna :)
Znik

23

Biorąc pod uwagę, że Python3 i PyYAML są dość łatwymi zależnościami do spełnienia w dzisiejszych czasach, mogą pomóc:

yaml() {
    python3 -c "import yaml;print(yaml.safe_load(open('$1'))$2)"
}

VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")

Uwielbiam shyaml, ale na odłączonych systemach jest to ratowanie życia. Powinien działać również ze zdecydowaną większością python2, np. RHEL.
rsaw 30.01.2019

2
Może używać, yaml.safe_loadponieważ jest bezpieczniejszy. pyyaml.org/wiki/PyYAMLDokumentacja
Jordan Stewart

14

tutaj rozszerzona wersja odpowiedzi Stefana Farestama:

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);
      }
   }'
}

Ta wersja obsługuje -notację i krótką notację dla słowników i list. Następujące dane wejściowe:

global:
  input:
    - "main.c"
    - "main.h"
  flags: [ "-O3", "-fpic" ]
  sample_input:
    -  { property1: value, property2: "value2" }
    -  { property1: "value3", property2: 'value 4' }

tworzy ten wynik:

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"

jak widać, -pozycje są automatycznie numerowane w celu uzyskania różnych nazw zmiennych dla każdego elementu. W bashtym przypadku nie ma tablic wielowymiarowych, więc jest to jeden ze sposobów obejścia problemu. Obsługiwanych jest wiele poziomów. Aby obejść problem z końcowymi spacjami wymienionymi przez @briceburg, należy zawrzeć wartości w pojedynczych lub podwójnych cudzysłowach. Nadal istnieją jednak pewne ograniczenia: rozszerzenie słowników i list może dawać nieprawidłowe wyniki, gdy wartości zawierają przecinki. Ponadto bardziej złożone struktury, takie jak wartości obejmujące wiele linii (jak klucze ssh), nie są (jeszcze) obsługiwane.

Kilka słów o kodzie: Pierwsze sedpolecenie rozszerza krótką formę słowników { key: value, ...}na zwykłe i konwertuje je na prostszy styl yaml. Drugie sedwywołanie robi to samo w przypadku krótkiej notacji list i konwertuje [ entry, ... ]do listy pozycji z -notacją. Trzecie sedwywołanie jest oryginalne, które obsługuje normalne słowniki, teraz z dodatkiem do obsługi list z -wcięciami. awkCzęść wprowadza indeks dla każdego poziomu wcięcia i zwiększa się ją, gdy nazwa zmienna jest pusta (czyli podczas przetwarzania listy). Zamiast pustej nazwy używana jest bieżąca wartość liczników. Podczas wchodzenia o jeden poziom liczniki są zerowane.

Edycja: Utworzyłem do tego repozytorium github .


11

Trudno powiedzieć, ponieważ zależy to od tego, co parser ma wyodrębnić z dokumentu YAML. W prostych przypadkach, może być w stanie wykorzystać grep, cut, awkitd. W przypadku bardziej złożonych parsowania trzeba by użyć pełnowymiarową analizowania takich jak biblioteka Pythona PyYAML lub YAML :: Perl .


11

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 keystablicę zawierającą listę nazw kluczy, childrentablicę zawierającą nazwy tablic podrzędnych i parentklucz, 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_parseFunkcja najpierw lokalizuje inputplik 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 bashpolecenia 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 yaypomocnik:

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, awkktó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 prefixużyć dla bieżącego elementu. To jest dodawane do nazwy klucza, aby utworzyć nazwę tablicy. Istnieje root_prefixtablica 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_keyto 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 prefixi 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 bashpolecenia: 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 prefixi 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 keyslisty 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 childrenlisty 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_parsemogą być analizowane jako polecenia bash przez bash evallub sourcewbudowane polecenia.


Czy zastanawiałeś się nad stworzeniem tego projektu na GitHub? Czy to już jest?
Daniel

@ Daniel, jest w GitHub, ale nie we własnym repozytorium - można go znaleźć tutaj . Patrz examplesi usr/libkatalogów, są one połączone w mojej odpowiedzi na pytanie. Jeśli jest zainteresowanie, mógłbym je rozbić na własne repozytorium.
starfry

4
Kudos w YAY. Na początku przerobiłem go na czysty bash, ale potem nie mogłem się powstrzymać i zaimplementowałem go jako podstawowy parser ze wsparciem dla tablic i struktur zagnieżdżonych, które nie mogą narzucać się nawzajem. Jest na github.com/binaryphile/y2s .
Binarny Phile

5
perl -ne 'chomp; printf qq/%s="%s"\n/, split(/\s*:\s*/,$_,2)' file.yml > file.sh

przydatne tylko w przypadku konfiguracji płaskich. nie dotyczy to strukturyzowanego yamlu. inny, jak zapobiec używaniu tymczasowego pliku.sh?
Znik

5

Inną opcją jest konwersja YAML na JSON, a następnie użycie jq do interakcji z reprezentacją JSON w celu wydobycia z niej informacji lub ich edycji.

Napisałem prosty skrypt bash, który zawiera ten klej - patrz projekt Y2J na GitHub


2

Jeśli potrzebujesz jednej wartości, możesz użyć narzędzia, które konwertuje twój dokument YAML na JSON i podaje jqna przykład plik danych yq.

Zawartość sample.yaml:

---
bob:
  item1:
    cats: bananas
  item2:
    cats: apples
  thing:
    cats: oranges

Przykład:

$ yq -r '.bob["thing"]["cats"]' sample.yaml 
oranges

1

Wiem, że jest to bardzo specyficzne, ale myślę, że moja odpowiedź może być pomocna dla niektórych użytkowników.
Jeśli masz nodei npmzainstalowałeś na swoim komputerze, możesz użyć js-yaml.
Pierwsza instalacja:

npm i -g js-yaml
# or locally
npm i js-yaml

następnie w skrypcie bash

#!/bin/bash
js-yaml your-yaml-file.yml

Również jeśli używasz jq, możesz zrobić coś takiego

#!/bin/bash
json="$(js-yaml your-yaml-file.yml)"
aproperty="$(jq '.apropery' <<< "$json")"
echo "$aproperty"

Ponieważ js-yamlkonwertuje plik yaml na dosłowny ciąg json. Następnie możesz użyć łańcucha z dowolnym parserem json w systemie uniksowym.


1

Jeśli masz Python 2 i PyYAML, możesz użyć tego analizatora składni, który napisałem o nazwie parse_yaml.py . Jedną z fajniejszych rzeczy, jakie robi, jest wybranie prefiksu (w przypadku, gdy masz więcej niż jeden plik z podobnymi zmiennymi) i wybranie pojedynczej wartości z pliku yaml.

Na przykład, jeśli masz te pliki 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

Możesz załadować oba bez konfliktu.

$ 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

Nawet wiśnia wybiera wartości, które chcesz.

$ 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

1

Można użyć odpowiednik z yq , co jest napisane w golang:

./go-yg -yamlFile /home/user/dev/ansible-firefox/defaults/main.yml -key
firefox_version

zwroty:

62.0.3

0

Możesz również rozważyć użycie Grunt (JavaScript Task Runner). Może być łatwo zintegrowany z powłoką. Obsługuje odczytywanie plików YAML ( grunt.file.readYAML) i JSON ( grunt.file.readJSON).

Można to osiągnąć, tworząc zadanie w Gruntfile.js(lub Gruntfile.coffee), np .:

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':
        });
    });

};

następnie z poziomu powłoki wystarczy po prostu uruchomić grunt foo(sprawdź grunt --helpdostępne zadania).

Co więcej, możesz zaimplementować exec:foozadania ( grunt-exec) ze zmiennymi wejściowymi przekazanymi z zadania ( foo: { cmd: 'echo bar <%= foo %>' }) w celu wydrukowania wyniku w dowolnym formacie, a następnie potokowanie go do innego polecenia.


Istnieje również podobne narzędzie do Grunta, nazywa się gulp z dodatkową wtyczką gulp-yaml .

Zainstaluj przez: npm install --save-dev gulp-yaml

Przykładowe użycie:

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/'))

Aby uzyskać więcej opcji radzenia sobie z formatem YAML , sprawdź witrynę YAML pod kątem dostępnych projektów, bibliotek i innych zasobów, które mogą pomóc w analizie tego formatu.


Inne narzędzia:

  • Jshon

    analizuje, czyta i tworzy JSON


0

Wiem, że moja odpowiedź jest konkretna, ale jeśli ktoś już ma zainstalowane PHP i Symfony , bardzo przydatne może być użycie parsera YAML Symfony.

Na przykład:

php -r "require '$SYMFONY_ROOT_PATH/vendor/autoload.php'; \
    var_dump(\Symfony\Component\Yaml\Yaml::parse(file_get_contents('$YAML_FILE_PATH')));"

Tutaj po prostu użyłem var_dumpdanych wyjściowych przeanalizowanej tablicy, ale oczywiście możesz zrobić znacznie więcej ... :)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.